Overview #
Provider W cards require a cardholder before a card can be issued. The cardholder goes through a two-stage approval:
- PayCA review — cardholder is created with
pending_reviewstatus and must be approved by a PayCA admin. - Provider audit — after PayCA approves, the cardholder is submitted to Provider W and moves through
wait_audit→pass_audit(orreject).
Only cardholders with status: "pass_audit" can be used to issue cards.
Cardholder Statuses #
| Status | Meaning |
|---|---|
pending_review |
Created, waiting for PayCA admin approval. |
rejected_by_admin |
Rejected by PayCA admin (see description for reason). |
wait_audit |
Submitted to Provider W, waiting for provider audit. |
under_review |
Provider W is reviewing the cardholder. |
pass_audit |
Approved — ready for card issuance. |
reject |
Rejected by Provider W (see description and statusFlowLocation). |
B2B Flow #
B2B cardholders require basic personal data only. No KYC documents are needed — file IDs are uploaded manually via the file upload endpoint.
Step 1 — Create 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",
"phone": "5551234567",
"phoneCode": "+1",
"birthday": "1990-01-15",
"country": "US",
"town": "NYC",
"address": "123 Main St",
"postCode": "10001"
}
}'
Use /v1/cardholders/regions for country codes and /v1/cardholders/cities for town codes.
Step 2 — Upload identity documents (optional, B2B only) #
curl -s -X POST "$PAYCA_BASE_URL/v1/cardholders/files" \
-H "x-client-id: $PAYCA_CLIENT_ID" \
-H "x-client-secret: $PAYCA_CLIENT_SECRET" \
-F "file=@passport_front.jpg"
Save the returned fileId values and include them in the user meta as idFrontImg, idBackImg, idHoldImg.
Step 3 — Create 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"
}'
Response returns a CardholderResponse with status: "pending_review".
Step 4 — Wait for approval #
Poll GET /v1/cardholders/{id} until status changes:
pass_audit— proceed to card issuance.rejected_by_admin— rejected by PayCA; checkdescriptionfor the reason.reject— rejected by Provider W; checkdescriptionandstatusFlowLocation.
Step 5 — Issue 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>"
}'
3DS OTP codes are sent to the cardholder's email and phone.
B2C Flow #
B2C cardholders require extended personal data and completed KYC verification. KYC documents are uploaded automatically from the KYC system when the cardholder is submitted to Provider W.
Step 1 — Create user #
Include all B2B fields plus B2C-specific fields:
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",
"nationality": "US",
"country": "US",
"town": "LAX",
"address": "456 Oak Ave",
"postCode": "90001",
"occupation": "IT"
}
}'
Use /v1/cardholders/occupations for occupation codes.
Step 2 — KYC verification #
Upload identity documents and a selfie, then submit for review. 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.
Wait for status: "completed" on GET /v1/users/{userId}/kyc (or subscribe to the kyc webhook) before proceeding.
Step 3 — Create cardholder #
Same as B2B Step 3. KYC must be completed — the endpoint rejects requests if KYC is not completed.
Step 4 — Wait for approval #
Same as B2B Step 4. After PayCA admin approves, KYC documents are automatically uploaded to Provider W.
Step 5 — Issue card #
Same as B2B Step 5 — pass the cardholderId in the card creation request.
Cardholder Is Always Required for Provider W #
Every Provider W card issue requires the requesting client to already have a pass_audit cardholder for the BIN's card type. The check runs as a preflight on POST /v1/cards; failures return HTTP 412 (Failed Precondition) with a kyc required: ... message before any funds are held or any workflow is started.
Once the client-level KYC is in place, the provider-side holderId for the actual card issuance is resolved in this priority:
- Explicit
cardholderIdin the request — must belong to your client, match the BIN's card type, and bepass_audit. - User's approved cardholder for the card type (
/v1/users/{id}/cardholder). - BIN-internal fallbacks (b2b holder pool, BIN default holder) — used when no per-user cardholder is set; never bypasses the client-level KYC preflight.
In practice: create the cardholder, wait for pass_audit, then issue. There is no longer a path that lets a client issue a Provider W card before completing its own KYC.
Required User Meta Fields #
B2B (minimum) #
| Field | Type | Description |
|---|---|---|
firstName |
string | First name. |
lastName |
string | Last name. |
email |
string | Email address. |
phone |
string | Phone number (digits only). |
phoneCode |
string | Country dial code (e.g. +1). Normalized to include + prefix. |
birthday |
string | Date of birth (YYYY-MM-DD). |
country |
string | Country code (from /v1/cardholders/regions). |
town |
string | City code (from /v1/cardholders/cities). |
address |
string | Street address. |
postCode |
string | Postal code. |
B2C (additional) #
| Field | Type | Description |
|---|---|---|
gender |
string | M or F. |
idType |
string | Document type: PASSPORT, DLN, etc. |
idNo |
string | Document number. |
nationality |
string | Nationality code (from /v1/cardholders/regions). |
occupation |
string | Occupation code (from /v1/cardholders/occupations). |
Optional fields (B2B and B2C) #
| Field | Type | Description |
|---|---|---|
idIssueDate |
string | Document issue date. |
idExpiryDate |
string | Document expiry date. |
idFrontImg |
string | File ID of document front (B2B only, via /v1/cardholders/files). |
idBackImg |
string | File ID of document back (B2B only). |
idHoldImg |
string | File ID of selfie holding document (B2B only). |
annualSalary |
string | Annual salary range. |
accountPurpose |
string | Purpose of account. |
expectedMonthlyVolume |
string | Expected monthly transaction volume. |
ipAddress |
string | User's IP address. |
API Endpoints Reference #
| Method | Endpoint | Description |
|---|---|---|
POST |
/v1/users/{id}/cardholder |
Create cardholder for a user. |
GET |
/v1/users/{id}/cardholder |
Get cardholder status for a user. |
GET |
/v1/cardholders |
List all cardholders for the client. |
GET |
/v1/cardholders/{id} |
Get cardholder by ID. |
GET |
/v1/cardholders/cities |
List city codes for registration. |
GET |
/v1/cardholders/occupations |
List occupation codes (B2C). |
GET |
/v1/cardholders/regions |
List country/region codes. |
POST |
/v1/cardholders/{id}/photos |
Upload passport photo and/or selfie for admin review (multipart/form-data). |
POST |
/v1/cardholders/files |
Upload file for B2B cardholder registration. |
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 admin-side lookup and audit views.
In transit, all client traffic to PayCA uses TLS. PayCA forwards plaintext
cardholder data to Provider W over TLS — Provider W does not currently
support encrypted intake, so end-to-end encryption to the issuer is out of
scope. No request/response format changes — clients continue to submit
meta exactly as documented above.
Error Handling #
| Error | Cause | Resolution |
|---|---|---|
cardholder requires user meta fields: ... |
Missing required fields in user meta. | Update user meta with the listed fields. |
B2C cardholder requires user meta fields: ... |
Missing B2C-specific fields. | Add gender, idType, idNo, nationality, occupation. |
bin does not support provider w cardholders |
BIN has no cardholder type configured for Provider W. | Use a BIN that supports Provider W cardholders. |
KYC must be completed before creating a B2C cardholder |
KYC not finished. | Complete KYC verification first. |
kyc required: no pass_audit cardholder for this bin |
Client has no approved cardholder matching this BIN's card type. | Create a cardholder via POST /v1/users/{id}/cardholder for this BIN and wait for pass_audit. |
kyc required: cardholder does not belong to this client |
The cardholderId in the issue request belongs to a different client. |
Use a cardholderId owned by the requesting client. |
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 whose card type matches the issuance BIN, or omit cardholderId to fall through to client-level lookup. |
kyc required: cardholder status is X, expected pass_audit |
The cardholderId exists but is not yet approved. |
Wait until the cardholder reaches pass_audit. |
cardholder not approved (status: ...) |
Provider-layer fallback for the same condition (only seen if the adapter preflight is bypassed in tests). | Wait for cardholder approval. |