Integration guide: API
Your backend opens a case, hands its token to your app, then reads the decision. Every address in this guide is relative to https://bazik.id.
API keys
- They are created in the partner console, Intégration page, by an Owner or a Developer. The full key is shown only once, when it is created: Bazik only keeps its hash.
bz_test_…: test environment.bz_live_…: production, available once Bazik has opened your space in production (after the contract).- Every call from your backend carries the key in the
X-Api-Keyheader. - A revoked key stops working within a minute.
Cases opened with a test key stay apart: the console shows them when you pick Test at the top of the screen. The checks are the same as in production; use your own test documents, never real customers'.
1. Open a case
POST /v1/cases
X-Api-Key: bz_live_…
Content-Type: application/json
{ "reference": "CLI-48210" }
The body is optional. reference is your own reference for the case (customer or application number): you find it in the console and in the response. Letters, digits and . _ : - only, 64 characters at most: never a name, a phone number or personal data.
{
"case_id": "cas_7e3d90c2a41f5b08",
"access_token": "…",
"expires_at": "2026-11-10T14:42:03Z",
"reference": "CLI-48210"
}
The token (access_token) is valid for this case only, for 30 minutes. A case without a decision after 30 minutes expires: open a new one to start over.
curl -X POST https://bazik.id/v1/cases \
-H "X-Api-Key: $BAZIK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reference": "CLI-48210"}'
2. Hand the token to the app
Your backend returns access_token to your app, which runs the flow with the SDK. The app gets nothing else: neither the key nor the decision that counts.
Without the SDK (your own app, a test), the app calls the three routes below itself, with the Authorization: Bearer <access_token> header.
Send the document photos
POST /v1/kyc/documents
Authorization: Bearer <access_token>
Content-Type: multipart/form-data
document_type = cin | passport
side = recto | verso (CIN) · data_page (passport)
image = the photo, JPEG, 15 MB at most
One side at a time. The response is { "accepted": true }, or { "accepted": false, "reason": "blurry" | "glare" | "unreadable" }: take the photo again. On the 3rd try for the same side, the server keeps it as is and lets the decision handle it. As soon as the front of a CIN is read, the national registry is looked up in the background.
With Accept: text/event-stream, the response comes live: a check event for each finished check (document_visible, no_glare, text_readable, with ok), then result with the same content as the JSON response.
Open the selfie session
POST /v1/liveness/sessions
Authorization: Bearer <access_token>
{
"session_id": "ses_…",
"nonce": "…",
"challenges": ["blink", "turn_left", "smile"],
"pauses_ms": [900, 1300],
"issued_at": "2026-11-10T14:14:00Z",
"expires_at": "2026-11-10T14:15:30Z",
"server_sig": "…"
}
Three challenges among blink, turn_left, turn_right, smile and open_mouth, and two pauses to respect between them. The session lasts 90 seconds and can be used once.
Send the selfie
POST /v1/liveness/sessions/{session_id}/submit
Authorization: Bearer <access_token>
Content-Type: multipart/form-data
payload = JSON (below)
frame_0, frame_1, … = the key frames, JPEG, 3 at least, 12 at most
{
"nonce": "the session's nonce",
"frames": [{ "t_ms": 0, "part": "frame_0" }, { "t_ms": 2140, "part": "frame_1" }],
"events": [{ "challenge": "blink", "shown_ms": 1200, "done_ms": 2100 }],
"attestation": "",
"device_sig": ""
}
- Times are in milliseconds since the start of the session.
shown_ms: when the challenge was shown;done_ms: when the gesture was recognized. eventsfollows the order of the session's challenges. A reaction under 120 ms, a challenge out of order or an expired session send the case to review.- One frame facing the camera at the start, then one key frame per gesture, at the moment it is made: the server rechecks the challenges and compares the faces on those frames.
attestationanddevice_sig: the device attestation (Play Integrity). Without it, outside dev mode, the case always goes to manual review.
The response is { "outcome": "verified" | "manual_review" }. It is only indicative, for the final screen: the decision that counts is the one your backend reads.
3. Read the decision
GET /v1/cases/cas_7e3d90c2a41f5b08
X-Api-Key: bz_live_…
The response is 404 until the person has submitted their selfie. Then:
| Field | Contents |
|---|---|
outcome | verified or manual_review: this is what you act on |
decision | accepter, revue or rejet: the strictest signal. A rejet always gives manual_review |
raisons | the checks that did not accept (registre, comparaison_document…) |
signaux | each check: nom, issue and its detail |
document | the data read on the document (donnees) and the MRZ |
registre | DELIDOC's answer: NINU status, date of birth, error if any |
reference, document_type, recu_le | your reference, cin or passport, the time of the decision |
revue | present once the manual review has been decided in the console, see below |
{
"case_id": "cas_7e3d90c2a41f5b08",
"reference": "CLI-48210",
"outcome": "manual_review",
"decision": "revue",
"raisons": ["registre", "comparaison_document"],
"signaux": [
{ "nom": "document", "issue": "accepter" },
{ "nom": "registre", "issue": "revue", "detail": { "statut": "non_consulte" } },
{ "nom": "comparaison_document", "issue": "revue", "detail": { "similarite": 0.31 } }
]
}
The checks, from the most to the least frequent in review: registre (DELIDOC did not answer, or the NINU is not active), comparaison_document (likeness between the selfie and the document photo), document (document to check, photo unreadable after 3 tries), comparaison_registre (likeness with the official photo), chronologie, integrite_session, visages_session.
Manual review
A manual_review case waits in the console's Revue manuelle (manual review) page. An Analyst or the Owner compares the faces, then accepts or refuses the identity with a reason. Your backend reads it in the revue field:
"revue": { "issue": "accepter", "motif": "registre_indisponible", "tranchee_le": "2026-11-10T14:15:09Z" }
issue:accepterorrefuser.motif:visage_concordant,registre_indisponible,piece_verifiee_autrement(accepted);visage_different,piece_douteuse,selfie_inexploitable,autre(refused).- The field only appears once the review is decided, and a review is decided once.
Bazik never tells the person about a refusal: you contact them.
Following the progress
GET /v1/cases/{case_id}/progress, with the API key, gives the state of each step (document, registry, selfie, decision) during the flow, with no data from the document. It answers 404 once the case is closed. The response of GET /v1/cases/{case_id} holds "decision" as soon as it exists: that is the field to test when you poll.
Errors
| Code | Cause | What to do |
|---|---|---|
401 | missing, unknown or revoked key | check the X-Api-Key header |
403 | partner space suspended | contact Bazik |
404 | unknown case, another partner's case, or not decided yet | wait for the end of the flow, or check the id |
422 | reference refused | letters, digits and . _ : - only |
429 | too many cases opened this minute | retry after the Retry-After delay |
503 | key check temporarily unavailable | retry in a moment |
Another partner's case answers exactly like a case that does not exist.
In short
- The key stays on your server; the app only gets the case token.
- The decision that counts is the one your backend reads, never the one the app shows.
manual_reviewmeans "pending": neither accepted nor refused until the review is decided.- The customer reference is never personal data.