Virtual Cards

Learn how to create, manage, and control virtual payment cards using the TSPay API.

Table of contents

  1. Overview
  2. Create Virtual Card
    1. Endpoint
    2. Authentication
    3. Request Body
    4. Request Parameters
      1. Top Level
      2. config
      3. config.authorizationWindow
      4. config.tolerance
    5. Validation Rules
    6. Example Request (cURL)
    7. Success Response (201 Created)
    8. Response Fields
  3. Card Data Security
    1. TSPay Data Handling
  4. Card Authorization Behaviour
  5. Card Lifecycle States
  6. Funding
  7. Usage Types
    1. Single-Use Cards
  8. Spending Controls
    1. Card Limit
    2. Authorization Window
    3. Tolerance
  9. Metadata
    1. Reserved Metadata Keys
  10. Default Values
  11. Best Practices
    1. 1. Use Unique Idempotency Keys
    2. 2. Set Card Limit with Tolerance
    3. 3. Set Authorization Windows for Single-Use Cards
    4. 4. Leverage Metadata for Reconciliation
    5. 5. Handle Sensitive Card Data Securely
  12. Error Handling
    1. Common Errors
  13. 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=true places 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 currency field 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.percentage is omitted (default 3%), cardLimit will be higher than requestedCardLimit. When tolerance.percentage is explicitly set to 0, 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 only
  • cvc: "***" — 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=true is 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=true must 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=true is submitted by a credential that does not have the reveal permission enabled, the API returns 403 Forbidden. The card creation request is not processed — no card is created. Remove the revealDetails parameter 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: 1 accept 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 active status. State transitions (e.g., cancelling a card) will be available via PATCH /api/v1/issuing/cards/{cardId} in a future version. If a card transitions to inactive unexpectedly, contact TSPay support with the cardId for investigation.

Single-use card lifecycle: Cards configured with maxTransactions: 1 are automatically cancelled after the first approved authorization. The card status transitions to canceled and no further authorizations are accepted. A declined authorization attempt does not consume the transaction count — the card remains active until 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:00Z
End: 2025-01-10T13:00:00Z
Immediate purchases
24 hours Start: 2025-01-10T00:00:00Z
End: 2025-01-11T00:00:00Z
Same-day transactions
7 days Start: 2025-01-10T00:00:00Z
End: 2025-01-17T23:59:59Z
Travel bookings
30 days Start: 2025-01-01T00:00:00Z
End: 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 cardLimit field 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 returns 400 Bad Request.
  • Metadata is not visible to cardholders
  • Use snake_case for 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.

expiryDuration and authorizationWindow are independent controls. expiryDuration determines the card’s expiry date. authorizationWindow determines 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


Back to top

© 2026 Travelsoft Pay — Confidential Partner Documentation