Merchants upload files and submit L2 parameters directly, then query reviews and supplementary requests—all without a hosted page.
L2 Advanced Verification OpenAPI Integration for Downstream Merchants
For merchant Apps and custom clients: the merchant collects information, and its server calls the OpenAPI to upload files, submit JSON, and query review results and supplementary requests. No hosted page is required at any stage.
1. Members and Verification Levels
First create a member through POST /v1/members, then complete the member's own L1 review. Only an approved primary L1 profile qualifies for L2. Quick KYC is a separate binding of card issuance information and does not raise the member's actual verification level. An L0 member bound to Quick KYC remains L0 and may obtain cards subject to product rules, but cannot directly apply for L2.
Successful L2 submission means the application is under review; it does not immediately enable services requiring L2. After L2 approval, query the member's status before using Global Remittance APIs. Remittances remain subject to merchant service switches, product rules, and transaction validation.
2. Call Sequence
| Step | Endpoint | Permission |
|---|---|---|
| Query eligibility and enum options | GET /v1/kyc/l2/config | kyc:read |
| Upload supporting files individually | POST /v1/kyc/files | kyc:write |
| Submit L2 information | POST /v1/kyc/l2/applications | kyc:write |
| Query the latest L2 application | GET /v1/kyc/l2/applications | kyc:read |
| Query the member's verification level | GET /v1/kyc | kyc:read |
| Query pending supplements and field templates | GET /v1/kyc/supplements | kyc:read |
| Submit supplementary parameters | POST /v1/kyc/supplements/{id} | kyc:write |
Use the environment domain provided in your integration configuration. Append the paths below to that domain; never store merchant API Keys or signing keys in the App.
All endpoints above require x-auth-token: Bearer <auth_token> and x-on-behalf-of: <external_member_id 或 mem_会员ID>. Obtain the token through POST /v1/connect/token using x-client-id and x-api-key.
POST requests additionally require x-idempotency-key (one UUID per business submission), x-timestamp (Unix seconds), x-nonce (a new UUID per request), and x-signature. v2 signing is recommended: join the following six lines with newline characters, without a trailing newline, compute HMAC-SHA256 with the merchant signing key, and Base64-encode it:
POST
/v1/kyc/l2/applications
<timestamp>
<nonce>
<请求体原始字节的 SHA-256 小写十六进制>
<SHA-256(x-on-behalf-of + 换行 + x-idempotency-key) 小写十六进制>
Calculate the sixth line from the exact values of the two headers actually sent. The supported legacy v1 signature covers only the first five lines; new integrations should use v2, which also covers the member and idempotency key.
For network retries, retain the same idempotency key, member, and original body, and generate a new timestamp, nonce, and signature. Use a new idempotency key after changing information. Query parameters, when present, are included in the second signature line.
3. Options and Eligibility
GET /v1/kyc/l2/config returns enabled, l1_approved, can_apply, options, currencies, and files. can_apply indicates eligibility at query time; submission revalidates it.
options contains employment_status, industry, job_title, account_purpose, and turnover_monthly, each in the form { "value": "代码", "label": "显示文案" }. Submit value, not label, and do not hard-code the number of options. Use Accept-Language to obtain localized labels.
currencies is the array of currently allowed currencies; files returns max_bytes and formats.
4. Upload Files
Call POST /v1/kyc/files with one file's raw binary bytes as the body and Content-Type: application/octet-stream. This is not multipart, Base64, or an external image URL; calculate the signature over the raw file bytes.
JPEG, PNG, WebP, and PDF are supported, with a maximum of 8 MiB (8,388,608 bytes) per file, based on detected content. Returns HTTP 201:
{ "file_url": "https://<接入域名>/kyc/file/<会员ID>/<文件ID>.pdf" }
Store file_url exactly as returned and use it for L2 or supplementary submissions. The file must have been uploaded for the current member and must still exist. Do not use another member's file, an external URL, or a fabricated nonexistent address.
5. L2 JSON Parameters
POST /v1/kyc/l2/applications, Content-Type: application/json, with a maximum body size of 128 KiB.
In the example below, replace industry and job title with real value entries from the configuration endpoint. Replace file addresses with upload results, and use the actual consent action's timestamp and device information.
{
"profile": {
"nickname": "Alex",
"postal_code": "018956",
"tax_number": "",
"employment_status": "Employed",
"industry": "<options.industry 中所选 value>",
"job_title": "<options.job_title 中所选 value>",
"company_name": "Example Ltd",
"annual_income": "60000.00",
"account_purpose": ["PURCHASE"],
"other_purpose": "",
"banking_countries": ["SG"],
"banking_currencies": ["USD", "SGD"],
"internationally": 1,
"turnover_monthly": "TM001",
"turnover_monthly_currency": "USD",
"proof_of_address": "<地址证明 file_url>",
"source_of_funds": "<资金来源证明 file_url>"
},
"tos_acceptance": {
"accepted": true,
"ip": "203.0.113.10",
"user_agent": "<会员实际设备 User-Agent>",
"accepted_at": "2026-09-24T12:00:00.000Z"
}
}
| profile field | Required | Rule |
|---|---|---|
| nickname | Yes | String, 1–50 characters |
| postal_code | Yes | String, 1–20 characters |
| tax_number | No | String, up to 100 characters |
| employment_status | Yes | Corresponding value from the configuration endpoint |
| industry | Yes | Corresponding value from the configuration endpoint |
| job_title | Yes | Corresponding value from the configuration endpoint |
| company_name | Yes | 1–100 characters: English letters, digits, spaces, and permitted symbols. Also required for unemployed, student, and retired applicants; provide truthful information satisfying the rules |
| annual_income | Yes | Annual income in USD, as a positive numeric string, with up to 12 integer digits and 2 decimal places; JSON numbers are not accepted |
| account_purpose | Yes | Nonempty string array using configured values |
| other_purpose | Conditional | Required when OTHERS is included; up to 500 characters |
| banking_countries | Yes | Nonempty array of two-letter uppercase country/region codes |
| banking_currencies | Yes | Nonempty array of values from currencies |
| internationally | No | Number 0 or 1; defaults to 0. Booleans and strings are not accepted |
| turnover_monthly | Yes | Monthly turnover bracket value from configuration |
| turnover_monthly_currency | Yes | A value from currencies |
| proof_of_address | Yes | Address proof file_url for the current member |
| source_of_funds | Yes | Source of funds proof file_url for the current member |
| income_proof | No | Income proof file_url for the current member; may be omitted or an empty string |
| other_proof | No | Other supporting file_url for the current member; may be omitted or an empty string |
Permitted symbols in company names: . , & ( ) - + % # @ * ! $ ^ _ ? ~ and backslash.
tos_acceptance is required: send accepted: true only after genuine member consent; ip currently supports only the member's IPv4 address; user_agent must be a nonempty string of at most 1000 characters; accepted_at must be the actual consent time in UTC ISO 8601 format (ending in Z, with at most three millisecond digits), no more than 5 minutes ahead of server time. Do not substitute the merchant server's IP, fabricated device details, or fictitious consent records.
Success returns HTTP 201:
{ "id": "kl2_123", "status": "pending" }
A member cannot create another application while one is under review, awaiting supplements, undergoing supplementary review, or approved. After rejection, apply again with a new idempotency key.
6. Query Reviews and Supplements
GET /v1/kyc/l2/applications returns the latest application; application is null if the member has never applied.
{
"application": {
"id": "kl2_123",
"status": "pending",
"reject_reason": "",
"created_at": "2026-09-24T12:01:00.000Z",
"updated_at": "2026-09-24T12:01:00.000Z"
}
}
| status | Action |
|---|---|
| pending | Wait for review |
| supplement_required | Query pending supplements and submit according to the template |
| supplement_submitted | Wait for supplementary review |
| rejected | Correct the information according to reject_reason and apply again |
| approved | Query GET /v1/kyc again to confirm level=2 |
GET /v1/kyc provides a level overview. Its l2_status pending combines review and supplementary flows; none can also mean all previous applications were rejected. Use the application endpoint above for details. Query again after receiving kyc.result.updated; do not treat the event itself as approval.
Use GET /v1/kyc/supplements for pending supplementary requests. Example entry:
{
"id": "ksup_456",
"level": "L2",
"message": "请补充证明材料",
"fields": [
{ "index": 0, "label": "资金来源说明", "type": "text", "required": true },
{ "index": 1, "label": "补充证明", "type": "photo", "required": true }
],
"created_at": "2026-09-24T12:05:00.000Z",
"expires_in_days": 14
}
The actual response is {data: [条目], next_cursor: null, has_more: false} and retains the compatibility field hosted_url, which API-only clients can ignore. The list contains both L1 and L2 entries, distinguished by level. Expiration is 14 days after creation; expires_in_days is a fixed policy, not the remaining number of days.
Upload supplementary proof first, then call POST /v1/kyc/supplements/{id} (the id in this example is ksup_456):
{
"answers": [
{ "index": 0, "value": "工资收入及储蓄,详见附件" },
{ "index": 1, "value": "<补充证明 file_url>" }
]
}
Submit using the server template's index, not a translated label as the field identifier. text allows up to 500 characters; photo uses a file URL uploaded for the current member. required=true fields must be filled; optional fields may be omitted. index values must not repeat or fall outside the template range.
Success returns HTTP 200: {"id":"ksup_456","level":"L2","status":"submitted"}. The request and review state update atomically, entering supplementary review. Retrying unchanged with the same idempotency key returns the original result; resubmitting a completed request with a new key returns state_invalid.
7. Error Handling
| code | Action |
|---|---|
| kyc_required | Complete the member's own primary L1 review first; Quick KYC does not satisfy this requirement |
| invalid_fields | Read key/reason in fields, correct the parameters, and submit with a new idempotency key |
| state_invalid | Requery configuration, application, or supplementary status; it may already be submitted or approved, the supplement may have expired, or the service may be disabled |
| member_not_found / not_found | Check member context, merchant ownership, and the supplementary request ID |
| insufficient_scope | Configure the necessary permissions for the server credentials |
| invalid_request | Check the JSON structure or the 128 KiB body size limit |
| kyc_upload_invalid / kyc_upload_too_large | Check the actual file format and 8 MiB limit |
| idempotency_key_reused | Do not change the member or body for the same idempotency key |
| idempotency_in_progress | The initial submission is still processing; retry later with the same idempotency key |
8. Compatibility with Existing Integrations
POST /v1/kyc/l2/sessions continues to offer optional hosted verification. New merchant App integrations can submit parameters directly as described here, without first creating a session or using a WebView. The JSON endpoints can only be called after deployment; refer to release notices and endpoint availability in the target environment.