KYC
Transaction endpoints refuse an unverified user. Submit each end user's KYC once, wait for it to reach
VERIFIED, then transact on their behalf.
Every endpoint here is keyed by userEmail. Within your partner account, that email is the user's
identity.
Status values
| Status | Meaning |
|---|---|
NONE | Nothing submitted. |
PROCESSING | Submitted and under review. |
VERIFIED | Approved. The user can transact. |
REJECTED | Refused. rejectReason says why. Fix and resubmit. |
Submit KYC
POST /api/v2/user/submit-kyc
Signed string
partnerCode|userEmail|nationality|<secretKey>
Request
Every user is verified in full, so the identity document, the two document photos, the selfie and the
KYC report are all required. If a submission is missing one, it comes back as errorCode 5
(INVALID_REQUEST_DATA), and the message names what was missing:
Missing required fields: national ID, document type, front image, selfie image.
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
userEmail | string | Yes | The user's identity. |
signature | string | Yes | |
firstName | string | Yes | Stored uppercase, with accents removed — it reads back that way. |
lastName | string | Yes | Same. |
nationality | string | Yes | ISO 3166-1 alpha-2, uppercase — VN, PH, BR. Checked against a list. |
type | string | Yes | PASSPORT, ID_CARD, DRIVERS_LICENSE, or RESIDENCE_PERMIT. |
nationalId | string | Yes | The ID or passport number. The format is not checked. Reusing one that belongs to another of your users is rejected with 52. |
dateOfBirth | string | Yes | YYYY-MM-DD. |
gender | string | Yes | male or female. |
issueDate | string | Yes | YYYY-MM-DD. |
expiryDate | string | Yes | YYYY-MM-DD. It is not checked against today, so an expired document passes. For a document that never expires, send its issue date. |
frontIdImage | string | Yes | A base64 data URL — see below. |
backIdImage | string | Yes, unless PASSPORT | Only a passport has no back side, so it is ignored for one. Omitting it for any other type fails with 58 rather than 5 — see below. |
holdIdImage | string | Yes | A selfie of the user, as a base64 data URL. |
kycReport | string | Yes | A KYC report PDF, as a base64 data URL — data:application/pdf;base64,…. |
addressLine1 | string | No | |
addressLine2 | string | No | |
city | string | No | |
state | string | No | Falls back to city if empty. |
zipCode | string | No | |
phoneNumber | string | No | |
phoneCountryCode | string | No | Digits only, without the + — 84, 63, 1. Checked against a list of calling codes. |
Images and the report are data URLs, not bare base64. Each must look like
data:image/jpeg;base64,/9j/4AAQ…, prefix included. A bare base64 string is rejected with 58, and
this is the most common KYC integration failure.
That same check explains an odd error code. A non-passport document with no backIdImage comes back
as 58 rather than the 5 you would expect. The back image is not on the missing-fields list. It is
simply uploaded, and an empty value is not a data URL. data.invalid names the document that failed.
Response data
| Field | Type | Notes |
|---|---|---|
id | number | The KYC record id. It is not the national ID, which older docs conflate it with. |
kycStatus | string | PROCESSING — every submission goes to review. |
signature | string | Over id|kycStatus|secretKey. |
{
"success": true,
"message": "Your request has been successful",
"data": { "id": 90412, "kycStatus": "PROCESSING", "signature": "K3tQ...==" },
"errorCode": 0,
"semanticCode": "SUCCESS"
}
Errors — plus the shared ones.
27 USER_EMAIL_INVALID | userEmail is malformed. |
5 INVALID_REQUEST_DATA | A required field or document is missing — the message names them. |
35 USER_KYC_UNDER_PROCESSING | A submission is already in flight. |
52 KYC_DOCUMENT_ALREADY_REGISTERED | Already verified, or that nationalId belongs to another of your users. |
58 KYC_DOCUMENT_FORMAT_INVALID | A document is not a valid data URL. |
Supplement a KYC profile
POST /api/v2/user/submit-kyc-supplement
Backfills fields that a VERIFIED user's profile does not yet hold. The standard it has to meet is
the required set in Submit KYC. A user verified before a field joined that set is short
of it, and this is where you fill it in. A user who was never verified has nothing to supplement, and
goes through Submit KYC instead.
It patches. Send the fields you are filling in, leave the rest out, and they keep their current values.
Send nothing and you get errorCode 5.
kycStatus is untouched, so the user stays VERIFIED and keeps transacting while the supplement is
reviewed. What moves is kycSupplementStatus, which reads SUBMITTED once the call succeeds.
Signed string — it does not cover the fields you are patching.
partnerCode|userEmail|<secretKey>
Request — every field below carries the same format and meaning as in Submit KYC. Required here means this call rejects the request without it. It does not mean the profile can do without the rest.
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
userEmail | string | Yes | Identifies the profile to patch. |
signature | string | Yes | |
firstName, lastName | string | No | Uppercased and stripped of accents, as on submit. |
type | string | No | PASSPORT, ID_CARD, DRIVERS_LICENSE, or RESIDENCE_PERMIT. Switching to PASSPORT clears the stored back image, and a backIdImage sent with it is ignored. |
nationalId | string | No | Changing it to one already held by another of your users is rejected with 52. |
dateOfBirth, issueDate, expiryDate | string | No | YYYY-MM-DD. For a document that never expires, expiryDate is its issue date. |
gender | string | No | male or female. |
nationality | string | No | ISO 3166-1 alpha-2. |
frontIdImage, backIdImage, holdIdImage | string | No | Base64 data URLs — data:image/jpeg;base64,…. Each one you send replaces the stored image. |
kycReport | string | No | A KYC report PDF, as a base64 data URL — data:application/pdf;base64,…. |
addressLine1, addressLine2, city, state, zipCode | string | No | |
phoneNumber, phoneCountryCode | string | No |
Response data
| Field | Type | Notes |
|---|---|---|
id | number | The KYC record id. |
kycStatus | string | The main status. This call does not change it. |
kycSupplementStatus | string | SUBMITTED. |
signature | string | Over id|kycStatus|secretKey, using the main status rather than the supplement one. |
{
"success": true,
"message": "Your request has been successful",
"data": {
"id": 90412,
"kycStatus": "VERIFIED",
"kycSupplementStatus": "SUBMITTED",
"signature": "K3tQ...=="
},
"errorCode": 0,
"semanticCode": "SUCCESS"
}
Errors — plus the shared ones.
27 USER_EMAIL_INVALID | userEmail is malformed. |
5 INVALID_REQUEST_DATA | You sent no patchable field at all. |
50 REQUESTED_INFO_NOT_FOUND | No such user, or the user has no KYC profile — they were never submitted. |
52 KYC_DOCUMENT_ALREADY_REGISTERED | The new nationalId belongs to another of your users. |
58 KYC_DOCUMENT_FORMAT_INVALID | A document you sent is not a valid data URL. The message names which. |
Get KYC information
POST /api/v2/user/get-kyc-information
Signed string
partnerCode|userEmail|<secretKey>
Request — partnerCode, userEmail, signature. All required.
Response data
| Field | Type | Notes |
|---|---|---|
kycStatus | string | PROCESSING, VERIFIED, or REJECTED. |
kycSupplementStatus | string | NONE, SUBMITTED, REJECTED, or APPROVED. The state of a supplement, independent of kycStatus. |
rejectReason | string | Only when REJECTED. |
firstName, lastName | string | |
dateOfBirth, issueDate, expiryDate | string | YYYY-MM-DD. |
gender | string | |
nationality | string | |
nationalId | string | |
addressLine1, addressLine2, city, state, zipCode | string | |
phoneNumber, phoneCountryCode | string | |
frontUrl, backUrl, holdUrl | string | URLs, not the base64 you uploaded. backUrl is absent for a passport. |
signature | string | Over nationalId|kycStatus|secretKey. |
Three things about this response are easy to get wrong.
The images come back as URLs, not as the base64 you uploaded.
The document type is never returned. Whatever you sent as type, the profile comes back without it,
so keep your own record of whether you submitted a passport or an ID card.
A REJECTED profile returns almost nothing: kycStatus, kycSupplementStatus, rejectReason and
signature, and no other field. Because nationalId belongs to the signed string but is not populated, the string that gets
signed contains the literal text undefined:
undefined|REJECTED|<secretKey>
If you verify response signatures, special-case this. It is not a typo in this page.
Errors — 50 REQUESTED_INFO_NOT_FOUND when the user does not exist or has never submitted KYC,
plus the shared ones. A rejected profile is a success, not an error.
Check service access
POST /api/v2/user/check-access-service
Asks whether a user is allowed to use a service in a given currency — verified KYC, permitted nationality. Call it before quoting, so you fail early with a reason instead of failing at order creation.
Signed string
partnerCode|userEmail|fiatCurrency|service|<secretKey>
Request
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
userEmail | string | Yes | |
fiatCurrency | string | Yes | Not enforced against a list; use a real currency — VND, PHP, BRL. |
service | string | Yes | One of SCAN_TO_PAY, ON_RAMP, OFF_RAMP. |
signature | string | Yes |
Response data
| Field | Type | Notes |
|---|---|---|
access | boolean | true. |
signature | string | Over access|secretKey — so the signed string is true|<secretKey>. |
access is always true. A user who is not allowed does not come back as access: false —
they come back as an error envelope:
34 USER_KYC_VERIFICATION_REQUIRED | No profile, or KYC is not VERIFIED. |
59 NATIONALITY_NOT_SUPPORTED | Nationality missing, or blocked for this service and currency. |
So branch on success, not on data.access. (Older docs show this endpoint returning a full KYC
profile. It does not — that was a copy-paste error.)