Z Zise Developers 简体中文
Account Center › Guides

Obtain a link for the end user to fill in fifteen fields and upload ID photos on our page—the data never passes through your system.

L1 KYC: Hosted Page and Required Information

You Only Need One Step


POST /v1/kyc/sessions
x-on-behalf-of:    <external_member_id>
x-idempotency-key: <UUID>

Receive 201:


{
  "hosted_url": "https://api.zinfra.dev/hosted/kyc/kyc_352ffcf7efe94531b3da9f902f2e4348",
  "expires_at": "2026-08-14T15:47:53.000Z"
}

⚠ x-idempotency-key is required (as it is for all write endpoints). Omitting it immediately returns 400

idempotency_key_required—the first error you may encounter in this flow.

⚠ expires_at is an ISO8601 string, not Unix seconds.

Pass this link to the end user through an in-app WebView, SMS, or email. Everything else takes place on our page.

The Ticket Is Bound to Three Things

BindingWhat happens without it
MemberYou could use A's link to create a profile for B
MerchantAnother merchant could consume the ticket
24 hours + single useA link in chat history could be forwarded and reused

⚠ Validity is 24 hours, not 5 minutes. You must pass it to the end user,

who then needs to find their ID and take photos. Do not expect longer—anyone holding this URL

can submit a KYC profile in that member's name. Issue a new one if it expires.

What the End User Enters on That Page

Fifteen Text / Selection Fields (All Required)

FieldTypeNotes
first_name / last_nameText ≤60
sexSelectionmale | female
birth_dateDateAn age range applies, see below
nationalityText, 2 charactersCountry code
occupationText ≤60
phone_zone_numberText ≤6Calling code
phonePhone number ≤20
identity_typeSelectionid_card | passport | resident_card | drivers_license
identity_numberText ≤60We do not validate the number format—national rules differ, and false rejection costs more than accepting an unusual format
cardholder_residenceSelectionchinese-mainland | chinese-mainland-residents-living-overseas | overseas-countries—see the warning below
address_province / address_cityText ≤60
addressText ≤160
street_numberText ≤40

Email is prefilled and cannot be changed—review results are sent to the address used when the member was created.

⚠ ID numbers and email addresses are both checked for duplicates across people. A conflict returns 400 at submission

(kyc_identity_taken / kyc_email_taken), with no manual override,

and no signal is sent to your side. For causes and prevention, see the relevant section

in the KYC Overview.

⚠ The default age range is 18–65 (configurable in the admin console). Applicants outside it are rejected immediately,

and this upper limit is not included in any response—you cannot determine eligibility in advance.

A user aged 66 or older may complete all 15 fields and upload three ID photos before being blocked at submission.

Products serving older users should explain this before starting the flow.

⚠ chinese-mainland is currently rejected (“Registration is not currently supported for mainland China residents”),

and mainland China phone numbers are also rejected. This is an admin configuration switch

(allow_mainland), not a permanent rule—which is why the option remains in the table.

Explain this early in your user guidance instead of blocking users after all fifteen fields are completed.

Three Photos

ItemForm fieldRequired
Front of IDphoto_frontYes
Back of IDphoto_backNo—a passport only has an information page
Selfie holding IDphoto_handheldYes

⚠ back is not marked required on the page, but non-passport documents do require a back image—

the server makes this determination; the page does not duplicate it and create a second source of truth.

Each image must be ≤ 8 MB. We detect the actual type from magic bytes, rather than trusting the client's declared Content-Type. Images are uploaded directly to our private storage and can only be retrieved by the owner and reviewers.

⚠ Depending on residence type, reviewers may additionally request a residence permit—

this uses the supplementary documents flow, not the initial form.

How Is the Completed Form Submitted?

It does not pass through your system. You have nothing to do at this step.

We render the page, and its <form> points to hosted_url itself:


<form method="post"
      action="/hosted/kyc/kyc_352ffcf7…"
      enctype="multipart/form-data">

The end user selects Submit → the browser sends the 15 text fields and photos as multipart/form-data in a same-origin POST directly to us (the photo form fields are photo_front / photo_back / photo_handheld)。

⚠⚠ This is the entire reason for the hosted page: ID images and numbers never pass through

your server, logs, CDN, or error reporting.

You only ever hold a URL and the subsequent review result.

Do not look for a “submit information” API—there is no such endpoint, and there should not be one.

What Happens After Submission

ResultPageTicket
Validation passesFinal “Submitted” pageOnly consumed at this point
Validation failsText is prefilled, errors appear, and the user can correct it immediatelyRemains valid

⚠ Failed validation does not consume the ticket—otherwise a single typo would force users to ask you for a new link.

However, photos must be selected again: browsers do not allow prefilling file controls. That is what the page's reminder explains.

Calling POST /v1/kyc/sessions again after successful submission returns 400 state_invalid (the member already has a pending or approved profile).

The Member's kyc_level Does Not Change Immediately

Submission only hands over the information; review is asynchronous. Until a decision is made, the kyc_level from GET /v1/members/{id} remains 0. Do not treat “Submitted” as “KYC approved” when enabling services—wait for kyc.result.updated。

What You Can Actually See

⚠⚠ Read this first, because it determines how to design your state machine.

GET /v1/kyc returns only four fields, and status has only three states:

statusWhen it applies
approvedlevel ≥ 1
pendingA profile is under review
rejectedRejected, with no profile currently under review
noneNever submitted

⚠ pending takes precedence over rejected. A user who resubmits after rejection has two records,

but their current state is “under review”—do not prompt them again.

⚠ On rejected, guide the user through the process again: call POST /v1/kyc/sessions

to obtain a new link (there is no ability to edit a few fields and resubmit; see below).

⚠ fail_reason (the rejection reason) is never exposed through the Open API. Event payloads

are strictly limited to IDs and states. Do not look for this field or promise users a reason in your interface.

Our Five Internal States Are Not Exposed

UNREVIEWED / APPROVED / REJECTED / SUPPLEMENT_REQUIRED / SUPPLEMENT_SUBMITTED is an internal transition, included here only to explain why several events may arrive.

⚠ If you write switch (status) { case "APPROVED": … } based on those states,

no branch will ever match, with no error—events arrive normally, signatures are valid, and you return 2xx.

The symptom is “the user's KYC was approved, but our system still shows pending.”

Events Are Not Sent for Every Transition

kyc.result.updated is sent only at two points:

Two transitions do not emit events:

TransitionConsequence
User finishes L1 submission (→ pending review)There is no “submitted” event—poll GET /v1/kyc for pending if you need to know
User submits supplementary documents (→ awaiting another review)Same: no event

⚠ If incoming events are the only trigger for advancing your state machine, these two states will remain stuck forever,

and your webhook endpoint will be the first thing you suspect during troubleshooting.

What to Do After Rejection

There is no option to return to the previous profile and edit a few fields (that path requires the member's own login session, while members created through POST /v1/members receive a random placeholder password and cannot log in to our App).

The only Open API path is to call POST /v1/kyc/sessions again for a new link.

⚠ The new link opens a blank form—the previous information is not prefilled.

Tell users in advance that they need to enter everything again.

This Is Not Quick KYC

Exclusively binding a profile from the platform pool (Quick KYC) does not change level to 1 and cannot replace the hosted page described here. Do not use either flow as a fallback for the other.

Related Endpoints