Skip to main content

Issuing a virtual card

Before you begin​

Before issuing a virtual card, make sure you have:

  • A cardholder with an identity_document on file. Issuance is rejected with identity_document_required if there isn't one. Set it when creating the cardholder, since it can't be added afterwards.
  • A cardholder with an address on file. The card is registered to this address, and issuance is rejected with address_required if there isn't one. Set it when creating the cardholder. Some programs don't allow an address change once the cardholder exists, and refuse PATCH /v1/cardholders/{cardholder_id} with address_change_requires_kyc; there, report a KYC attestation carrying the address instead.
  • A cardholder with kyc_status: approved. Issuance is rejected with kyc_not_approved until the cardholder has been approved.
  • An account in the currency the card will spend from. See creating accounts.
  • A funded account if you want to test spending. A card linked to an empty account will have every authorization declined with insufficient_funds.

How it works​

A virtual card doesn't need to be manufactured or shipped, but it isn't usable the moment it's created. It goes through the same statuses as every other card, and you decide when it starts working:

  1. Create it. POST /v1/cards returns status: "pending" while the processor issues the card. On a program that issues at once it returns inactive instead.
  2. Wait for card.issued. The processor has the card, and its status is now inactive. Issuing can take a few minutes, and sometimes close to an hour. We check for you, so don't poll.
  3. Activate it. POST /v1/cards/{card_id}/activate is a server-to-server call with no body. The card becomes active and card.activated fires.

Until it's active, every authorization on the card is declined.

Activation is a separate call so that virtual and physical cards behave the same way, and so the card only starts working when you are ready for it to. The call needs the cardholder to still be verified.

Steps​

1. Create the card​

curl -X POST "$ALETA_BASE_URL/v1/cards" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"cardholder_id": "chd_01HX8ZQ2K4M7P9RSTVWXYZAB",
"account_id": "acct_01HX8ZQ2K4M7P9RSTVWXYZAB",
"form_factor": "virtual",
"embossed_name": "A TAN",
"delivery_address": {
"line1": "350 Orchard Road",
"city": "Singapore",
"postal_code": "238868",
"country": "SGP"
},
"reference": "user-4471-card-1"
}'
Response
{
"id": "card_01HX8ZQ2K4M7P9RSTVWXYZAB",
"object": "card",
"cardholder_id": "chd_01HX8ZQ2K4M7P9RSTVWXYZAB",
"account_id": "acct_01HX8ZQ2K4M7P9RSTVWXYZAB",
"program_id": "prog_01HX8ZQ2K4M7P9RSTVWXYZAB",
"form_factor": "virtual",
"card_type": "debit",
"status": "pending",
"currency": "SGD",
"is_default": true,
"reference": "user-4471-card-1",
"created_at": "2026-07-29T14:32:08Z"
}

The request needs five fields: cardholder_id, account_id, form_factor, embossed_name and delivery_address. The last two are required for a virtual card too, because they are fixed when the card is issued and can't be changed if you later order a physical card for it. The card gets its currency from the account, so you don't need to send currency. Everything else in the request is optional.

The address and identity_document belong to the cardholder, not the card creation request. The card is registered to the cardholder's address, so issuance is rejected with address_required if no address is on file.

last_four and expires_at appear once the card has been issued, and only when the processor reports them, so don't depend on them being there. The full card number and CVV are never in this response and aren't sent to your servers. Retrieving them is a separate, separately scoped and audited operation. See displaying PAN and CVV securely.

2. Wait for card.issued​

The event's payload carries status: "inactive". That is your signal that the card can be activated. See card issuing events.

3. Activate the card​

curl -X POST "$ALETA_BASE_URL/v1/cards/card_01HX8ZQ2K4M7P9RSTVWXYZAB/activate" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"

The response is the card, now active. Calling it again on an active card returns card_already_activated, and calling it before card.issued returns card_invalid_state.

If the call fails, the card stays inactive and you can call it again.

Verifying​

Retrieve the card and check its status at each step:

curl "$ALETA_BASE_URL/v1/cards/card_01HX8ZQ2K4M7P9RSTVWXYZAB" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN"

Expect pending, then inactive after card.issued, then active after the activation call. Then prove it spends: in sandbox, simulate an authorization and confirm the funding account's held_balance moves. A card that is not active is declined with card_not_active.

curl -X POST "$ALETA_BASE_URL/v1/simulate/authorization" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"card_id": "card_01HX8ZQ2K4M7P9RSTVWXYZAB",
"amount": { "amount": 4250, "currency": "SGD" },
"merchant": { "name": "Kopi Roastery", "mcc": "5814", "country": "SGP" }
}'

Common errors​

StatuscodeFix
400missing_parametercardholder_id, account_id or form_factor wasn't sent
400invalid_parameterAn id is the wrong kind, form_factor isn't virtual or physical, embossed_name is over 26 characters, or the address is incomplete. Or the processor rejected the application: quote the request_id and we find the reason
400unknown_parameterA field name typo. param names it
404cardholder_not_foundThe cardholder doesn't exist, or belongs to another organization
404account_not_foundThe account doesn't exist, or belongs to another organization
409idempotency_key_reuseThe same Idempotency-Key was used with a different body
422embossed_name_requiredembossed_name wasn't sent. It is required for every card
422delivery_address_requireddelivery_address wasn't sent. It is required for every card
422identity_document_requiredThe cardholder has no identity_document. Set it when creating the cardholder
422address_requiredThe cardholder has no address. Set it with PATCH /v1/cardholders/{cardholder_id}, or report it in a KYC attestation on programs that don't allow the change
422kyc_not_approvedThe cardholder hasn't been verified. Wait for cardholder.kyc_approved instead of polling
422account_blockedUnblock the funding account first
422account_cardholder_mismatchThe account belongs to a different cardholder. Use one of this cardholder's accounts
422unsupported_form_factorThe program doesn't issue this form factor. The error lists the ones it does issue
422card_limit_reachedThe cardholder already has a card with the processor behind this program, which holds one card per cardholder
422card_type_not_configured, program_not_configuredThe program isn't set up for this card. Nothing was sent to the processor; raise it with your account contact
422program_not_provisionedThe program isn't usable on this deployment. Raise it with your account contact
422card_invalid_stateOn activation: the card is still pending, frozen, or expired. Wait for card.issued, unfreeze it, or issue a new card
422card_already_activatedOn activation: the card is already active
422card_closedOn activation: the card is closed. Issue a new card
502processor_unavailableThe processor couldn't take the application. Retry with the same Idempotency-Key. Check the card list first if you can't be sure it didn't go through
502processor_permission_deniedThe program isn't approved for this operation at the processor. Send us the request_id