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/checks → Send 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": "[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

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
[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 elseDeterministic age 18–65, hashed from the string.
omittedAge 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:

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