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, lastName and email are read. Address, phone,
    date of birth and document fields are neither required nor used.
  • POST /v1/cardholders/files and the idFrontImg / 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 reject or submission_failed on a B2B BIN
    become usable again by simply calling POST /v1/users/{id}/cardholder for 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:

  • email is 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/SANDBOX rejected, 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/users and PATCH /v1/users/{id} store meta as 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:

  1. Explicit cardholderId in the request — must belong to your client, match the
    BIN's card type, and be pass_audit.
  2. 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
}
  • cardholderId is required and must belong to the BIN's card type — otherwise
    the request comes back 400.
  • remaining is what you can still issue on that BIN for that cardholder. The
    ceiling is BIN-specific; do not hard-code a number.
  • hasLimit: false means 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 4RPSW34RP); 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_CARDNATIONAL_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/regions lists every region
code, but not all of them are accepted for a B2C cardholder — the allowed
set is card-product specific. In particular RU (Russia) is not supported:
submitting country/nationality = RU fails with This cardholder does not support the selected country/region. Use a supported region such as ES
(Spain), and pick town from 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.