Overview #
A cardholder is the end user a card belongs to. On Provider W BINs every card
is issued against a cardholder of yours, and the cardholder must be in
pass_audit before POST /v1/cards will accept it. This is what ties each card
to an identified person on your side.
How much data a cardholder needs depends on the BIN. Each BIN carries a
providerCardHolderModel field, returned by GET /v1/accounts (in the BIN's
settings) and by GET /v1/bins:
providerCardHolderModel |
What it means | Effort |
|---|---|---|
B2B |
Short form. firstName, lastName, email only. The cardholder is created already approved. |
One call, no waiting |
B2C |
Full identity check. Full personal data plus completed KYC, reviewed before approval. | Async, minutes to hours |
| absent | The BIN does not use cardholders — issue the card directly. | — |
Read the model from the BIN rather than hard-coding it: the same client can hold
both kinds of BINs, and the flows are not interchangeable.
If you integrated before August 2026 #
The B2B path used to demand the same ten-field identity form as B2C. It no longer
does:
- On a B2B BIN only
firstName,lastNameandemailare read. Address, phone,
date of birth and document fields are neither required nor used. POST /v1/cardholders/filesand theidFrontImg/idBackImg/idHoldImg
meta fields are not part of any current flow — B2B collects no documents, and
B2C takes them from KYC automatically. Existing calls keep working; drop them.- A B2B cardholder is returned already approved, so remove the polling loop.
- Cardholders you were left with in
rejectorsubmission_failedon a B2B BIN
become usable again by simply callingPOST /v1/users/{id}/cardholderfor the
same user and BIN — the existing record is reused, not duplicated.
B2B Flow (short form) #
No identity documents, no address, no date of birth, no review step. The
cardholder exists to attribute cards to your end user.
Step 1 — Create the user #
curl -s -X POST "$PAYCA_BASE_URL/v1/users" \
-H 'Content-Type: application/json' \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET" \
-d '{
"externalId": "usr-001",
"meta": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com"
}
}'
Anything else you keep in meta is stored as-is and ignored by the cardholder
flow.
Step 2 — Create the cardholder #
curl -s -X POST "$PAYCA_BASE_URL/v1/users/{userId}/cardholder" \
-H 'Content-Type: application/json' \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET" \
-d '{
"accountId": "<account-uuid>",
"bin": "537100"
}'
The response already carries status: "pass_audit" — there is nothing to poll
for. Calling the endpoint again for the same user and BIN returns the same
cardholder, so it is safe to make it part of your user-provisioning path.
Two rules to be aware of:
emailis unique per BIN. Reusing an address that another cardholder on
the same BIN already holds is rejected (ErrAlreadyExists) — an email
identifies exactly one cardholder on a BIN.- Name rules still apply (Latin letters, space, hyphen, apostrophe, period;
max 40 chars;TEST/MOCK/SANDBOXrejected, also in sandbox), so the same
user can later be moved onto a B2C BIN without discovering the stored names
were never acceptable.
Step 3 — Issue the card #
curl -s -X POST "$PAYCA_BASE_URL/v1/cards" \
-H 'Content-Type: application/json' \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET" \
-d '{
"accountId": "<account-uuid>",
"bin": "537100",
"balance": "100.00",
"idempotencyKey": "issue-001",
"cardholderId": "<cardholder-uuid>"
}'
B2C Flow (full identity check) #
B2C cardholders carry a full personal profile and require completed KYC. Their
data is reviewed before the cardholder is approved.
Step 1 — Create the user #
curl -s -X POST "$PAYCA_BASE_URL/v1/users" \
-H 'Content-Type: application/json' \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET" \
-d '{
"externalId": "usr-002",
"meta": {
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@example.com",
"phone": "5559876543",
"phoneCode": "+1",
"birthday": "1988-05-20",
"gender": "F",
"idType": "PASSPORT",
"idNo": "AB1234567",
"idIssueDate": "2019-03-11",
"nationality": "ES",
"country": "ES",
"town": "MAD",
"address": "456 Oak Ave",
"postCode": "28001",
"occupation": "IT"
}
}'
Reference data: /v1/cardholders/regions for country and nationality,
/v1/cardholders/cities?region=<country> for town,
/v1/cardholders/occupations for occupation. The codes are validated against
the live lists — a free-text city name is rejected.
POST /v1/usersandPATCH /v1/users/{id}storemetaas given and do not
validate it. The field rules below are enforced when the cardholder is created
or updated, so validate at the point your user fills the form rather than at
card issuance.
Step 2 — KYC verification #
Documents are sent as base64-encoded JSON (not multipart). Max 10 MB decoded per
file.
# Helper — base64-encode a file and POST it
upload_kyc() {
local USER_ID="$1" DOC_TYPE="$2" FILE="$3"
local CONTENT=$(base64 < "$FILE" | tr -d '\n')
curl -s -X POST "$PAYCA_BASE_URL/v1/users/$USER_ID/kyc/documents" \
-H 'Content-Type: application/json' \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET" \
-d "{
\"documentType\": \"$DOC_TYPE\",
\"fileName\": \"${FILE##*/}\",
\"fileContent\": \"$CONTENT\",
\"mimeType\": \"image/jpeg\"
}"
}
upload_kyc "$USER_ID" passport passport.jpg
upload_kyc "$USER_ID" selfie_with_document selfie.jpg
# Submit KYC for review
curl -s -X POST "$PAYCA_BASE_URL/v1/users/$USER_ID/kyc/submit" \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET"
Valid documentType values: passport, driving_licence, id_card,
residence_permit, proof_of_address, selfie_with_document. The newest
non-rejected document per slot is the one used.
Wait for status: "completed" on GET /v1/users/{userId}/kyc (or subscribe to
the kyc webhook) before proceeding — the cardholder endpoint rejects the
request otherwise.
Step 3 — Create the cardholder #
Same call as B2B step 2. Personal data and KYC documents are pulled from the user
automatically — you never re-upload them here.
The cardholder is created with status: "pending_review".
Step 4 — Approval #
The cardholder is reviewed, then submitted for audit. If your own operations team
signs off on the review, you can drive the submission yourself:
curl -s -X POST "$PAYCA_BASE_URL/v1/cardholders/{id}/submit" \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET"
This is asynchronous (202): the decision arrives on the cardholder webhook,
or you can poll GET /v1/cardholders/{id} until the status reaches pass_audit.
Step 5 — Issue the card #
Same call as B2B step 3 — pass cardholderId.
Cardholder Statuses #
| Status | Meaning |
|---|---|
pending_review |
Created, waiting for review. B2C only. |
rejected_by_admin |
Rejected at review (see description). Correct the data and resubmit. |
wait_audit |
Submitted, waiting for the audit result. |
under_review |
The audit has started. |
pending_approval |
Audit passed, waiting for final confirmation. |
pass_audit |
Approved — ready for card issuance. |
reject |
Rejected at audit (see description and statusFlowLocation). |
submission_failed |
Could not be submitted (e.g. a transport error, or documents missing at submit time). Retryable. |
On a B2B BIN the cardholder is returned in pass_audit directly; the
intermediate states apply to the B2C flow.
Status changes are pushed to the cardholder webhook — see
Cardholder Webhooks. Registering it is strongly
preferred over polling.
Fixing a Rejected or Failed Cardholder #
A B2C cardholder rejected over its data (a mistyped document number, a phone
already registered by another cardholder, a non-ASCII address) does not have to
be recreated — the description field carries the reason.
| Call | Use when |
|---|---|
PATCH /v1/cardholders/{id} |
The data needs correcting. Only the fields in the body change; values are written back to the linked user's meta, so a later POST /v1/users/{id}/cardholder sees the corrected data. |
POST /v1/cardholders/{id}/retry |
Status is submission_failed and the underlying cause is fixed — resubmit unchanged. |
POST /v1/cardholders/{id}/submit |
Status is pending_review and the review is done. |
PATCH is allowed in submission_failed, reject, rejected_by_admin and
pending_review. An approved cardholder (pass_audit) or one currently under
audit cannot be edited. From submission_failed, PATCH resubmits immediately
and answers with wait_audit; in the other statuses the data is stored and the
status is kept — follow with submit.
KYC documents are not part of PATCH: re-upload them via
POST /v1/users/{id}/kyc/documents before resubmitting.
Cardholder Is Always Required #
Every card issue on a Provider W BIN needs an approved cardholder. The check runs
as a preflight on POST /v1/cards and fails with HTTP 412 (Failed Precondition) and a kyc required: ... message before any funds are held or
any work is started.
The cardholder for an issue is resolved in this order:
- Explicit
cardholderIdin the request — must belong to your client, match the
BIN's card type, and bepass_audit. - Otherwise, your approved cardholder for that BIN's card type.
Passing cardholderId explicitly is recommended: it makes the attribution
unambiguous when a user holds cardholders on several BINs.
Issuance Capacity #
Card issuance on a BIN is capped. Check the remaining headroom before a bulk
issue:
curl -s -G "$PAYCA_BASE_URL/v1/bins/537100/capacity" \
--data-urlencode "cardholderId=<cardholder-uuid>" \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET"
{
"bin": "537100",
"cardholderId": "1ff17026-f9d7-4870-8d48-daee288edd0a",
"hasLimit": true,
"used": 137,
"max": 400,
"remaining": 263
}
cardholderIdis required and must belong to the BIN's card type — otherwise
the request comes back400.remainingis what you can still issue on that BIN for that cardholder. The
ceiling is BIN-specific; do not hard-code a number.hasLimit: falsemeans no cap is tracked for that pair — the counts are zero
and the cardholder should be treated as effectively unlimited.
If a BIN runs out of headroom, cardholder creation fails with this bin has no holder capacity left; contact support — an operational limit on our side, not a
problem with your request. Get in touch and it is raised.
Required User Meta Fields #
B2B BINs #
| Field | Type | Constraints |
|---|---|---|
firstName |
string | Latin letters, space, hyphen, apostrophe, period; starts with a letter; max 40 chars; TEST/MOCK/SANDBOX rejected. |
lastName |
string | Same rules as firstName. |
email |
string | Max 50 chars. Unique per BIN. |
Nothing else is read. Identity fields, if present, are ignored.
B2C BINs #
All of the above, plus:
| Field | Type | Constraints |
|---|---|---|
phone |
string | Digits only, 7–15, without the country code. Must be a mobile number for country — a landline is rejected. |
phoneCode |
string | + and 1–4 digits, e.g. "+371". A missing + is added automatically. |
birthday |
string | Date of birth, YYYY-MM-DD. |
country |
string | Country code from /v1/cardholders/regions. |
town |
string | City code from /v1/cardholders/cities?region=<country>. |
address |
string | Letters, digits, hyphens and spaces only — no commas, periods, #, / or other punctuation — max 40 chars, ASCII only. |
postCode |
string | Letters and digits only, max 15 chars. Spaces are stripped automatically (SW3 4RP → SW34RP); hyphens are rejected. |
gender |
string | M or F. |
idType |
string | One of PASSPORT, DLN, HK_HKID, GOVERNMENT_ISSUED_ID_CARD (case-insensitive). A national ID card is GOVERNMENT_ISSUED_ID_CARD — NATIONAL_ID is rejected. |
idNo |
string | Document number. |
idIssueDate |
string | Document issue date, YYYY-MM-DD. Required, even though the schema marks it optional. |
nationality |
string | Country code from /v1/cardholders/regions. |
occupation |
string | Occupation code from /v1/cardholders/occupations. |
Length limits count bytes, so a non-ASCII character consumes more than one of
the 40/50 budget.
Supported regions for B2C.
/v1/cardholders/regionslists every region
code, but not all of them are accepted for a B2C cardholder — the allowed
set is card-product specific. In particularRU(Russia) is not supported:
submittingcountry/nationality=RUfails withThis cardholder does not support the selected country/region. Use a supported region such asES
(Spain), and picktownfrom that region.
Optional fields (B2C) #
| Field | Type | Description |
|---|---|---|
idExpiryDate |
string | Document expiry date (YYYY-MM-DD). When supplied, must not be earlier than idIssueDate. |
annualSalary |
string | Annual salary range (free text). |
accountPurpose |
string | Purpose of the account (free text). |
expectedMonthlyVolume |
string | Expected monthly transaction volume (free text). |
ipAddress |
string | User's IP address. |
3DS and Activation Codes #
Codes for a card are pushed to your card webhook, keyed by cardId:
{
"cardId": "0f4b6c1e-6f6a-4c22-9d1e-6a2f8f3f8a11",
"type": "3ds_code",
"code": "418302",
"currency": "USD",
"amount": "42.10",
"merchant": "EXAMPLE STORE",
"expirationTime": 1754236800
}
type is one of 3ds_code (one-time password for a transaction),
3ds_auth_url (redirect the user to code) or activation_code. Deliveries are
idempotent — the same code may be re-sent and must not be treated as a second
challenge.
PII Handling #
Cardholder personal data is encrypted at rest in PayCA's database using
field-level envelope encryption (AES-256-GCM with an RSA-OAEP-wrapped per-record
content key). The following fields, when supplied in user meta, are stored
encrypted: phone, phoneCode, birthday, country, town, address,
postCode, gender, idType, idNo, idIssueDate, idExpiryDate,
nationality, occupation, annualSalary, accountPurpose,
expectedMonthlyVolume, ipAddress.
firstName, lastName and email are stored in plaintext at rest to support
lookup and audit views.
On B2B BINs none of the encrypted fields are collected in the first place — the
short form carries name and email only.
All client traffic to PayCA uses TLS. No request or response format changes:
submit meta exactly as documented above.
API Endpoints Reference #
| Method | Endpoint | Description |
|---|---|---|
POST |
/v1/users/{id}/cardholder |
Create a cardholder for a user (idempotent per user + BIN card type). |
GET |
/v1/users/{id}/cardholder |
Get a user's cardholder. |
GET |
/v1/cardholders |
List all your cardholders. |
GET |
/v1/cardholders/{id} |
Get a cardholder by ID (poll for status). |
PATCH |
/v1/cardholders/{id} |
Correct the data of a not-yet-approved cardholder; auto-resubmits from submission_failed. |
POST |
/v1/cardholders/{id}/submit |
Submit a reviewed pending_review cardholder. |
POST |
/v1/cardholders/{id}/retry |
Retry a submission_failed cardholder unchanged. |
POST |
/v1/cardholders/{id}/photos |
Upload passport photo / selfie for review (multipart, JPEG or PNG, max 5 MB each). |
GET |
/v1/cardholders/regions |
Country/region codes (B2C). |
GET |
/v1/cardholders/cities |
City codes, filterable by ?region= (B2C). |
GET |
/v1/cardholders/occupations |
Occupation codes (B2C). |
GET |
/v1/bins |
Full BIN catalogue with providerCardHolderModel, isEnabled, isArchived. |
GET |
/v1/bins/{bin}/capacity |
Remaining card capacity for a given cardholderId. |
Error Handling #
| Error | Cause | Resolution |
|---|---|---|
cardholder requires user meta fields: ... |
Missing required fields in user meta. |
Add the listed fields to the user. |
B2C cardholder requires user meta fields: ... |
Missing B2C-specific fields. | Add gender, idType, idNo, idIssueDate, nationality, occupation. |
firstName must be at most 40 characters ... / postCode can only contain letters and digits ... |
A field violates the charset or length rules above. | Fix the field; see the constraints tables. |
this email is already registered as a cardholder on this bin; use a different email |
The (BIN, email) pair is taken by another cardholder. |
Use a distinct email per cardholder on a BIN. |
this BIN does not use cardholders; issue the card directly |
The BIN has no cardholder model. | Call POST /v1/cards without a cardholder. |
KYC must be completed before creating a B2C cardholder |
KYC is not finished. | Complete KYC, wait for completed, then retry. |
this bin has no holder capacity left; contact support |
The BIN has no issuance headroom left. | Contact support — this is resolved on our side. |
kyc required: no approved cardholder for this bin — ... |
No approved cardholder matches the BIN's card type. | Create one with POST /v1/users/{id}/cardholder (on a B2B BIN this only needs firstName, lastName, email). |
kyc required: cardholder does not belong to this client |
The cardholderId belongs to another client. |
Use one of your own. |
kyc required: cardholder is not registered for this bin's card type |
The cardholderId was created against a different BIN's card type. |
Use a cardholder created for this BIN, or omit cardholderId. |
kyc required: cardholder status is X, expected pass_audit |
The cardholder is not approved yet. | Wait for pass_audit (B2C), or check the cardholder webhook for a rejection reason. |