Start L1 KYC (returns a link to our hosted screen)
x-on-behalf-of required
Requires x-idempotency-key
Called on behalf of a member; x-on-behalf-of is required.
This follows kyc_required: create member → order returns kyc_required → GET /v1/kyc/requirements indicates level 1 → obtain a link here and give it to the end user to complete.
⚠ No document-image or original-profile bytes pass through your servers: we host the form. You receive only a URL and expiry time. This is a compliance requirement and explains why the Open API has no identity-profile submission input.
Prerequisites
- The member has no non-rejected L1 profile; otherwise returns
state_invalid.
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-on-behalf-of |
string | Required | The member for whom to initiate KYC. Accepts external_member_id or mem_<uuid>.
Omitting it returns member_context_required.
⚠ It participates in the idempotency fingerprint, as described above. |
x-idempotency-key |
string | Required | UUID v4. One per member; sharing a key across a batch causes
409 idempotency_key_reused. |
Request Body
No fields. The member is determined entirely by x-on-behalf-of.
Response
hosted_url to the end user.
The response deliberately has no session ID, as described above.{
"hosted_url": "https://api.zinfra.vip/hosted/kyc/kyc_8f2a1c9e5b7d403a91e6c4d2b7f05a13",
"expires_at": "2026-08-14T05:00:00.000Z"
}member_context_required: missing x-on-behalf-of.
state_invalid: the member already has a non-rejected identity profile.
No internal reason is returned; query progress through GET /v1/kyc.insufficient_scope: missing kyc:write.member_not_found, including members belonging to another merchant or suspended members.idempotency_key_reused: same key with a different member or body.
idempotency_in_progress: the original request is still being processed.Additional Details
Returns only hosted_url, not a separate session ID
The final segment of hosted_url is the credential itself. The URL requires no additional authentication. Anyone possessing it can submit a complete L1 profile and upload identity-document images as that member for 24 hours.
We deliberately do not return it separately as an apparently nonsensitive session identifier. That would encourage storing it in business tables and request logs, granting every reader of those logs the ability to impersonate the member for KYC submission. Use your own external_member_id for correlation.
Likewise, do not log this link or forward it to third parties in URL parameters.
Valid for 24 hours; request another after expiry
The link must reach the user, who then finds documents and takes photos, so its lifetime is not just a few minutes. There is no grace period after expiry. Call this endpoint again, using a new x-idempotency-key.
⚠ The idempotency window and ticket lifetime are both 24 hours and begin at the same moment. Reusing the key within the window replays the original response with X-Idempotent-Replay: true, meaning a link with less remaining lifetime. A replay in hour 23 returns a link that expires 1 hour later. Use a new key for a fresh link; replaying the same key does not renew it.
⚠ One idempotency key per member
The endpoint path and request body are both constant, with an empty-object body. Only x-on-behalf-of identifies the member. We include this header in the idempotency fingerprint: same key, different member → 409 idempotency_key_reused, rather than replaying the previous member's link.
When issuing links in bulk, generate a new UUID for each member.
Members with an existing profile do not receive a ticket
If the member already has a non-rejected identity profile, including one under review, this endpoint immediately returns 400 state_invalid. It does not issue a link that is guaranteed to fail: otherwise you would forward it to the user, who would fill fifteen fields before being rejected. Check progress through GET /v1/kyc (pending means submitted for review) and GET /v1/kyc/supplements, which provides a link when supplements are needed. Rejected members may receive another link.
curl -X POST 'https://api.zinfra.vip/v1/kyc/sessions' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-on-behalf-of: $MEMBER_ID' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{}'const res = await fetch("https://api.zinfra.vip/v1/kyc/sessions", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
body: JSON.stringify({}),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/kyc/sessions",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/kyc/sessions",
strings.NewReader(`{}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-on-behalf-of", "$MEMBER_ID")
req.Header.Set("x-idempotency-key", "$IDEMPOTENCY_KEY")
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
// Decode amount fields as string, not float64.HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.zinfra.vip/v1/kyc/sessions"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-on-behalf-of", "$MEMBER_ID")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{}
"""))
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/kyc/sessions');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-on-behalf-of: $MEMBER_ID',
'x-idempotency-key: $IDEMPOTENCY_KEY',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"hosted_url": "https://api.zinfra.vip/hosted/kyc/kyc_8f2a1c9e5b7d403a91e6c4d2b7f05a13",
"expires_at": "2026-08-14T05:00:00.000Z"
}