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

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

POST/v1/kyc/session
FieldTypeNotes
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_refstring, optionalYour own id for the person, up to 120 characters. Returned when you poll.
lang"en", "zh" or "ja", optionalLanguage of the scan page: a valid URL language takes priority, then the browser’s saved choice, then English.
return_urlstring, optionalStored 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"}'
200
{
  "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

GET/v1/kyc/session/{session_id}

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"
200, before the applicant finishes
{
  "session_id": "_2L_jByICyPD_528Fk6JB7Y_y6SwJAu3",
  "status": "pending",
  "level": "A",
  "verdict": null,
  "record_id": null,
  "applicant_ref": "user_123"
}
200, after the applicant finishes
# 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 }] }
  }
}

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.

reasonoutcomeMeaning
okverifiedAll checks passed.
no_face_selfie, no_face_documentrejectedNo face found in the scan or on the ID photo.
document_back_sidereviewNo 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_documentrejectedNot one of the accepted ID types.
expired_documentrejectedThe ID's expiry date has passed.
face_mismatchrejectedThe face does not match the ID photo.
missing_documentsrejectedLevel B without both the residence and funds documents.
uncertainreviewFace similarity in the uncertain band.
expiry_unreadablereviewAccepted ID type, but the expiry date could not be read.
identity_reviewreviewName, email or phone did not validate.
flash_reviewreviewThe screen-flash reflection pattern was wrong.
flash_sequence_reviewreviewThe screen-flash colours reflected clearly, but not in the order this session issued.
liveness_reviewreviewNo usable screen-flash reading (skipped, too bright, older page).
documents_unreadablereviewLevel B residence or funds document could not be read.
edd_reviewreviewLevel 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_unavailablereviewOur 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

POST/v1/kyc/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"}
200 (synthetic images: no face found, document reader not configured)
{
  "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

POST/v1/kyc/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.

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"
200 (same synthetic images; no residence or funds documents sent)
{
  "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

POST/v1/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"}'
200 (synthetic images)
{"faces_a": 0, "faces_b": 0, "similarity": null, "verdict": "no_face"}

Face fingerprint

POST/v1/face/embed

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"}'
200 (synthetic image)
{"faces": 0, "embedding": null}

ID document reading

POST/v1/document/ocr

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"}'
502 (document reader not configured on the test server)
{"detail": "ocr unavailable. not charged, retry later"}

Proof-of-residence reading

POST/v1/document/por

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"}'
502 (document reader not configured on the test server)
{"detail": "ocr unavailable. not charged, retry later"}

Name, email and phone check

POST/v1/identity/basic

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"}'
200
{
  "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.

CallCreditsNot charged when
POST /v1/kyc/session0Creating is free; needs a positive balance.
Hosted session, Level A1 per sessionRetries while the session remains charged; document reader outage.
Hosted session, Level B2 per sessionRetries while the session remains charged; any document reader outage.
GET /v1/kyc/session/{id}0Always free.
POST /v1/kyc/a1Document reader outage; bad image; the record could not be saved.
POST /v1/kyc/b2Any document reader outage; bad image; the record could not be saved.
POST /v1/face/compare1No face in either image; bad image.
POST /v1/face/embed1No face found; bad image.
POST /v1/document/ocr1Not a readable ID (422); reader outage (502).
POST /v1/document/por1Null, unknown, unreadable or unrecognized POR (422); reader outage (502).
POST /v1/identity/basic1Charged 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.

StatusWhendetail
400An image cannot be decoded.bad image
401No bearer token, or an unknown or disabled key.missing bearer token, invalid api key
402Zero 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)"}
403Hosted submission: workspace disabled.{"detail":"tenant inactive"}
404Session unknown or belongs to another workspace.{"detail":"session not found"}
409Already 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"}.
410Hosted config or submit: link expired.{"detail":"this verification link has expired. ask for a new one"}
422Request model validation: body, field or content-type problems.{"detail":[{"type":"missing","loc":["body","image"],"msg":"Field required","input":{}}]} (illustrative; fields vary).
422Business 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"}.
422Hosted 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."}}
429Monthly 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.
500The check ran but its record could not be saved. Not charged; retry.could not save the check result. not charged, please retry
502Document 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.

CallRecordWhat it keeps
Hosted session, A or BYes, one per sessionDetails 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/bYesName, 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/compareYesVerdict, similarity, downscaled images of both photos.
/v1/document/ocrYesDocument fields and a downscaled document image.
/v1/face/embedNoUsage log only (endpoint, time, billed or not).
/v1/document/porNoUsage log only.
/v1/identity/basicNoUsage log only.

Health

GET/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
200 (local server, document reader not configured)
{"ok": true, "db": "ok", "ocr": "missing", "face_model": "ok"}