Issuing a virtual card
Before you begin
Before issuing a virtual card, make sure you have:
- A cardholder with an
identity_documenton file. Issuance is rejected withidentity_document_requiredif there isn't one. Set it when creating the cardholder, since it can't be added afterwards. - A cardholder with an
addresson file. The card is registered to this address, and issuance is rejected withaddress_requiredif there isn't one. Set it when creating the cardholder. Some programs don't allow an address change once the cardholder exists, and refusePATCH /v1/cardholders/{cardholder_id}withaddress_change_requires_kyc; there, report a KYC attestation carrying the address instead. - A cardholder with
kyc_status: approved. Issuance is rejected withkyc_not_approveduntil 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:
- Create it.
POST /v1/cardsreturnsstatus: "pending"while the processor issues the card. On a program that issues at once it returnsinactiveinstead. - Wait for
card.issued. The processor has the card, and its status is nowinactive. Issuing can take a few minutes, and sometimes close to an hour. We check for you, so don't poll. - Activate it.
POST /v1/cards/{card_id}/activateis a server-to-server call with no body. The card becomesactiveandcard.activatedfires.
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"
}'
{
"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
| Status | code | Fix |
|---|---|---|
400 | missing_parameter | cardholder_id, account_id or form_factor wasn't sent |
400 | invalid_parameter | An 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 |
400 | unknown_parameter | A field name typo. param names it |
404 | cardholder_not_found | The cardholder doesn't exist, or belongs to another organization |
404 | account_not_found | The account doesn't exist, or belongs to another organization |
409 | idempotency_key_reuse | The same Idempotency-Key was used with a different body |
422 | embossed_name_required | embossed_name wasn't sent. It is required for every card |
422 | delivery_address_required | delivery_address wasn't sent. It is required for every card |
422 | identity_document_required | The cardholder has no identity_document. Set it when creating the cardholder |
422 | address_required | The 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 |
422 | kyc_not_approved | The cardholder hasn't been verified. Wait for cardholder.kyc_approved instead of polling |
422 | account_blocked | Unblock the funding account first |
422 | account_cardholder_mismatch | The account belongs to a different cardholder. Use one of this cardholder's accounts |
422 | unsupported_form_factor | The program doesn't issue this form factor. The error lists the ones it does issue |
422 | card_limit_reached | The cardholder already has a card with the processor behind this program, which holds one card per cardholder |
422 | card_type_not_configured, program_not_configured | The program isn't set up for this card. Nothing was sent to the processor; raise it with your account contact |
422 | program_not_provisioned | The program isn't usable on this deployment. Raise it with your account contact |
422 | card_invalid_state | On activation: the card is still pending, frozen, or expired. Wait for card.issued, unfreeze it, or issue a new card |
422 | card_already_activated | On activation: the card is already active |
422 | card_closed | On activation: the card is closed. Issue a new card |
502 | processor_unavailable | The 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 |
502 | processor_permission_denied | The program isn't approved for this operation at the processor. Send us the request_id |
Related
- Issuing a physical card — to have a card printed
- Card lifecycle
- Operations by card status
- Freeze, replace, close
- Displaying PAN and CVV securely