Creating a cardholder
Create the record everything else references.
Before you begin
- A sandbox or production access token — see authentication.
- Nothing else. A cardholder is the first object you create, and it needs no prior verification — see below.
Creating a cardholder does not require verification. The record exists
immediately with kyc_status: not_started, and you can open accounts and even
issue cards against it — provided it has an address and an
identity_document, which issuance needs but creation does not.
What verification gates is card activation. Creating early means the record, the account, and the card are all in place the moment you report a passing outcome, rather than serializing four steps.
How it works
A cardholder is the spine of the model: accounts reference it, cards reference it, and nothing can be issued without one. See core concepts.
type is individual. Business or company card will be supported in the
future.
type, first_name and last_name are required — a card has to be issued to
somebody. Which of the rest you must supply depends on your program's
configuration and on what your own verification process needs.
Two fields are required not for creation but for the next step:
identity_document and address. A cardholder can be created without either,
but a card cannot be issued to one that lacks them.
identity_document — missing it fails issuance with
identity_document_required. Only the number and its type are held — no image,
no verification result — and the number is never returned; the response reports
has_identity_document instead. This one cannot be added later: it is not
patchable, so a cardholder created without it must be recreated.
address — missing it fails issuance with address_required. The card is
registered to this address, so it is the cardholder's own. Unlike the document,
it can be added later with PATCH /v1/cardholders/{cardholder_id}.
Set both at creation unless you are recording a cardholder you will not yet issue a card to.
Steps
1. Create the cardholder
curl -X POST "$ALETA_BASE_URL/v1/cardholders" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"type": "individual",
"first_name": "Mei",
"last_name": "Tan",
"email": "mei.tan@example.com",
"phone": "+6591234567",
"date_of_birth": "1990-04-12",
"address": {
"line1": "350 Orchard Road",
"line2": "#12-01",
"city": "Singapore",
"postal_code": "238868",
"country": "SGP"
},
"identity_document": {
"type": "national_id",
"number": "S1234567D"
},
"reference": "user-4471"
}'
{
"id": "chd_01HX8ZQ2K4M7P9RSTVWXYZAB",
"object": "cardholder",
"type": "individual",
"kyc_status": "not_started",
"first_name": "Mei",
"last_name": "Tan",
"email": "mei.tan@example.com",
"reference": "user-4471",
"has_identity_document": true,
"created_at": "2026-07-29T14:32:05Z"
}
kyc_status starts at not_started. It moves only when you
report an outcome.
2. Set reference
reference is your own identifier. It is filterable and appears in every
webhook payload for this cardholder:
curl "$ALETA_BASE_URL/v1/cardholders?reference=user-4471" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN"
Without it, every incoming cardholder.kyc_approved needs a lookup table to
map back to your user. With it, the event carries your own key. Retrofitting
means backfilling every cardholder you have already created.
3. Field rules worth knowing
| Field | Rule |
|---|---|
phone | E.164, with the +. +6591234567, not 91234567 |
date_of_birth | YYYY-MM-DD. A date, not a timestamp — no _at suffix |
address.country | ISO 3166-1 alpha-3. SGP, not SG. Checked against the real list, so ZZZ is refused too |
email | Used for program communications where configured |
Unknown fields are rejected with unknown_parameter rather than ignored, so
a typo surfaces immediately instead of silently dropping data.
4. Updating later
curl -X PATCH "$ALETA_BASE_URL/v1/cardholders/chd_01HX8ZQ2K4M7P9RSTVWXYZAB" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"email": "mei.tan@newdomain.example"}'
PATCH accepts contact details and reference only. It cannot change
kyc_status — that moves solely through
reporting an outcome, so the verification record
stays a deliberate, attributable act rather than a field update.
Verifying
curl "$ALETA_BASE_URL/v1/cardholders/chd_01HX8ZQ2K4M7P9RSTVWXYZAB" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN"
Expect kyc_status: "not_started" and your reference echoed back.
cardholder.created fires on your webhook endpoint.
Common errors
| Status | code | Fix |
|---|---|---|
400 | missing_parameter | type, first_name and last_name are required |
400 | invalid_parameter | type must be individual; or phone isn't in E.164 format, email isn't an address, or a country isn't an ISO 3166-1 alpha-3 code. param names the field |
400 | unknown_parameter | A field name typo, or business_name, which is no longer accepted. Check param in the error |
409 | cardholder_already_exists | Another cardholder in your organization already has this email or phone |
409 | idempotency_key_reuse | Same key, different body |
Errors when updating a cardholder
PATCH /v1/cardholders/{cardholder_id} returns these as well. The fields you
sent are validated first, then written, then sent to the processor if it holds
them.
| Status | code | Fix |
|---|---|---|
400 | invalid_parameter | email, phone or address is malformed, or reference is over 128 characters. param names the field |
400 | unknown_parameter | The field can't be changed. Name, type and the identity document are fixed; re-verify instead |
404 | cardholder_not_found | The cardholder doesn't exist, or belongs to another organization |
409 | cardholder_already_exists | Another cardholder in your organization already has this email or phone |
422 | address_change_requires_kyc | This program's processor can't take an address change. Report a KYC attestation that carries the new address |
422 | operation_unsupported | The processor can't take this change |
502 | processor_unavailable | Your change is saved here but the processor didn't take it. Send the same change again |
502 | processor_permission_denied | The program isn't approved to change this at the processor. Send us the request_id |