All validation failures on the Cards API are returned synchronously. Business logic errors are returned as 422 responses with an invalid_parameters array that identifies which fields failed and why, so you can correct the request before retrying.
{
"code": "validation_error",
"title": "Validation Error",
"detail": "One or more request parameters failed validation",
"invalid_parameters": [
{
"parameter": "<param>",
"reason": "<reason>"
}
]
}
Every mutating request (POST, PATCH, and the action endpoints) requires an Idempotency-Key header. Omitting it returns 400 operation_not_allowed (reason: parameter "Idempotency-Key" in header has an error: value is required but missing). A request body that is not valid JSON returns 400 bad_request with The request body contains malformed JSON.
POST /v0/cards
Creates a card. The card is always created in active status.
Required Fields
| Parameter | Validation Rule | Error Reason |
|---|
account_id | Required object | property account_id is missing |
entity_id | Required object | property entity_id is missing |
details | Required object | property details is missing |
details.card_product_name | Required | property card_product_name is missing |
details.is_virtual | Required | property is_virtual is missing |
details.card_type | Required | property card_type is missing |
details.expiry_date | Required | property expiry_date is missing |
details.last_four | Required | property last_four is missing |
Idempotency-Key header | Required (all mutations) | 400 operation_not_allowed — parameter Idempotency-Key in header has an error: value is required but missing |
Field Validations
| Parameter | Validation Rule | Error Reason |
|---|
entity_id | Must match the pattern ^entity_[^\s]{1,33}$ | String doesn’t match the regular expression ^entity_[^\s]{1,33}$ |
account_id | Must match the pattern ^account_\w+$ | String doesn’t match the regular expression ^account_\w+$ |
details.last_four | Must match the pattern ^[0-9]{4}$ | String doesn’t match the regular expression ^[0-9]{4}$ |
details.expiry_date | Must be a valid ISO 8601 date (YYYY-MM-DD), expressed as the last day of the expiration month | String doesn’t match the format “date” |
details.expiry_date | Must not be earlier than the time of object creation (e.g., if created any time on 2026-06-01 UTC, an expiry_date before 2026-06-01 is rejected) | invalid_date |
details.card_type | Must be one of hsa, fsa, consumer, commercial | value is not one of the allowed values ["hsa","fsa","consumer","commercial"] |
details.is_virtual | Must be a boolean value | Value must be a boolean |
details.replacement_for | If provided, must reference a card that exists, is in closed status, is not already referenced by another card, and matches the pattern ^card_\w+$ | Referenced card must be in closed status to be replaced
Referenced card not found
Replacement for card has already been referenced
String doesn’t match the regular expression ^card_\w+$ |
Reference Validations
| Parameter | Validation Rule | Error Reason |
|---|
account_id | Must reference a valid account whose status is active or inactive | The account_id referenced by this card does not exist |
entity_id | Must be linked to the provided account_id as an account_holder or authorized_user | The entity_id is not associated with the referenced account |
Creating a card against a closed account is rejected with 400 operation_not_allowed (title “This operation cannot be done on this object.”) rather than a 422, because it is an operation that cannot be performed on the object.
GET /v0/cards/{id}
Retrieves a card by its server-generated ID.
| Validation | Error |
|---|
id must match the pattern ^card_\w+$ | 404 not_found: the card ID passed in cannot be found |
| Card must exist and belong to the caller’s program | 404 not_found: the requested card was not found |
GET /v0/cards?client_card_id={client_card_id}
Retrieves a card by the client-provided client_card_id (for cards created via file ingestion).
| Validation | Error |
|---|
client_card_id query parameter is required | 422 validation_error: parameter client_card_id in query has an error: value is required but missing |
client_card_id must not be empty | 400 bad_request |
| Card must exist for the caller’s program | 404 not_found: the requested card was not found |
PATCH /v0/cards/{id}
Partial update. Only details.card_product_name and metadata are mutable.
| Parameter / Rule | Behavior | Error Reason |
|---|
details.card_product_name, metadata | Mutable — updated when present | — |
Any other field (status, details.expiry_date, details.is_virtual, details.card_type, details.last_four, account_id, entity_id) | Silently ignored — the value is not applied, and updated_at is only bumped if a mutable field actually changed | — (returns 200) |
details.card_product_name | If provided, must be a non-empty string | 422 validation_error: details.card_product_name → Card is missing required client_card_product_name field |
Card status | A card in closed status cannot be updated | 422 validation_error: status → Card cannot be updated when status is closed |
metadata | Must not exceed the DynamoDB item size limit | 422 validation_error (oversized metadata) |
Unlike a create, non-mutable fields sent on a PATCH are ignored rather than rejected (consistent with the Accounts API). A PATCH that changes nothing (empty body, or only non-mutable fields) returns 200 without bumping updated_at.
The status of a card is system-managed and changes only through the three action endpoints below (each takes an Idempotency-Key header):
| Endpoint | Allowed source states | Required body | Enforced status_reason enum |
|---|
POST /v0/cards/{id}/activate | inactive, closed | — (no body) | — |
POST /v0/cards/{id}/deactivate | active, closed | status_reason | dormant, frozen, other |
POST /v0/cards/{id}/close | active, inactive | status_reason | entity_closed, client_closed, paid_off, charged_off, canceled |
POST /v0/cards/{id}/activate
| Validation Rule | Error Reason |
|---|
Card must not already be active | status → card is already active |
A card whose expiry_date is in the past cannot be activated | status → card is expired and cannot be activated |
A card that has been replaced (i.e., its ID has been referenced in the replacement_for field on a separate card) cannot be reactivated | status → card has been replaced and cannot be reactivated |
Reactivating a closed card clears its closed_at and status_reason.
POST /v0/cards/{id}/deactivate
| Validation Rule | Error Reason |
|---|
status_reason is required | property status_reason is missing |
status_reason must be a valid enum value | value is not one of the allowed values ["dormant","frozen","other"] |
Card must not already be inactive | status → card is already inactive |
A card that has been replaced (i.e., its ID has been referenced in the replacement_for field on a separate card) cannot be deactivated | status → card has been replaced and cannot be deactivated |
Deactivating a closed card clears its closed_at and sets the new status_reason.
POST /v0/cards/{id}/close
| Validation Rule | Error Reason |
|---|
status_reason is required | property status_reason is missing |
status_reason must be a valid enum value | value is not one of the allowed values ["entity_closed","client_closed","paid_off","charged_off","canceled"] |
Card must not already be closed | status → card is already closed |
Authentication
Authentication and scope are enforced at the gateway.
| Scenario | Error |
|---|
Using a card/read token to call POST, PATCH, or an action endpoint | 403 insufficient_scope: you cannot call /v0/cards with the scope card/read |
| Using an unrelated scope token | 403 insufficient_scope |
| Using a missing, malformed, or expired token | 403 |