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": "[email protected]",
"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": "[email protected]",
"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
[email protected].
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 |
|---|---|
[email protected] | Age 25. Passes a target of 18 or 21. |
[email protected] | Age 19. Passes 18, fails 21. |
[email protected] | Age 16. Fails 18 and above. |
[email protected] | 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"; [email protected]
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