Card Issuing

Card Sandbox Simulation

Due to the complexity of card issuing, Column supports end-to-end card program testing in Sandbox from creating the program, to simulating an authorization, a capture, and a refund against a card.

Prerequisites

To simulate card transactions in Sandbox, start by creating a non-root entity. Debit card programs require a bank account for each non-root entity. Charge and credit card programs require a loan object for each non-root entity. Cardholder-secured charge card programs require a secured loan for each non-root entity, backed by a collateral bank account that holds the cardholder's deposit. Before simulating card transactions, a bank account must have a non-zero balance. A loan object must have a non-zero maximum principal balance and available credit.

If you plan to test real-time decisioning, please review webhook setup requirements as well prior to simulating card transactions. You can also simulate card transactions without real-time decisioning as described below.

Card Program

In Production, Column will configure a card program on your behalf. However, in Sandbox you can create a card program with POST /simulate/issuing/card-programs specifying the program type.

curl https://api.column.com/simulate/issuing/card-programs \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "type": "debit",
      "scheme": "visa",
      "description": "sandbox card program"
  }'
Field
Required
Description
type
Yes
debit, credit, or charge.
scheme
No
visa. Defaults to visa.
credit_policy
No
unsecured, cardholder_secured, or platform_secured. Defaults to unsecured. Ignored for debit programs.
jit_funding_enabled
No
true enables just-in-time funding. Debit programs only. Defaults to false.
description
No
Free-form description, max 255 characters.

The response is a card program object. Sandbox assigns and activates a BIN range for you, which allows cards created under the program to be resolved from simulated network messages.

Card Authorization Policies

Once a card program has been created, you can create card authorization policies. Policies are evaluated before real-time decisioning, so they are the cheapest way to enforce baseline merchant and velocity rules.

Create a policy and its first version with POST /issuing/card-authorization-policies.

curl https://api.column.com/issuing/card-authorization-policies \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "scope": "card_program",
      "effective_on": "<RFC 3339 timestamp at least 5 minutes from now>",
      "config": {
        "blocked_merchant_category_codes": ["7995"],
        "allowed_merchant_country_codes": ["US"],
        "card_spend_policies": [
          {
            "scope": "card_program",
            "interval": "1_day",
            "total_volume": 100000,
            "number_of_transactions": 20
          }
        ]
      }
  }'
Field
Required
Description
scope
Yes
card_program, card_account, or card.
effective_on
Yes
When the version becomes effective, as an RFC 3339 timestamp. Offsets are converted to UTC.
config
Yes
The category and spend rules. See Card Authorization Policies.

To change authorization rules later, add a new version to the existing policy with POST /issuing/card-authorization-policy-versions, passing the card_authorization_policy_id along with the same scope, a new effective_on, and the new config.

The active policy version is the one with the most recent effective_on that has already passed. You must set effective_on at least 5 minutes in the future, so wait for the version to become effective before you simulate transactions against it.

A policy is only evaluated once it is attached to a card program, card account, or card. Attach a card_program-scoped policy by updating your card program with PATCH /issuing/card-programs/:card_program_id:

curl https://api.column.com/issuing/card-programs/cpgm_3J6EP7ebC8vIwpP6lQfvy371JFQ \
  -XPATCH \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "card_authorization_policy_id": "caup_2x8gszy5folpA9s0TOCseE9ABDM"
  }'

card_account-scoped and card-scoped policies are attached by passing card_authorization_policy_id when you create the card account or card, or later by updating the card account with PATCH /issuing/card-accounts/:card_account_id or updating the card with PATCH /issuing/cards/:card_id.

Card Accounts

You can now create card accounts under your card program with applicable card authorization policies. For a debit program, the card account links to the bank account that funds it. For a credit program, the card account links to the line of credit that funds it. See Card Account Types for how each program type links its card accounts.

curl https://api.column.com/issuing/card-accounts \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "card_program_id": "cpgm_3J6EP7ebC8vIwpP6lQfvy371JFQ",
      "bank_account_id": "bacc_2wQFHhraSXePmI1rC2uhnodtvh2"
  }'
Field
Required
Description
card_program_id
Yes
The program to open the card account under.
bank_account_id
Debit only
Bank account backing the card account. Must be omitted for credit and charge programs.
loan_id
Credit/Charge only
Loan backing the card account. For a cardholder_secured program, the loan must have type=secured; its collateral bank account is derived from the loan and cannot be overridden.
card_authorization_policy_id
No
Include only if policy is scoped to a card account.

Cards

Next, create a card. Virtual cards are the fastest path to testing in Sandbox as shipping details are not required.

curl https://api.column.com/issuing/cards \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "type": "virtual",
      "card_account_id": "cacc_3J6FHczL9wF2o3xsoVbOxK2Zu60",
      "authorized_user_entity_id":"enti_3BPEJh4QweINlzZxBH1oZ94qrAQ",
      "status":"active"
  }'
Field
Required
Description
type
Yes
virtual or physical.
card_account_id
Yes
The card account to create the card under.
authorized_user_entity_id
Yes
The person entity authorized to use the card.
shipping_details
Physical only
card_authorization_policy_id
No
Policy applied at the card scope.
card_template_id
No
Card template to use. Must belong to the card account's card program.
expiration_month / expiration_year
No
Override the generated expiration date.
status
No
Initial status for the card. By default status is "paused" if not explicitly set to "active".
encrypted_pin
No
Base64-encoded encrypted PIN. Leave it out to create the card without a PIN and set one later.
generate_pin
No
Cards on a program with PIN reveal enabled only. true has Column set a random PIN, which the cardholder sees through PIN reveal. Can't be combined with encrypted_pin.

The PAN and CVV2 are under PCI scope and are not returned when you create a card. You do not need them to simulate transactions — the simulate endpoints address the card by its card_id. If you are building a cardholder-facing card display, show card details with Column's card reveal SDKs for the web and React Native. Your server calls POST /issuing/cards/:card_id/reveal-tokens to create a single-use, short-lived reveal token, and the SDK uses that token to display the card details to the cardholder, so the card number and CVV2 never pass through your systems.

PIN Encryption

A card doesn't need a PIN. Pass encrypted_pin to set one when you create the card, or leave it out to create the card without a PIN. A card without a PIN has pin_status set to unset; it works for transactions that don't require a PIN, and transactions that do decline with pin_not_set until you set one. Renewals and replacements that keep the card number carry over the original card's PIN when encrypted_pin is omitted; replacements that issue a new card number are created without a PIN. See PINs on renewed and replaced cards. The PIN is the customer's chosen 4-digit code, encrypted with RSA-2048, OAEP padding, SHA-256 hash, then base64-encoded.

Encrypt the PIN on the cardholder's device, in your mobile app or web frontend, as soon as they enter it. Send only the base64 ciphertext to your servers and on to Column. Never send a plain-text PIN to your backend or encrypt it there, so the PIN never appears in your systems or logs. Don't store or reuse an encrypted PIN beyond the request that sets it.

Sandbox public key:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApBDtgxhJhZCmbvXWr7Zz
xs8u9JrSVWjtIDcIsD+5cfyv0OsWgjTxBIoBhUWeoTMizEGWWmLl9l0y/fWXFar3
wTriE+sun/cdFydCaBzB0nlyVqH9lIoKhsmpZUOsBS4Wrsnfb6GBzWFNlGu9ZkQE
+sXAEpf5G6gWKIoU6RACbslt0h79xW/EIeagOyl6MCW8Jtb8DsYwItGpVwoPPfU2
1XzmHBgeGTUOtRAWT1Ky//t5P9jcd5THqZjLBUqIq3K9Rz7OL/uY2oNFy57hdSWv
dXd0gnSdFKxOH6BrgTdUXkOU/ytdweOIcRgSGDVYKEHQuZa93XJirH3AY+Xk21CU
kwIDAQAB
-----END PUBLIC KEY-----

This single key serves every sandbox platform on Column. It works only against test_-prefixed API credentials — production uses a different key issued during onboarding.

To set this on an already created card, update the card's PIN with PATCH /issuing/cards/:card_id/pin:

curl https://api.column.com/issuing/cards/card_3J6FHczL9wF2o3xsoVbOxK2Zu60/pin \
    -XPATCH \
    -H 'Content-Type: application/json' \
    -u :<YOUR API KEY> \
    --data '{
        "encrypted_pin": "<base64 ciphertext>"
    }'

The API never returns a card's PIN. If PIN reveal is enabled for the card program, the cardholder can see it through the card reveal SDKs. To have the cardholder confirm their current PIN before changing it, also pass current_encrypted_pin, encrypted the same way as encrypted_pin. The PIN only changes if it matches the card's current PIN; otherwise the request fails with current_pin_incorrect and the PIN is unchanged. Sending current_encrypted_pin for a card with no PIN fails with invalid_field_value. Column doesn't limit attempts, so limit them on your side.

Treat an encrypted PIN like the PIN itself: never log it, store it, or reuse it. The current-PIN check only proves that the caller has the encrypted PIN, not that they know the PIN. If you keep the encrypted_pin from an earlier request, anyone who gets hold of it, such as someone who takes over the cardholder's account with you, could replay it as current_encrypted_pin to pass the check and set a new PIN.

Activate Card

A card must be active before it will authorize. If the card was created in a paused state, activate it:

curl https://api.column.com/issuing/cards/card_3J6Gg2WgNLoZAz67PrHhJy9aHlT/activate \
  -XPATCH \
  -u :<YOUR API KEY>

Simulate Card Transaction

With a funded, active card you can drive the full network message lifecycle. Every simulate endpoint returns the resulting card transaction object, so you can inspect amounts and events after each step.

Step
Endpoint
Authorization
POST /simulate/issuing/cards/:card_id/card-transactions/authorization
Incremental authorization
POST /simulate/issuing/card-transactions/:card_transaction_id/incremental-authorization
Capture
POST /simulate/issuing/card-transactions/:card_transaction_id/capture
Reversal
POST /simulate/issuing/card-transactions/:card_transaction_id/reversal
Refund
POST /simulate/issuing/card-transactions/:card_transaction_id/refund
Single-message financial
POST /simulate/issuing/cards/:card_id/card-transactions/financial
3DS authentication
POST /simulate/issuing/cards/:card_id/threeds/challenge

All amounts are expressed in the smallest unit of the merchant's currency and must be greater than 0 and no more than 999999999999.

Authorization

Authorization places a hold on the card account. It is the entry point for exercising both authorization policies and real-time decisioning.

curl https://api.column.com/simulate/issuing/cards/card_3J6Gg2WgNLoZAz67PrHhJy9aHlT/card-transactions/authorization \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 2500,
      "merchant_currency": "USD",
      "realtime_decision":"approve"
  }'
Field
Required
Description
cardholder_amount
Yes
Amount in the merchant's currency, in the smallest currency unit.
merchant_currency
Yes
Merchant's ISO 4217 currency code.
cardholder_fx_rate
Non-USD only
Rate converting the merchant currency to USD, the cardholder billing currency.
merchant_category_code
No
ISO 18245 MCC, e.g. 5812 (restaurants) or 6011 (ATM). Defaults to 5999.
merchant_name
No
Card acceptor name, max 25 characters. Defaults to ACQUIRER NAME.
merchant_city
No
Card acceptor city, max 13 characters. Defaults to CITY NAME.
merchant_country_code
No
Card acceptor country, ISO 3166-1 alpha-2. Defaults to US.
realtime_decision
No
Inject an issuer decision: approve, decline, or partially_approve.
realtime_decision_reason
No
Decline reason when realtime_decision is decline. Defaults to generic_decline.
realtime_partially_approved_amount
Conditional
Required when realtime_decision is partially_approve. In USD, the cardholder billing currency. Must be greater than 0 and less than the billed amount (cardholder_amount, converted at cardholder_fx_rate for non-USD merchants).
realtime_funding_account_id
No
Funding account the injected decision names, to test just-in-time funding. Only when realtime_decision is approve or partially_approve.
network_standin
No
Simulate the network deciding on the issuer's behalf: approved or declined.
network_decision_reason
No
Decline reason when network_standin is declined.
three_ds_attempt_id
No
Links an authenticated 3DS attempt on the same card, producing authentication details with liability shift. See 3DS Authentication.
cardholder_input.pin
No
The PIN the cardholder enters at the terminal, as 4 to 12 digits in plaintext. Physical cards only. See Testing PINs.
entry_mode
No
How the card is presented: swipe, keyed or ecommerce. Defaults to swipe for physical cards and ecommerce for virtual cards. See Testing entry modes.
cardholder_input.cvv2
No
The CVV2 the cardholder enters, as 3 or 4 digits. Only with entry_mode keyed or ecommerce; defaults to the card's real CVV2. See Testing CVV2.
cardholder_input.expiration_date
No
The expiration date the cardholder presents, as {"month": 12, "year": 2029}. See Testing expiration dates.
cardholder_input.billing_address
No
The street and postal_code the cardholder enters, checked by address verification (AVS). See Testing address verification.

Use the merchant fields to exercise authorization policy and real-time decisioning rules that depend on the merchant category or country. For example, with the authorization policy created above, an authorization with merchant_category_code set to 7995 is declined before it reaches your decisioning endpoint.

Testing your real-time decisioning endpoint. When you omit both realtime_decision and network_standin, the simulator sends a real decision webhook to the endpoint your platform has subscribed to decision.issuing.card_transaction and waits for your response. If no destination is configured, the authorization declines. Set realtime_decision instead when you want to exercise a specific outcome without standing up an endpoint. See Real-time Decisioning for the full decisioning flow.

realtime_decision and network_standin are mutually exclusive — sending both is rejected. network_standin produces a transaction with decision_source set to card_network, which is how you can simulate an issuer timeout.

The response contains the new card_transaction_id (prefixed ctxn_) used by the remaining steps, and emits issuing.card_transaction.created.

Testing PINs

To simulate a cardholder typing a PIN at the terminal, pass it in cardholder_input.pin on an authorization or a single-message financial for a physical card. The simulator encrypts it the way a terminal would, and Column checks it with the same PIN verification and lockout as in Production:

curl https://api.column.com/simulate/issuing/cards/card_3J6Gg2WgNLoZAz67PrHhJy9aHlT/card-transactions/authorization \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 2500,
      "merchant_currency": "USD",
      "realtime_decision": "approve",
      "cardholder_input": {"pin": "1234"}
  }'
Outcome
decisioning_details on the first event
The PIN matches the card's PIN
The transaction continues to decisioning.
The PIN doesn't match
decision declined, decision_reason pin_invalid, decision_source column.
The card has no PIN
decision_reason pin_not_set.
The card's PIN is blocked
decision_reason pin_blocked, whatever the PIN.

Three wrong PINs in a row block the card's PIN. Unblock it with POST /issuing/cards/:card_id/pin/unblock to test again. A PIN on a virtual card is rejected with 400, since a virtual card has no terminal.

Testing entry modes

Set entry_mode to choose how the card is presented to the merchant, so you can test card-present and card-not-present decisioning. It sets the PAN entry mode and POS condition code on the simulated network message, and the authorization's point_of_sale_details reflect it:

entry_mode
Presentation
pan_entry_mode
card_presence
terminal_type
swipe
The magnetic stripe is read at an attended terminal. Column verifies the card's track data.
swipe
present
attended
keyed
The card number is typed into the terminal by hand, with the card's CVV2.
manual_entry
not_present
unknown
ecommerce
The card is used at an online checkout, with the card's CVV2.
manual_entry
not_present
ecommerce

A physical card supports all three and defaults to swipe. A virtual card only supports ecommerce, its default. A PIN in cardholder_input.pin can be entered with swipe or keyed, but not with ecommerce, which has no PIN pad. These combinations are rejected with 400.

Chip and contactless entry are not supported: they need an EMV cryptogram that the simulator cannot generate under sandbox card keys.

Testing CVV2

A keyed or online transaction carries the card's real CVV2. To simulate a cardholder typing a different CVV2, pass it in cardholder_input.cvv2 with entry_mode keyed or ecommerce, a virtual card's default. Column checks it with the same CVV2 verification as in Production:

curl https://api.column.com/simulate/issuing/cards/card_3J6Gg2WgNLoZAz67PrHhJy9aHlT/card-transactions/authorization \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 2500,
      "merchant_currency": "USD",
      "realtime_decision": "approve",
      "entry_mode": "ecommerce",
      "cardholder_input": {"cvv2": "123"}
  }'
Outcome
decisioning_details on the first event
The CVV2 matches the card's CVV2
The transaction continues to decisioning.
The CVV2 doesn't match
decision declined, decision_reason invalid_cvv2, decision_source column.

A CVV2 with entry_mode swipe, a physical card's default, is rejected with 400, since the cardholder doesn't enter a CVV2 when the card is swiped. On a single-message financial, which has no entry_mode, a CVV2 is only supported on a virtual card.

Testing expiration dates

To simulate the expiration date the cardholder presents, pass it in cardholder_input.expiration_date with a month (1 to 12) and a 4-digit year (2000 to 2068). Column compares it with the card's expiration_month and expiration_year, as it does in Production:

curl https://api.column.com/simulate/issuing/cards/card_3J6Gg2WgNLoZAz67PrHhJy9aHlT/card-transactions/authorization \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 2500,
      "merchant_currency": "USD",
      "realtime_decision": "approve",
      "cardholder_input": {"expiration_date": {"month": 12, "year": 2029}}
  }'
Outcome
decisioning_details on the first event
The date matches the card's expiration
The authorization continues to decisioning.
The date doesn't match
decision declined, decision_reason invalid_expiration, decision_source column.

Without expiration_date, the simulated transaction carries no expiration date and Column skips the check. A card past its own expiration declines with card_expired whatever date is presented. The same field and outcomes apply to a single-message financial.

Testing address verification

To simulate a cardholder entering a billing address, for example at checkout or at a fuel pump, pass it in cardholder_input.billing_address on an authorization or a single-message financial. The simulator sends it in the Visa address verification (AVS) fields, and Column compares each field with the address of the card's authorized user entity, as in Production:

curl https://api.column.com/simulate/issuing/cards/card_3J6Gg2WgNLoZAz67PrHhJy9aHlT/card-transactions/authorization \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 2500,
      "merchant_currency": "USD",
      "realtime_decision": "approve",
      "cardholder_input": {"billing_address": {"street": "123 Main St", "postal_code": "94105"}}
  }'

The results appear in the transaction's cardholder_verification_details:

Field
street_address_verification_status / postal_code_verification_status
Matches the address on file
verified
Doesn't match
invalid
Omitted
not_provided

The street matches on its digits (the house number, plus any unit number), so 123 Main St matches 123 Main Street. A 5-digit ZIP matches a ZIP+4 on file with the same first 5 digits; send a ZIP+4 as 9 digits without the hyphen. AVS is informational: a mismatch doesn't decline the transaction, and your real-time decisioning can read the results. street takes 1 to 40 printable ASCII characters and postal_code 1 to 9 letters, digits, spaces or hyphens; anything else is rejected with 400.

Incremental Authorization

Incremental authorization adds to an existing hold, commonly seen with hotels, gas stations, and rideshare companies.

curl https://api.column.com/simulate/issuing/card-transactions/ctxn_3J6f3h8oQ6J0SWTTNQOdEiy43pg/incremental-authorization \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 500
  }'
Field
Required
Description
cardholder_amount
Yes
Additional amount to add to the existing hold.
fx_rate
No
Rate for multi-currency transactions. Defaults to the original transaction's rate.

The original transaction must have started as an authorization and must not have captured funds. Currency codes carry over from the original authorization. Each increment sends a real-time decision request to your endpoint subscribed to decision.issuing.card_transaction, and declines if none is configured.

Capture

Capture clears the transaction and moves funds. By default a capture is final and releases the entire remaining hold. A capture will not succeed if there is no authorization hold to capture.

curl https://api.column.com/simulate/issuing/card-transactions/ctxn_3J6f3h8oQ6J0SWTTNQOdEiy43pg/capture \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 2500
  }'
Field
Required
Description
cardholder_amount
Yes
Amount to capture.
is_partial_capture
No
When true, keeps the remaining hold active for future captures (split shipment). Defaults to false.

On a final capture, over-capture is permitted so you can simulate tips, hotel incidentals, and fuel charges. On a partial capture the amount must not exceed the remaining authorized amount.

Each capture appends a clear event to the card transaction and emits issuing.card_transaction.updated. See Card Transactions for how the six amount types roll up across events — negative amounts are funds held or withdrawn from the card account, positive amounts are funds released or deposited to the card account.

Single-Message Financial

Single-message financial simulates a transaction that authorizes and clears in a single network message, so there is no authorization hold before capture. Examples include ATM transactions and debit purchases.

curl https://api.column.com/simulate/issuing/cards/card_3J6FHczL9wF2o3xsoVbOxK2Zu60/card-transactions/financial \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 4000,
      "merchant_currency": "USD",
      "realtime_decision":"approve"
  }'
Field
Required
Description
cardholder_amount
Yes
Amount in the merchant's currency, in the smallest currency unit.
merchant_currency
Yes
Merchant's ISO 4217 currency code.
cardholder_fx_rate
Non-USD only
Rate converting the merchant currency to USD, the cardholder billing currency.
merchant_category_code
No
ISO 18245 MCC. Defaults to 5999.
merchant_name
No
Card acceptor name, max 25 characters. Defaults to ACQUIRER NAME.
merchant_city
No
Card acceptor city, max 13 characters. Defaults to CITY NAME.
merchant_country_code
No
Card acceptor country, ISO 3166-1 alpha-2. Defaults to US.
realtime_decision
No
Inject an issuer decision: approve, decline, or partially_approve.
realtime_decision_reason
No
Decline reason when realtime_decision is decline. Defaults to generic_decline.
realtime_partially_approved_amount
Conditional
Required when realtime_decision is partially_approve. In USD, the cardholder billing currency. Must be greater than 0 and less than the billed amount (cardholder_amount, converted at cardholder_fx_rate for non-USD merchants).
realtime_funding_account_id
No
Funding account the injected decision names, to test just-in-time funding. Only when realtime_decision is approve or partially_approve.
cardholder_input.pin
No
The PIN the cardholder enters at the terminal, as 4 to 12 digits in plaintext. Physical cards only. See Testing PINs.
cardholder_input.cvv2
No
The CVV2 the cardholder enters at an online checkout, as 3 or 4 digits. Virtual cards only; defaults to the card's real CVV2. See Testing CVV2.
cardholder_input.expiration_date
No
The expiration date the cardholder presents, as {"month": 12, "year": 2029}. See Testing expiration dates.
cardholder_input.billing_address
No
The street and postal_code the cardholder enters, checked by address verification (AVS). See Testing address verification.

PIN debit purchases are usually single-message financials. To simulate one, pass the PIN in cardholder_input.pin. The PIN outcomes are the same as on an authorization. Setting merchant_category_code to 6011 puts the transaction at an ATM merchant category, but the simulator always sends a purchase, so the transaction's type is purchase rather than cash_withdrawal.

As with an authorization, when you omit realtime_decision the simulator sends a real decision webhook to the endpoint your platform has subscribed to decision.issuing.card_transaction, and declines if no destination is configured. A partially approved financial transaction settles only the approved amount. Card status, authorization policy, and balance checks run first and can decline a financial transaction before it reaches your endpoint. There is no network_standin field for financial transactions.

Reversal

Reversal simulates an acquirer reversing the message that opened a transaction, such as a voided sale or a terminal that never received its authorization response. The transaction must have started as an authorization or a single-message financial message.

curl https://api.column.com/simulate/issuing/card-transactions/ctxn_3J6f3h8oQ6J0SWTTNQOdEiy43pg/reversal \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 1000
  }'
Field
Required
Description
cardholder_amount
No
Amount to release from the authorization hold. Must be less than the outstanding authorized amount. Omit to reverse in full.

Omit cardholder_amount to reverse in full: an authorization releases whatever hold remains, including incremental authorizations, and a financial transaction reverses its settled amount. Partial reversals only apply to authorizations. Reversals act on the outstanding hold, so a transaction that has already been captured has nothing left to reverse — refund it instead.

Refund

Refund returns funds to the card account against a previously captured transaction.

curl https://api.column.com/simulate/issuing/card-transactions/ctxn_3J6f3h8oQ6J0SWTTNQOdEiy43pg/refund \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "cardholder_amount": 2500
  }'

Each refund creates a new card transaction linked to the purchase by original_card_transaction_id. The total refunded must not exceed the settled amount on the original transaction.

3DS Authentication

3DS authentication simulates a 3DS authentication attempt for an online purchase. Link a successful attempt to an authorization with three_ds_attempt_id to test a liability shift.

curl https://api.column.com/simulate/issuing/cards/card_3J6Gg2WgNLoZAz67PrHhJy9aHlT/threeds/challenge \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "amount": 2500,
      "currency": "USD",
      "realtime_decision": "challenge"
  }'
Field
Required
Description
amount
Yes
Transaction amount in the smallest currency unit.
currency
Yes
ISO 4217 currency code.
realtime_decision
No
Inject an authentication decision: frictionless_approve, challenge, or decline_authentication.
decline_reason
No
suspected_fraud or transaction_not_permitted when realtime_decision is decline_authentication. Defaults to suspected_fraud.
message_category
No
payment or non_payment (for example, adding a card to a merchant's file).
merchant_name / merchant_url / merchant_category_code / merchant_country_code
No
Merchant details placed on the attempt and on the decision request.

When you omit realtime_decision, the simulator sends a decision request to your endpoint subscribed to decision.issuing.card_authentication. If none is subscribed, it creates an OTP challenge.

  • frictionless_approve authenticates the attempt and emits issuing.3ds.authentication_completed.
  • challenge starts an OTP challenge and emits issuing.3ds.otp_generated.
  • decline_authentication rejects the attempt and emits issuing.3ds.authentication_completed.

Complete the challenge with POST /simulate/issuing/threeds/:three_ds_attempt_id/complete, setting outcome to successful or failed. A successful outcome leaves the attempt authenticated, ready to pass as three_ds_attempt_id on a simulated authorization.

curl https://api.column.com/simulate/issuing/threeds/<three_ds_attempt_id>/complete \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "outcome": "successful"
  }'

Simulate Card Tokens

You can simulate the digital wallet provisioning flow and network token lifecycle events for cards on a card program. Tokens are only created through the network provisioning flow, so in Sandbox you start by simulating a wallet's eligibility check. See Card Tokens for token statuses, events, and provisioning decisioning.

Step
Endpoint
Eligibility check
POST /simulate/issuing/cards/tokens/check-eligibility
Approve provisioning
POST /simulate/issuing/cards/tokens/:card_token_id/approve-provisioning
Step-up: retrieve methods
POST /simulate/issuing/cards/tokens/:card_token_id/step-up/methods
Step-up: send passcode
POST /simulate/issuing/cards/tokens/:card_token_id/step-up/send-passcode
Step-up: complete
POST /simulate/issuing/cards/tokens/:card_token_id/step-up/complete
Device binding
POST /simulate/issuing/cards/tokens/:card_token_id/device-binding
Network suspend
POST /simulate/issuing/cards/tokens/:card_token_id/suspend
Network resume
POST /simulate/issuing/cards/tokens/:card_token_id/resume
Network deactivate
POST /simulate/issuing/cards/tokens/:card_token_id/deactivate

Eligibility Check

Eligibility check simulates a digital wallet checking whether a card can be tokenized. An eligible card returns a new card token in pending_activation status; an ineligible card returns an error with the reason.

curl https://api.column.com/simulate/issuing/cards/tokens/check-eligibility \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "card_id": "card_3J6Gg2WgNLoZAz67PrHhJy9aHlT"
  }'

Approve Provisioning

Approve provisioning simulates the wallet's provisioning request for the pending_activation token returned by the eligibility check.

curl https://api.column.com/simulate/issuing/cards/tokens/<card_token_id>/approve-provisioning \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "wallet_type": "apple_pay",
      "device_type": "mobile_phone",
      "device_name": "Cardholder iPhone",
      "action_code": "approve"
  }'
Field
Required
Description
action_code
No
Force the decision: approve, decline, or step_up.
wallet_type
No
apple_pay, google_pay, or samsung_pay.
device_type
No
mobile_phone, tablet, or watch.
device_name
No
Human-readable name of the device requesting provisioning.
token_requestor_name
No
Name of the entity that initiated the provisioning request.
token_type
No
wallet or card_on_file. Defaults to wallet.

Testing your provisioning decisioning endpoint. When you omit action_code, the request goes through Column's real provisioning decisioning, including your endpoint subscribed to decision.issuing.card_token. An approve decision activates the token and emits issuing.card_token.activated.

Step-up Verification

A step_up decision leaves the token ready for cardholder verification. If the cardholder has contact info on file, the simulator also runs the retrieve-methods and send-passcode steps for you, so you only need to complete the step-up. Otherwise, or to choose the delivery method yourself, run the steps individually:

  1. Retrieve methods returns the SMS and email methods available from the cardholder's contact info.
  2. Send passcode emits issuing.card_token.send_passcode with a simulated one-time passcode and moves the token to pending_authentication. Pass one of the returned method identifiers as otp_method_identifier, or omit it to select one automatically.
  3. Complete finishes verification.
curl https://api.column.com/simulate/issuing/cards/tokens/<card_token_id>/step-up/complete \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "outcome": "success"
  }'

An outcome of success activates the token and emits issuing.card_token.activated; failure moves it to activation_denied and emits issuing.card_token.activation_denied. The token must be in pending_authentication.

Device Binding

Device binding simulates binding an existing token to a new device and returns the updated card token.

curl https://api.column.com/simulate/issuing/cards/tokens/<card_token_id>/device-binding \
  -XPOST \
  -H 'Content-Type: application/json' \
  -u :<YOUR API KEY> \
  --data '{
      "device_type": "watch",
      "device_name": "Cardholder Watch",
      "decision_override": "approve"
  }'
Field
Required
Description
decision_override
No
Force the outcome: approve, decline, or step_up. Omit to call your endpoint subscribed to decision.issuing.card_token.device_binding.
device_id
No
Identifier of the device being bound.
device_type
No
mobile_phone, tablet, or watch.
device_name
No
Human-readable name of the device being bound.
wallet_account_id
No
Wallet account identifier associated with the binding.

Network Lifecycle Events

Simulate the card network changing a token's status, exactly as a production network notification would, with suspend, resume, and deactivate. None of these endpoints require a request body.

Endpoint
Result
suspend
Token moves to paused and emits issuing.card_token.paused.
resume
A paused token moves back to active and emits issuing.card_token.activated.
deactivate
Token moves to deactivated and emits issuing.card_token.deactivated. Deactivating a token that is already deactivated returns it unchanged without emitting an event.

To pause, activate, or deactivate a token from your own platform instead, use the card token endpoints.