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"
}'typedebit, credit, or charge.schemevisa. Defaults to visa.credit_policyunsecured, cardholder_secured, or platform_secured. Defaults to unsecured. Ignored for debit programs.descriptionThe 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
}
]
}
}'scopecard_program, card_account, or card.effective_onRFC 3339 timestamp. Offsets are converted to UTC.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"
}'card_program_idbank_account_idloan_idcardholder_secured program, the loan must have type=secured; its collateral bank account is derived from the loan and cannot be overridden.card_authorization_policy_idCards
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"
}'typevirtual or physical.card_account_idcard_authorization_policy_idcard_template_idexpiration_month / expiration_yearstatusencrypted_pinThe 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.
POST /simulate/issuing/cards/:card_id/card-transactions/authorizationPOST /simulate/issuing/card-transactions/:card_transaction_id/incremental-authorizationPOST /simulate/issuing/card-transactions/:card_transaction_id/capturePOST /simulate/issuing/card-transactions/:card_transaction_id/reversalPOST /simulate/issuing/card-transactions/:card_transaction_id/refundPOST /simulate/issuing/cards/:card_id/card-transactions/financialPOST /simulate/issuing/cards/:card_id/threeds/challengeAll 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"
}'cardholder_amountmerchant_currencycardholder_fx_rateUSD onlyUSD, the cardholder billing currency.merchant_category_code5812 (restaurants) or 6011 (ATM). Defaults to 5999.merchant_nameACQUIRER NAME.merchant_cityCITY NAME.merchant_country_codeUS.realtime_decisionapprove, decline, or partially_approve.realtime_decision_reasonrealtime_decision is decline. Defaults to generic_decline.realtime_partially_approved_amountrealtime_decision is partially_approve. Must be greater than 0 and less than cardholder_amount.network_standinapproved or declined.network_decision_reasonnetwork_standin is declined.three_ds_attempt_idauthenticated 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
}'cardholder_amountfx_rateThe 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
}'cardholder_amountis_partial_capturetrue, 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"
}'cardholder_amountmerchant_currencycardholder_fx_rateUSD onlyUSD, the cardholder billing currency.merchant_category_code5999.merchant_nameACQUIRER NAME.merchant_cityCITY NAME.merchant_country_codeUS.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
}'cardholder_amountOmit 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"
}'amountcurrencyrealtime_decisionfrictionless_approve, challenge, or decline_authentication.decline_reasonsuspected_fraud or transaction_not_permitted when realtime_decision is decline_authentication. Defaults to suspected_fraud.message_categorypayment or non_payment (for example, adding a card to a merchant's file).merchant_name / merchant_url / merchant_category_code / merchant_country_codeWhen 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_approveauthenticates the attempt and emitsissuing.3ds.authentication_completed.challengestarts an OTP challenge and emitsissuing.3ds.otp_generated.decline_authenticationrejects the attempt and emitsissuing.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.
POST /simulate/issuing/cards/tokens/check-eligibilityPOST /simulate/issuing/cards/tokens/:card_token_id/approve-provisioningPOST /simulate/issuing/cards/tokens/:card_token_id/step-up/methodsPOST /simulate/issuing/cards/tokens/:card_token_id/step-up/send-passcodePOST /simulate/issuing/cards/tokens/:card_token_id/step-up/completePOST /simulate/issuing/cards/tokens/:card_token_id/device-bindingPOST /simulate/issuing/cards/tokens/:card_token_id/suspendPOST /simulate/issuing/cards/tokens/:card_token_id/resumePOST /simulate/issuing/cards/tokens/:card_token_id/deactivateEligibility 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"
}'action_codeapprove, decline, or step_up.wallet_typeapple_pay, google_pay, or samsung_pay.device_typemobile_phone, tablet, or watch.device_nametoken_requestor_nametoken_typewallet 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:
- Retrieve methods returns the SMS and email methods available from the cardholder's contact info.
- Send passcode emits
issuing.card_token.send_passcodewith a simulated one-time passcode and moves the token topending_authentication. Pass one of the returned method identifiers asotp_method_identifier, or omit it to select one automatically. - 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"
}'decision_overrideapprove, decline, or step_up. Omit to call your endpoint subscribed to decision.issuing.card_token.device_binding.device_iddevice_typemobile_phone, tablet, or watch.device_namewallet_account_idNetwork 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.
suspendpaused and emits issuing.card_token.paused.resumeactive and emits issuing.card_token.activated.deactivatedeactivated 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.