Help Centre/Sending a verification (UI + API)

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/checksSend verification button.

  1. Pick the check type: Age or Identity.
  2. 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)
  3. For age checks: pick the age threshold (16, 18, 21, 25 by default; for anything else, send via API).
  4. For identity checks: pick the assurance level (standard, high; this corresponds to the GPG 45 profile).
  5. 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

FieldRequiredNotes
methodyesselfie, document or combined.
target_ageyesInteger, 13–99.
test_subjectsandboxPicks the canned outcome — see the sandbox table below.
redirect_urlnoWhere to send the subject after they finish.
metadatanoAny JSON object. Put your own reference here.

POST /v2/verify/identity

FieldRequiredNotes
gpg45_target_tieryeslow, medium, high or very_high.
profile_idnoA specific provider profile, if you have been given one.
test_subjectsandboxPicks the canned outcome.
redirect_urlnoWhere to send the subject after they finish.
metadatanoAny JSON object.
force_full_checknotrue 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
StatusWhat it means
pendingCreated. Subject hasn't started yet.
processingSubject opened the link, in progress.
completedSubject finished. Check outcome for pass/fail.
failedProvider 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
}
FieldWhat it means
passedestimated_age >= target_age.
estimated_ageYears.
confidence0–1. Higher for stronger methods and a wider margin.
method_usedEchoes the method you sent.
target_ageEchoes 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_subjectResult
pass.adult@certivus.testAge 25. Passes a target of 18 or 21.
pass.young.adult@certivus.testAge 19. Passes 18, fails 21.
fail.minor@certivus.testAge 16. Fails 18 and above.
error.timeout@certivus.testSimulated provider timeout — status: "failed", result: null.
anything elseDeterministic age 18–65, hashed from the string.
omittedAge 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:

CodeWhenWhat to do
IDEMPOTENCY_KEY_REUSEDSame key, different request bodyUse a new key. We will never answer one request with another's result.
IDEMPOTENT_REQUEST_IN_PROGRESSThe first request is still runningRetry 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