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.
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 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",
      "encrypted_pin":"SKIP"
  }'
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. This is required for debit cards. Use "SKIP" to set this at a later date.

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, use the two-step reveal flow. Call POST /issuing/cards/:card_id/reveal-tokens to create a single-use, short-lived reveal token. Then call GET /issuing/cards/:card_id/reveal, authenticating with Authorization: Bearer <card_reveal_token_id> instead of your API key, to reveal card details.

PIN Encryption

Debit card programs require an encrypted_pin prior to use, replacement, or renewal. The PIN is the customer's chosen 4-digit code, encrypted with RSA-2048, OAEP padding, SHA-256 hash, then base64-encoded.

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>"
    }'

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. Must be greater than 0 and less than cardholder_amount.
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.

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.

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"
  }'
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.

Unlike an authorization, a simulated financial transaction never calls your real-time decisioning endpoint, so there is no realtime_decision or network_standin field to set. Card status, authorization policy, and balance checks still apply and can decline a financial transaction.

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.