Sending a verification (UI + API)
In brief: Step-by-step for both surfaces: age and identity checks, idempotency, the outcome shape.
A "verification" in Certivus Verify is a single check on a single person. Two flavours:
- Age verification: "is this person at least N years old?"
- Identity verification: "is this person who they say they are?"
Both have a dashboard form and an API endpoint. The result is the same regardless of which surface you use.
Sending from the dashboard
/verify-app/checks → Send verification button.
- Pick the check type: Age or Identity.
- Fill in the subject details:
- Email (required)
- First + last name
- DOB (required for age, optional for identity)
- Phone (optional, used as a Pass-reuse hint)
- For age checks: pick the age threshold (16, 18, 21, 25 by default; for anything else, send via API).
- For identity checks: pick the assurance level (standard, high; this corresponds to the GPG 45 profile).
- Click Send.
The check is created in pending state. The subject gets an email with a one-click verification link. You can watch the row in the checks list; it'll flip to processing when they start, then completed (or failed) when they finish.
If the subject has an existing Certivus Pass, the dashboard offers a Try Pass reuse first toggle (default on). See Pass interop.
Sending via API
The base URL
https://ldhxrzqwbvwmmpbawagq.supabase.co/functions/v1/verify-api
That is the host that answers today. A vanity host is not live yet:
api.certivus.com does not resolve, and app.certivus.com/v2/...
serves the dashboard's HTML rather than the API. Earlier versions of
this page and of the console pointed at both, and neither works.
Check it before you build anything else:
curl https://ldhxrzqwbvwmmpbawagq.supabase.co/functions/v1/verify-api/v2/health
# {"status":"ok","api":"verify","version":"v2"}
Age verification
POST /v2/verify/age
Authorization: Bearer sk_sandbox_...
Content-Type: application/json
Idempotency-Key: cart-abc-123-attempt-1
{
"method": "selfie",
"target_age": 18,
"test_subject": "pass.adult@certivus.test",
"redirect_url": "https://your-app.example.com/verified",
"metadata": { "reference": "cart-abc-123" }
}
Identity verification
POST /v2/verify/identity
Authorization: Bearer sk_sandbox_...
Content-Type: application/json
Idempotency-Key: signup-xyz-456
{
"gpg45_target_tier": "medium",
"test_subject": "pass.standard@certivus.test",
"redirect_url": "https://your-app.example.com/verified",
"metadata": { "reference": "signup-xyz-456" }
}
Response
Sandbox settles immediately — there is no link to forward, because the outcome is already decided:
{
"id": "8f2b1c40-0000-0000-0000-000000000000",
"mode": "sandbox",
"status": "completed",
"check_type": "age",
"result": {
"passed": true,
"estimated_age": 25,
"confidence": 0.93,
"method_used": "selfie",
"target_age": 18
},
"pass_offered": false
}
Live identity returns status: "pending" plus a hosted session for the
subject to complete:
{
"id": "8f2b1c40-0000-0000-0000-000000000000",
"mode": "live",
"status": "pending",
"check_type": "identity",
"result": null,
"pass_offered": false,
"hosted_session_url": "https://...",
"external_session_id": "..."
}
If the subject already holds a Certivus Pass you get pass_offered: true
with a pass_consent_url and expires_in_seconds instead of a hosted
session — send them there and we settle from the existing Pass at the
reuse fee. Send force_full_check: true to skip that lookup. See
Pass interop.
Live age verification is not available yet — it returns
501 LIVE_NOT_IMPLEMENTED. Use a sandbox key, or contact
sales@certivus.com.
Field reference
POST /v2/verify/age
| Field | Required | Notes |
|---|---|---|
method | yes | selfie, document or combined. |
target_age | yes | Integer, 13–99. |
test_subject | sandbox | Picks the canned outcome — see the sandbox table below. |
redirect_url | no | Where to send the subject after they finish. |
metadata | no | Any JSON object. Put your own reference here. |
POST /v2/verify/identity
| Field | Required | Notes |
|---|---|---|
gpg45_target_tier | yes | low, medium, high or very_high. |
profile_id | no | A specific provider profile, if you have been given one. |
test_subject | sandbox | Picks the canned outcome. |
redirect_url | no | Where to send the subject after they finish. |
metadata | no | Any JSON object. |
force_full_check | no | true skips the Pass-reuse lookup on live identity. |
There is no subject object, no age_threshold, no assurance_level,
no reference and no notify field. Earlier versions of this page
documented all five. None has ever been accepted — sending them gets you
400 VALIDATION_ERROR. Put your own reference in metadata.
The verification lifecycle
pending → processing → completed
↘
failed
| Status | What it means |
|---|---|
pending | Created. Subject hasn't started yet. |
processing | Subject opened the link, in progress. |
completed | Subject finished. Check outcome for pass/fail. |
failed | Provider error, expired link, or subject declined. Check failure_reason. |
Pending verifications expire after 7 days. After expiry, the link
returns a "this link has expired" page and the verification flips to
failed with failure_reason = 'expired'.
The outcome shape
Once status is completed, result carries the outcome. It is a
different shape per check type, and it sits at the top level — there is
no outcome wrapper, no subject echo and no reference field. Put
your own reference in metadata.
Age:
{
"passed": true,
"estimated_age": 25,
"confidence": 0.93,
"method_used": "selfie",
"target_age": 18
}
| Field | What it means |
|---|---|
passed | estimated_age >= target_age. |
estimated_age | Years. |
confidence | 0–1. Higher for stronger methods and a wider margin. |
method_used | Echoes the method you sent. |
target_age | Echoes the target_age you sent. |
Identity:
{
"passed": true,
"gpg45_tier": "medium",
"gpg45_score": {
"part_a": 2, "part_b": 2, "part_c": 2, "part_d": 2, "part_e": 2
}
}
gpg45_tier is low, medium, high, very_high — or failed.
A check that fails at the provider returns status: "failed" with
result: null.
Sandbox: deterministic results
In sandbox the outcome is decided by test_subject, not by the
subject's real email — sandbox never contacts a provider or a person.
test_subject | Result |
|---|---|
pass.adult@certivus.test | Age 25. Passes a target of 18 or 21. |
pass.young.adult@certivus.test | Age 19. Passes 18, fails 21. |
fail.minor@certivus.test | Age 16. Fails 18 and above. |
error.timeout@certivus.test | Simulated provider timeout — status: "failed", result: null. |
| anything else | Deterministic age 18–65, hashed from the string. |
| omitted | Age 30. |
For identity, any test_subject starting fail. returns
passed: false with gpg45_tier: "failed"; error.timeout@certivus.test
fails the check; anything else passes at the tier you requested.
Use these to build every code path before going live.
Idempotency
Always send an idempotency key — either as the Idempotency-Key
header (preferred) or as idempotency_key in the body. If you retry
the same request (network blip, your retry queue, whatever), we'll
return the same verification object you got the first time: no
duplicate check created, no duplicate bill.
The replay carries the original request's correlation id and an
Idempotent-Replay: true header, so you can tell which of two
identical responses did the work.
Two cases return 409 rather than a replay:
| Code | When | What to do |
|---|---|---|
IDEMPOTENCY_KEY_REUSED | Same key, different request body | Use a new key. We will never answer one request with another's result. |
IDEMPOTENT_REQUEST_IN_PROGRESS | The first request is still running | Retry in a few seconds with the same key. |
Keys are scoped to your account. Max length 128 chars. We keep them for 24 hours.
What to do next
- Wire a webhook so you get pushed when the check finishes: Webhooks.
- Try the Pass reuse path for sub-second results on returning Pass holders: Pass interop.
- Understand the billing before going live: Billing & monthly invoicing.
Didn't find what you needed?
Contact support