Virtual Cards
Learn how to create, manage, and control virtual payment cards using the TSPay API.
Table of contents
- Overview
- Create Virtual Card
- Card Data Security
- Card Authorization Behaviour
- Card Lifecycle States
- Funding
- Usage Types
- Spending Controls
- Metadata
- Default Values
- Best Practices
- Error Handling
- Next Steps
Overview
Virtual cards are a primary capability of the TSPay API and enable controlled payment authorization for supplier transactions. The Version 1.0 API allows you to programmatically create Mastercard virtual commercial cards with fine-grained spending controls, suited for use cases such as:
Each API credential is associated with a specific issuing account. Permissions, supported currencies, and applicable card controls are determined by that account’s programme configuration.
- Travel bookings - Single-use cards for airline or hotel reservations
- Vendor payments - Controlled spending limits for supplier payments
Create Virtual Card
Create a new virtual card with customizable controls and metadata.
Endpoint
POST /api/v1/issuing/cards
Authentication
Requires a valid JWT access token in the Authorization: Bearer <token> header.
To receive plaintext card details (PAN and CVC), append ?revealDetails=true to the request URL. This requires the appropriate reveal permission on the calling credential and is always audit-logged.
When revealDetails is not used, the response returns masked pan and masked cvc values only.
Plaintext PAN and CVC are returned only at card creation time. TSPay does not support later retrieval of plaintext card data.
POST /api/v1/issuing/cards?revealDetails=true
Using
?revealDetails=trueplaces the calling system in PCI scope. PAN and CVC are returned only at card creation time — TSPay does not support later retrieval of plaintext card data. This parameter must only be used by systems operating within the appropriate PCI-DSS scope. See Card Data Security below.
Request Body
{
"requestId": "1230537f-e892-4678-b945-17bfb6d1a456",
"cardLimit": 10000,
"currency": "EUR",
"config": {
"expiryDuration": 12,
"authorizationWindow": {
"startDate": "2025-01-10T00:00:00Z",
"endDate": "2025-01-17T23:59:59Z"
},
"tolerance": {
"percentage": 5
},
"cardType": "MTA",
"maxTransactions": 1
},
"metadata": {
"card_name": "Travel bookings",
"file_ref": "AB1234",
"departure_date": "2025-01-10",
"project_id": "Project-Alpha-456",
"cost_center": "Marketing-Q3",
"airline_code": "BA",
"hotel_brand": "BOOKING"
}
}
Request Parameters
Top Level
| Field | Type | Required | Description |
|---|---|---|---|
requestId | UUID | Yes | Must be a valid UUID v4. Unique idempotency key for this request. Must be included in the request body. Repeated requests using the same requestId within the 24-hour idempotency window return the original card without creating a duplicate. |
cardLimit | integer | Yes | Requested spending limit in minor units following ISO 4217 currency exponent (e.g., EUR/GBP/USD → cents, so 10000 = 100.00; JPY → no decimals, so 10000 = ¥10,000). The effective limit returned may differ if a tolerance is configured. |
currency | string | Yes | Transaction currency for the card. Supported currencies depend on the issuing programme configuration of your issuing account. Contact TSPay support to confirm the currencies enabled for your account. |
config | object | No | Card configuration and controls. All defaults are applied server-side when fields are omitted. |
metadata | object | No | Custom key-value pairs for tracking (Map<String, String>, up to 50 pairs) |
config
| Field | Type | Required | Description |
|---|---|---|---|
expiryDuration | integer | No | Card validity period in months from creation date (default: 24, max: 60). Determines when the card itself expires — distinct from authorizationWindow. |
authorizationWindow | object | No | Time window during which merchant authorization attempts are accepted. Default: startDate = time the API request is received (UTC); endDate = 14 days after the time the API request is received (UTC). endDate must be greater than startDate. |
tolerance | object | No | Overage tolerance configuration. The effective cardLimit is increased by the tolerance percentage. Accepted values: 0 to 100. Default: 3. Setting percentage: 0 disables tolerance entirely. |
cardType | string | No | Card type. Supported value in v1.0: MTA (default). |
maxTransactions | integer | No | Maximum number of approved authorizations allowed. Default: 1 (single-use). When maxTransactions is 1, no further authorizations are accepted after the first approved authorization. Set to a higher value for multi-use cards. |
allowedCategories | array | No | TSPay supported merchant category identifiers to allow (e.g., "airlines_air_carriers"). Values must be within the platform-level allowlist — categories outside will be rejected. If omitted, the platform-level allowlist applies in full. See Merchant Categories for the complete category reference. |
Sending
"allowedCategories": []is equivalent to omitting the field — the platform-level allowlist applies in full. To restrict categories, provide at least one category name.
Sending a category outside the platform allowlist returns 400 Bad Request with a message indicating the invalid category name.
The
currencyfield is part of the API contract even when only a single currency is currently enabled for an issuing account. This ensures integrations remain stable when additional currencies are enabled in the future.
config.authorizationWindow
| Field | Type | Required | Description |
|---|---|---|---|
startDate | string | No | Authorization window start (ISO-8601 UTC). Default: Timestamp at which the API request was received. Must be in the future. |
endDate | string | No | Authorization window end (ISO-8601 UTC). Default: 14 days from startDate. Must be greater than startDate. |
config.tolerance
| Field | Type | Required | Description |
|---|---|---|---|
percentage | integer | No | Overage tolerance as a percentage (e.g., 5 for 5%). The API computes effectiveCardLimit = ceil(cardLimit × (1 + percentage / 100)) and returns it as cardLimit in the response. |
Validation Rules
| Field | Rule |
|---|---|
requestId | Required. Must be unique per logical card creation request within the issuing account. |
cardLimit | Must be an integer greater than 0. Expressed in minor units according to the currency exponent. There is no API-enforced maximum — however, the card can only be authorized if sufficient prefunded balance is available on the issuing account at authorization time. |
currency | Must be a supported ISO 4217 uppercase currency code enabled for the issuing account. |
config.expiryDuration | If provided, must be a positive integer number of months. Maximum: 60. |
config.authorizationWindow.startDate / endDate | Must be valid ISO-8601 UTC timestamps. endDate must be greater than startDate. |
config.tolerance.percentage | If provided, must be an integer between 0 and 100 inclusive. Setting 0 explicitly disables overage tolerance — the card limit enforced at authorization will equal the requested cardLimit exactly. Omitting the field applies the default tolerance of 3%. |
config.maxTransactions | If provided, must be an integer greater than or equal to 1. |
metadata | Keys and values must be strings. Maximum: 50 key-value pairs per card. |
config.allowedCategories | If provided, must be an array of valid category identifiers within the platform allowlist. If omitted, null, or empty, the platform allowlist applies in full. |
Example Request (cURL)
curl -X POST https://tspay-api.sandbox.travelsoftpay.com/api/v1/issuing/cards \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "1230537f-e892-4678-b945-17bfb6d1a456",
"cardLimit": 10000,
"currency": "EUR",
"config": {
"cardType": "MTA",
"maxTransactions": 1,
"tolerance": {
"percentage": 0
}
},
"metadata": {
"file_ref": "AB1234",
"departure_date": "2025-01-10",
"project_id": "Project-Alpha-456",
"cost_center": "Marketing-Q3",
"airline_code": "BA",
"hotel_brand": "BOOKING"
}
}'
Success Response (201 Created)
{
"cardId": "ic_1ScUKNPj0YljeLEKBRcVSSuO",
"pan": "************0054",
"cvc": "***",
"expMonth": 1,
"expYear": 2029,
"status": "active",
"requestedCardLimit": 10000,
"cardLimit": 10000,
"currency": "EUR",
"createdAt": "2025-01-10T14:30:00Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
cardId | string | Unique card identifier — use this for future queries and operations |
pan | string | Primary Account Number. Masked by default (e.g., ************0054). Plaintext only when revealDetails=true is used with the required permission. |
cvc | string | Card Verification Code. Masked by default (***). Plaintext only via revealDetails=true. |
expMonth | integer | Expiration month (1–12) |
expYear | integer | Expiration year (4 digits) |
status | string | Card status: active, canceled, or inactive |
requestedCardLimit | integer | The cardLimit value submitted in the request, in minor units, before tolerance is applied. |
cardLimit | integer | Effective spending limit in minor units after tolerance is applied. This is the actual limit enforced at authorization time. |
currency | string | ISO 4217 currency code (uppercase, e.g., EUR) |
createdAt | string | Card creation timestamp (ISO-8601 UTC) |
When
tolerance.percentageis omitted (default3%),cardLimitwill be higher thanrequestedCardLimit. Whentolerance.percentageis explicitly set to0, both fields return the same value.
Card Data Security
PCI-DSS: Handle all card data according to PCI-DSS requirements. Never log or store PAN or CVC values in plaintext.
By default, the API returns masked values:
pan:"************0054"— last 4 digits onlycvc:"***"— always masked unless reveal is used
To retrieve full card details, add ?revealDetails=true to the request URL. This requires the required permission on your credential and every call is audit-logged by TSPay.
Important rules for reveal:
- PAN and CVC values are returned only once at card creation when
revealDetails=trueis used - TSPay does not allow retrieval of plaintext PAN after creation
- Access requires specific permissions granted to the API credential
- These requests must only be performed by systems operating within the appropriate PCI-DSS scope
- Use of
revealDetails=truemust be explicitly enabled for the calling credential and approved for the relevant PCI-controlled use case
curl -X POST "https://tspay-api.sandbox.travelsoftpay.com/api/v1/issuing/cards?revealDetails=true" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
...
If
?revealDetails=trueis submitted by a credential that does not have the reveal permission enabled, the API returns403 Forbidden. The card creation request is not processed — no card is created. Remove therevealDetailsparameter or contact TSPay support to enable the permission for your credential before retrying.
TSPay Data Handling
TSPay does not store or log PAN or CVC values. Card data received from the issuing processor is transmitted to the API response at creation time only and is not persisted in TSPay systems. This design ensures that TSPay’s infrastructure does not introduce additional PCI DSS scope for partners who do not use revealDetails=true.
When revealDetails=true is not used, no plaintext card data transits the partner’s infrastructure, limiting PCI DSS scope to the card scheme network and the issuing processor.
Card Authorization Behaviour
Cards enforce the following rules at every authorization attempt:
- Cards can only be authorized within the configured
authorizationWindow. Authorization attempts outside this window are declined regardless of card status or available limit. - The authorization amount cannot exceed the configured
cardLimit(the effective limit after tolerance is applied). - Multiple authorization attempts may occur depending on merchant behaviour (e.g., pre-authorizations, reversals).
- If sufficient available balance is not present when authorization is attempted, the authorization is declined.
- Cards with
maxTransactions: 1accept no further authorizations after the first approved authorization attempt. - Cards can be canceled before authorization to prevent further use.
Card Lifecycle States
| Status | Description |
|---|---|
active | Card is valid and can be authorized by merchants within the configured controls |
canceled | Card is permanently disabled. No further authorizations are accepted. Cancellation cannot be reversed. |
inactive | Card is temporarily disabled. No further authorizations are accepted. |
In v1.0, cards are created with
activestatus. State transitions (e.g., cancelling a card) will be available viaPATCH /api/v1/issuing/cards/{cardId}in a future version. If a card transitions toinactiveunexpectedly, contact TSPay support with thecardIdfor investigation.
Single-use card lifecycle: Cards configured with
maxTransactions: 1are automatically cancelled after the first approved authorization. The card status transitions tocanceledand no further authorizations are accepted. A declined authorization attempt does not consume the transaction count — the card remainsactiveuntil an authorization is approved.
Funding
TSPay operates a prefunded issuing model.
Card creation does not check or reserve available balance. A partner may therefore create cards before funds are available on the issuing account.
Available balance is checked only when a merchant authorization is attempted. At that point, funds are ring-fenced for the authorization amount if sufficient balance is available. If sufficient balance is not available, the authorization is declined.
Funding transfers may be subject to processing delays depending on payment rail timing and banking cut-off windows.
Cards cannot be authorized against a credit line. Sufficient prefunded balance must be available at authorization time.
Usage Types
Single-Use Cards
Best for one-time transactions:
{
"config": {
"maxTransactions": 1
}
}
Behavior:
- After the first approved authorization, the card is automatically cancelled. No further authorizations are accepted.
- Ideal for vendor payments, travel bookings
- Reduces fraud exposure
Spending Controls
Card Limit
Set the maximum spending in minor units (e.g., cents). All amounts are integers:
{
"cardLimit": 100000
}
This sets a 1,000.00 EUR limit. Note: amounts are in minor units, not major units (e.g., use 100000 for 1 000 EUR, not 1000).
Limit Enforcement:
- Checked at authorization time
- Includes all currently authorized amounts, even if not yet settled
Authorization Window
Set the time window during which authorizations are accepted:
{
"config": {
"authorizationWindow": {
"startDate": "2025-01-10T00:00:00Z",
"endDate": "2025-01-17T23:59:59Z"
}
}
}
endDate must be greater than startDate. Authorizations attempted outside this window are declined.
Common Authorization Windows:
| Duration | Example Window | Use Case |
|---|---|---|
| 1 hour | Start: 2025-01-10T12:00:00ZEnd: 2025-01-10T13:00:00Z | Immediate purchases |
| 24 hours | Start: 2025-01-10T00:00:00ZEnd: 2025-01-11T00:00:00Z | Same-day transactions |
| 7 days | Start: 2025-01-10T00:00:00ZEnd: 2025-01-17T23:59:59Z | Travel bookings |
| 30 days | Start: 2025-01-01T00:00:00ZEnd: 2025-01-31T23:59:59Z | Monthly subscriptions |
Shorter authorization windows reduce fraud exposure. Set the window to match the expected transaction timeframe.
Tolerance
Allow a configurable overage on the card limit to absorb price variations (e.g., taxes, fees, currency fluctuations):
{
"cardLimit": 10000,
"config": {
"tolerance": {
"percentage": 5
}
}
}
How it works:
- The API computes:
effectiveCardLimit = ceil(cardLimit × (1 + percentage / 100)) - The result is always rounded up to the nearest minor unit (cent), ensuring the card never under-covers the intended amount
- The response
cardLimitfield returns the effective limit — this is the actual limit enforced at authorization
Examples (5% tolerance):
Requested cardLimit | Calculation | effectiveCardLimit |
|---|---|---|
10000 (100.00 EUR) | ceil(10000 × 1.05) = ceil(10500.00) | 10500 (105.00 EUR) |
10001 (100.01 EUR) | ceil(10001 × 1.05) = ceil(10501.05) | 10502 (105.02 EUR) |
9999 (99.99 EUR) | ceil(9999 × 1.05) = ceil(10498.95) | 10499 (104.99 EUR) |
Metadata
Attach custom string key-value pairs to cards for tracking and reconciliation:
{
"metadata": {
"invoice_id": "INV-2025-1234",
"supplier": "Acme Corp",
"po_number": "PO-5678",
"cost_center": "engineering"
}
}
Metadata rules:
- Up to 50 key-value pairs per card
- Keys and values must be strings
- Maximum key length: 64 characters
- Maximum value length: 512 characters
- Keys must not be blank
- Keys starting with
tspay_are reserved for internal use by TSPay. Submitting a key with this prefix returns400 Bad Request. - Metadata is not visible to cardholders
- Use
snake_casefor key names for consistency
Privacy: Do not store personally identifiable information (PII) — such as names, passport numbers, email addresses, or booking reference data that could identify an individual — in metadata fields. Partners are solely responsible for ensuring their use of the metadata field complies with applicable data protection regulations (including GDPR). TSPay applies no technical restriction on metadata content; compliance is a partner obligation under the TSPay partner agreement.
Reserved Metadata Keys
TSPay writes the following internal metadata keys automatically. These keys cannot be set or overridden by partners:
| Key | Description |
|---|---|
tspay_requested_card_limit | The original cardLimit submitted in the request, before tolerance |
tspay_applied_tolerance_percentage | The tolerance.percentage applied at card creation |
If a partner submits a key starting with tspay_, the API returns:
{
"correlationId": "...",
"status": 400,
"message": "Metadata key 'tspay_requested_card_limit' uses a reserved prefix",
"details": {
"field": "metadata.key",
"invalidValue": "tspay_requested_card_limit"
},
"timestamp": "2025-01-10T14:30:00Z"
}
Default Values
When optional fields are omitted, the following defaults are applied server-side:
| Field | Default |
|---|---|
expiryDuration | 24 months from creation |
authorizationWindow.startDate | Time the API request is received |
authorizationWindow.endDate | 14 days from startDate |
tolerance.percentage | 3% |
cardType | MTA |
maxTransactions | 1 (single-use) |
allowedCategories | Platform-level allowlist |
Cards default to
maxTransactions = 1, which limits the number of approved authorization events. This may be appropriate for simple one-time supplier payments, but not for merchant flows that involve pre-authorizations, incremental authorizations, delayed capture, or re-presentment, such as hotels.
Hotel, car rental, cruise, and other travel merchants may require more than one authorization event for a single booking lifecycle.
expiryDurationandauthorizationWindoware independent controls.expiryDurationdetermines the card’s expiry date.authorizationWindowdetermines when merchant authorization attempts are accepted. A card valid for 24 months can be restricted to accept authorizations only within a specific 14-day window.
Best Practices
1. Use Unique Idempotency Keys
Always provide a unique requestId to prevent duplicate card creation on retries. If a repeated request uses the same requestId within the idempotency window, TSPay returns the original result even if the new request payload differs. The new payload is ignored and no update is applied.
// Recommended: Use UUID v4
// crypto.randomUUID() requires Node.js >= 14.17
const requestId = crypto.randomUUID();
// e.g., "1230537f-e892-4678-b945-17bfb6d1a456"
2. Set Card Limit with Tolerance
Use the tolerance field rather than manually inflating the cardLimit. TSPay will compute and return the effective limit:
{
"cardLimit": 10000,
"config": {
"tolerance": {
"percentage": 5
}
}
}
Response returns cardLimit: 10500. This is the actual limit enforced at authorization.
3. Set Authorization Windows for Single-Use Cards
Always set authorizationWindow for single-use cards to minimize the exposure window:
{
"config": {
"maxTransactions": 1,
"authorizationWindow": {
"startDate": "2025-01-10T00:00:00Z",
"endDate": "2025-01-17T23:59:59Z"
}
}
}
4. Leverage Metadata for Reconciliation
Include reference data to simplify downstream reconciliation:
{
"metadata": {
"invoice_id": "INV-2025-1234",
"supplier": "Acme Corp",
"po_number": "PO-5678"
}
}
5. Handle Sensitive Card Data Securely
// NEVER do this
console.log('Card created:', cardResponse);
// Do this instead
const { pan, cvc, ...safeData } = cardResponse;
sendToPaymentGateway({ pan, cvc }); // Use immediately, do not store
Error Handling
All error responses follow a consistent structure:
{
"correlationId": "2aaa9f82-4873-4ba9-a0a3-e2228ff25078",
"status": 400,
"message": "cardLimit must be at least 1",
"details": {},
"timestamp": "2025-01-10T14:30:00Z"
}
Always provide the correlationId when contacting TSPay support.
For the complete list of HTTP status codes, see the API Reference. For supported merchant category identifiers, see the Merchant Categories reference.
Common Errors
Invalid Card Limit (400)
{
"correlationId": "...",
"status": 400,
"message": "cardLimit must be at least 1",
"details": {},
"timestamp": "2025-01-10T14:30:00Z"
}
Unsupported Currency (400)
{
"correlationId": "...",
"status": 400,
"message": "Currency not supported for this issuing account",
"details": {
"field": "currency",
"invalidValue": "USD"
},
"timestamp": "2025-01-10T14:30:00Z"
}
Duplicate Request ID (409)
Returned when the same requestId is submitted outside the 24-hour idempotency window and a card already exists with that key. Within the 24-hour window, the original card response is returned regardless of payload changes — no new card is created and no error is raised.
{
"correlationId": "...",
"status": 409,
"message": "Card already exists for this requestId",
"details": {},
"timestamp": "2025-01-10T14:30:00Z"
}
Next Steps
- Getting Started - Complete integration guide
- Authentication - Learn about JWT authentication
- Merchant Categories - Configure spending controls with merchant category filters