API reference
FirstPass is a KYC pre-screen: face match, ID reading and a face fingerprint for deduping your own users. It is not certified eKYC and not an identity verification method under Japan's 犯収法. Liveness only runs in hosted sessions.
The examples illustrate responses with synthetic noise images (no real face or document) and document reading not configured. That is why face results show no_face and document reading shows ocr_unavailable; with real photos those fields carry the values described below.
Basics
- Base URL:
https://api.firstpass.cloud - Customer integration endpoints (session creation and polling, direct and granular checks) need
Authorization: Bearer YOUR_KEY. Hosted config, submit and help use the session link as a credential; health, demo, signup and website support do not require an API key. Get a key and top up credits in the dashboard. - The JSON model endpoints documented here need
Content-Type: application/json. Without it the body is not parsed and the call fails with 422. - Images are base64 strings (a
data:URL prefix is accepted), JPEG, PNG or WebP, up to 8 MiB of base64 each, including any data URL prefix (hosted scan frames and videos have separate limits). - HTTP and request-validation errors return
{"detail": ...}with the HTTP status (see Errors).
Hosted sessions (recommended)
You open a session and send the applicant to the returned url. On that page they do a live face scan (selfie uploads are disabled; ID uploads are allowed), photograph their ID and, for Level B, add proof of residence, source of funds and declared details. FirstPass runs the check, stores the record in your workspace, and you poll for the result. This is the only flow with server-side liveness checks.
Create a session
| Field | Type | Notes |
|---|---|---|
level | "A" or "B" | Default "A". Send "A" or "B". The model permits strings of at most one character; only "B" selects Level B, and longer strings return 422. |
applicant_ref | string, optional | Your own id for the person, up to 120 characters. Returned when you poll. |
lang | "en", "zh" or "ja", optional | Language of the scan page: a valid URL language takes priority, then the browser’s saved choice, then English. |
return_url | string, optional | Stored with the session, up to 300 characters. |
curl -X POST https://api.firstpass.cloud/v1/kyc/session \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"level": "A", "applicant_ref": "user_123", "lang": "en"}'
{
"session_id": "_2L_jByICyPD_528Fk6JB7Y_y6SwJAu3",
"url": "https://firstpass.cloud/kyc.html?session=_2L_jByICyPD_528Fk6JB7Y_y6SwJAu3&level=A&lang=en",
"status": "pending"
}
Creating a session costs nothing, but your workspace must have a positive balance and be under its monthly quota (otherwise 402 or 429). The first billable submission costs 1 credit for Level A or 2 for Level B; a positive balance at creation does not guarantee enough credits for B. Retries are free while the session remains charged. After a refund, the next billable submission is charged again. A link expires 24 hours after creation and accepts at most 5 model-valid submissions, including failed liveness and ID-side prechecks. Verified or closed sessions cannot be retried. Creation checks a 300-session hourly threshold per workspace; concurrent requests can exceed it.
Poll a session
Needs the Authorization header. Only the workspace that created the session can read it; any other id returns 404. Polling works even when your balance is 0.
curl https://api.firstpass.cloud/v1/kyc/session/SESSION_ID \ -H "Authorization: Bearer $KEY"
{
"session_id": "_2L_jByICyPD_528Fk6JB7Y_y6SwJAu3",
"status": "pending",
"level": "A",
"verdict": null,
"record_id": null,
"applicant_ref": "user_123"
}
# Ran locally with the face and OCR engine replaced by synthetic values (no real
# person can be scanned in a test). Billing, storage and this response are the
# real endpoint code. face_embedding is 128 numbers, shortened here.
{
"session_id": "4WWxCP-EB-khWirnzZNBb61Rat8M9XE9",
"status": "done",
"level": "A",
"verdict": "verified",
"record_id": 1,
"applicant_ref": "user_123",
"result": {
"outcome": "verified",
"reason": "ok",
"face_match": { "faces_a": 1, "faces_b": 1, "similarity": 0.6712, "verdict": "match" },
"document": {
"doc_type": "パスポート", "doc_number": "TZ0000000", "name": "TEST APPLICANT",
"birth_date": "1990-01-01", "expiry_date": "2031-05-14", "expiry_printed": true
},
"identity": {
"level": "A",
"name": { "input": "Test Applicant", "valid": true },
"email": { "input": "applicant@example.com", "valid": true, "mx_found": true, "disposable": false },
"phone": { "input": "+81 90 1234 5678", "valid": true, "e164": "+819012345678", "country": "JP", "type": "mobile" },
"passed": true
},
"face_embedding": [0.0123, 0.0123, ...],
"liveness_flash": { "result": "pass", "score": 1.0, "signal": 0.0412, "sequence_ok": true,
"measured": [{ "color": "R", "fraction": 0.4691, "hit": true }] }
}
}
statusispending,doneorexpired(a link nobody finished within 24 hours).- The top-level
verdictis the overall outcome (verified,review,rejected, orerasedafter an erasure request). The face-only verdict isresult.face_match.verdict. resultappears once the session is done. Level B addsproof_of_residence,source_of_fundsandedd(POR/SOF keys are omitted when their values are null) (see Level B). Images are not returned; view them in the dashboard. Level B residence and funds document images are the exception: they are not in the dashboard, exports or API, only authorised FirstPass staff reviewing records can view them through the staff page (access is logged). They become inaccessible after 90 days; startup or the next expiry job overwrites the encrypted images. The fields read from them stay in the result.liveness_flash.resultispass,fail,inconclusiveornot_measured.sequence_okis optional (absent on thenot_measuredearly return) and says whether the flash frames came in the colour order this session issued (nullif none was issued). A clear reading in the wrong order is afailwithfail_reason: "sequence"and changes an otherwiseverifiedcheck to review (flash_sequence_review); a wrong reflection pattern isfail_reason: "reflectance"(flash_review). Existing rejection or review reasons take priority; flash downgrades only an otherwise verified result.
Outcomes and reasons
A hosted check ends as verified, review or rejected. Review cases are yours to decide: The service requesting verification is responsible for decisions and applicant follow-up; no reviewer assignment or response is guaranteed. FirstPass staff access to proof images supports operations and is not an applicant-review service. If you need original proofs for your own review, arrange collection with the applicant through your own process. verified needs a matching face, an accepted in-date ID (residence card, driver's license, My Number card, passport, or another country's national ID card), valid name, email and phone (format/MX/disposable-domain checks, not mailbox delivery or a name-to-ID comparison), and a passing screen-flash reading. A national ID explicitly marked expiry_printed: false does not require an expiry date. Earlier face/ID/identity reasons take priority; Level B missing-document and cross-check reasons apply only after preceding checks pass.
| reason | outcome | Meaning |
|---|---|---|
ok | verified | All checks passed. |
no_face_selfie, no_face_document | rejected | No face found in the scan or on the ID photo. |
document_back_side | review | No face anywhere on the ID image (the back of the card, or not the photo side). The scan page normally catches this first and asks for the photo side before anything is charged or stored. Not charged. |
bad_document | rejected | Not one of the accepted ID types. |
expired_document | rejected | The ID's expiry date has passed. |
face_mismatch | rejected | The face does not match the ID photo. |
missing_documents | rejected | Level B without both the residence and funds documents. |
uncertain | review | Face similarity in the uncertain band. |
expiry_unreadable | review | Accepted ID type, but the expiry date could not be read. |
identity_review | review | Name, email or phone did not validate. |
flash_review | review | The screen-flash reflection pattern was wrong. |
flash_sequence_review | review | The screen-flash colours reflected clearly, but not in the order this session issued. |
liveness_review | review | No usable screen-flash reading (skipped, too bright, older page). |
documents_unreadable | review | Level B residence or funds document could not be read. |
edd_review | review | Level B cross-check disagreed, or the proof of residence is older than 90 days or future-dated (JST calendar). This is not a rejection; upload a proof issued within the last 90 days, no later than today. |
ocr_unavailable | review | Our document reader was down. Not charged. |
Direct Level A and B
If you capture images yourself, post them directly. These calls run no liveness check: they accept any selfie image, including an uploaded photo. They also return no overall outcome: you get the face verdict, the document fields and the identity checks, and decide yourself.
ID images must show the photo side. When no face is found anywhere on the ID image (face_match.faces_b is 0), it is most likely the back of the card: the record keeps the fields read from it but not the image, and you should ask the applicant to photograph the front. The same applies to /v1/face/compare (image_b) and /v1/document/ocr.
Level A
1 credit. Body: name, email, phone, selfie, document (required), region (default country for the phone number, default "JP").
curl -X POST https://api.firstpass.cloud/v1/kyc/a \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d @kyc_a.json
# kyc_a.json: {"name": "Test Applicant", "email": "applicant@example.com",
# "phone": "090-1234-5678", "region": "JP", "selfie": "BASE64_JPEG", "document": "BASE64_JPEG"}
{
"level": "A",
"identity": {
"level": "A",
"name": { "input": "Test Applicant", "valid": true },
"email": { "input": "applicant@example.com", "valid": true, "mx_found": true, "disposable": false },
"phone": { "input": "090-1234-5678", "valid": true, "e164": "+819012345678", "country": "JP", "type": "mobile" },
"passed": true
},
"face_match": { "faces_a": 0, "faces_b": 0, "similarity": null, "verdict": "no_face" },
"document": { "ocr_unavailable": true },
"face_embedding": null
}
With real photos, face_match.verdict is match, uncertain, mismatch or no_face, similarity is 1 minus the face distance, document holds doc_type, doc_number, name, birth_date, expiry_date and expiry_printed (or is null when the image is not a readable ID), and face_embedding is the selfie's 128-number fingerprint for deduping within your own platform. When OCR classifies an image as a My Number card, doc_number is cleared and sensitive-number filtering is applied to OCR text fields. This is not a guarantee against misclassification; submit only the photo side and mask sensitive numbers before upload.
Level B
2 credits per call, even if the optional proofs are omitted. The official hosted B page requires both proofs and all declared fields before submission; the direct API accepts the following optional fields in addition to Level A: por_document, sof_document (images), birth_date (YYYY-MM-DD), nationality, address, occupation, pep (true/false), funds_source, annual_income.
- The residence and funds documents are optional here; when left out,
proof_of_residenceandsource_of_fundsare null. - Cross-checks only run when the data is there:
birth_date_matches_idneeds a birth date and a readable ID birth date;address_matches_porneeds an address and a readable proof of residence. - Occupation, PEP status, source of funds and income are recorded as declared and are not verified. There is no sanctions or PEP screening.
curl -X POST https://api.firstpass.cloud/v1/kyc/b \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d @kyc_b.json
# kyc_b.json: the Level A fields plus "birth_date": "1990-01-01", "address": "Tokyo",
# "occupation": "engineer", "pep": false, "funds_source": "salary", "annual_income": "3m_6m"
{
"level": "B",
"identity": { ...same shape as Level A..., "passed": true },
"face_match": { "faces_a": 0, "faces_b": 0, "similarity": null, "verdict": "no_face" },
"document": { "ocr_unavailable": true },
"face_embedding": null,
"proof_of_residence": null,
"source_of_funds": null,
"edd": {
"birth_date": "1990-01-01",
"nationality": null,
"address": "Tokyo",
"declared": { "occupation": "engineer", "pep": false, "funds_source": "salary", "annual_income": "3m_6m" },
"cross_checks": {}
}
}
When read, proof_of_residence is {doc_kind, name, address, issue_date, issuer, recent} (recent is true only for issue dates from today minus 90 days through today, inclusive, by JST calendar day; future dates return recent: false and an additional reason: "por_future_date") and source_of_funds is {doc_kind, name, issuer, amount, period}.
Granular endpoints
Single steps for composing your own flow. 1 credit each. None of them runs liveness.
Face compare
Body: image_a (selfie), image_b (ID photo). Stored as a record.
curl -X POST https://api.firstpass.cloud/v1/face/compare \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"image_a": "BASE64_JPEG", "image_b": "BASE64_JPEG"}'
{"faces_a": 0, "faces_b": 0, "similarity": null, "verdict": "no_face"}
Face fingerprint
Body: image. Returns the face count and the largest face's 128-number embedding (or null). Not stored.
curl -X POST https://api.firstpass.cloud/v1/face/embed \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"image": "BASE64_JPEG"}'
{"faces": 0, "embedding": null}
ID document reading
Body: image. Returns {doc_type, doc_number, name, birth_date, expiry_date, expiry_printed}. Stored as a record. An image that is not a readable ID returns 422; a reader outage returns 502. Neither is charged.
curl -X POST https://api.firstpass.cloud/v1/document/ocr \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"image": "BASE64_JPEG"}'
{"detail": "ocr unavailable. not charged, retry later"}
Proof-of-residence reading
Body: image. Returns {doc_kind, name, address, issue_date, issuer, recent}. Not stored. A null OCR result returns 422 and a reader outage returns 502, both refunded. Unknown, unreadable or unrecognized document kinds also return 422, refund the charge and record usage with ok=0. A readable but future-dated proof still costs 1 credit and returns recent: false with reason: "por_future_date".
curl -X POST https://api.firstpass.cloud/v1/document/por \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"image": "BASE64_JPEG"}'
{"detail": "ocr unavailable. not charged, retry later"}
Name, email and phone check
Body: name, email, phone, region (default "JP"). Checks the name, the email format, mail server (MX) and disposable domains, and parses the phone number. Charged whenever it returns 200. Not stored.
curl -X POST https://api.firstpass.cloud/v1/identity/basic \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Test Applicant", "email": "applicant@example.com", "phone": "090-1234-5678", "region": "JP"}'
{
"level": "A",
"name": { "input": "Test Applicant", "valid": true },
"email": { "input": "applicant@example.com", "valid": true, "mx_found": true, "disposable": false },
"phone": { "input": "090-1234-5678", "valid": true, "e164": "+819012345678", "country": "JP", "type": "mobile" },
"passed": true
}
Credits
1 credit = $0.10, prepaid. Credits are taken before the check runs. Direct A/B calls are billed per call, including no-face or unreadable-ID results unless OCR is unavailable. Their engine exceptions, OCR outages and handled record-save failures are refunded, but errors in image preparation or final settlement are not all covered: an arbitrary 500 does not guarantee a refund. Hosted submissions refund their new charge on processing or storage failure and release the charge claim; after a refund the next billable submission is charged again. A free retry has no new charge to refund. Unreadable POR within direct or hosted Level B still costs the overall 2 credits; hosted B goes to review/documents_unreadable. A future-dated POR goes to review/edd_review in hosted B, without an automatic rejection. Direct B returns the fields for the customer to decide.
| Call | Credits | Not charged when |
|---|---|---|
POST /v1/kyc/session | 0 | Creating is free; needs a positive balance. |
| Hosted session, Level A | 1 per session | Retries while the session remains charged; document reader outage. |
| Hosted session, Level B | 2 per session | Retries while the session remains charged; any document reader outage. |
GET /v1/kyc/session/{id} | 0 | Always free. |
POST /v1/kyc/a | 1 | Document reader outage; bad image; the record could not be saved. |
POST /v1/kyc/b | 2 | Any document reader outage; bad image; the record could not be saved. |
POST /v1/face/compare | 1 | No face in either image; bad image. |
POST /v1/face/embed | 1 | No face found; bad image. |
POST /v1/document/ocr | 1 | Not a readable ID (422); reader outage (502). |
POST /v1/document/por | 1 | Null, unknown, unreadable or unrecognized POR (422); reader outage (502). |
POST /v1/identity/basic | 1 | Charged whenever it returns 200. |
Errors
HTTP and model-validation errors use {"detail": ...}. Model-validation 422 responses contain a list; business 422 responses contain a string or object. Unhandled 500 errors do not guarantee JSON or a refund.
| Status | When | detail |
|---|---|---|
| 400 | An image cannot be decoded. | bad image |
| 401 | No bearer token, or an unknown or disabled key. | missing bearer token, invalid api key |
| 402 | Zero balance or insufficient credits for this call. | {"detail":"no credits left. top up your balance (hello@firstpass.cloud)"} or {"detail":"not enough credits. top up your balance (dashboard or hello@firstpass.cloud)"} |
| 403 | Hosted submission: workspace disabled. | {"detail":"tenant inactive"} |
| 404 | Session unknown or belongs to another workspace. | {"detail":"session not found"} |
| 409 | Already verified, or closed after erasure. | {"detail":"session already verified"} or {"detail":"this verification was closed. ask for a new link"}. Help after erasure: {"detail":"this session's data has been erased"}. |
| 410 | Hosted config or submit: link expired. | {"detail":"this verification link has expired. ask for a new one"} |
| 422 | Request model validation: body, field or content-type problems. | {"detail":[{"type":"missing","loc":["body","image"],"msg":"Field required","input":{}}]} (illustrative; fields vary). |
| 422 | Business error: unreadable ID or unreadable/unrecognized POR result, refunded. | {"detail":"not a readable document. not charged"}. ID OCR: {"detail":"not a readable identity document. not charged"}. |
| 422 | Hosted ID photo-side precheck: no face detected, not charged or stored; retake the photo side. | {"detail":{"reason":"document_back_side","message":"No face was found on the ID image. Photograph the FRONT (photo side) of your ID and submit again. Not charged."}} |
| 429 | Monthly quota, session attempts or session creation threshold. | detail is a string: monthly quota exceeded, monthly quota exceeded for this service. try again later, too many attempts on this link. request a new verification link, or session limit reached (300/hour). retry later. A new link does not fix monthly quota exhaustion. |
| 500 | The check ran but its record could not be saved. Not charged; retry. | could not save the check result. not charged, please retry |
| 502 | Document reader outage. Not charged; retry later. | ocr unavailable. not charged, retry later |
What is stored
Stored records are encrypted, visible only in your own dashboard (and to our authorised operations staff), and never matched across customers. Except for Level B proof images (90 days), they have no automatic deletion period, and storage cannot be switched off. Our operations staff erase a record on request: email hello@firstpass.cloud. Details in the privacy policy.
| Call | Record | What it keeps |
|---|---|---|
| Hosted session, A or B | Yes, one per session | Details entered, results, downscaled selfie and ID images, live-scan frames, screen-flash frames, scan video, face fingerprint; Level B document fields and declared data. A retry replaces the previous attempt. Residence and funds images are encrypted in a separate table for 90 days, accessible only to authorised FirstPass staff reviewing records through the logged staff page, not to customers. |
/v1/kyc/a, /v1/kyc/b | Yes | Name, email, phone, results, document fields, downscaled selfie and ID images; Level B document fields and declared data. The returned face fingerprint is not kept. Residence and funds images are encrypted separately for 90 days (one that may show a My Number is not stored); only authorised FirstPass staff reviewing records can access them through the logged staff page. |
/v1/face/compare | Yes | Verdict, similarity, downscaled images of both photos. |
/v1/document/ocr | Yes | Document fields and a downscaled document image. |
/v1/face/embed | No | Usage log only (endpoint, time, billed or not). |
/v1/document/por | No | Usage log only. |
/v1/identity/basic | No | Usage log only. |
Health
Public, no key. 200 when the service can reach its database and load the face model, 503 with "ok": false when it cannot. ocr says whether document reading is configured.
curl https://api.firstpass.cloud/health
{"ok": true, "db": "ok", "ocr": "missing", "face_model": "ok"}