Integration guide

ClearConsent confirms that the person in front of the camera is the person on the ID. Your user records a short video selfie and takes a photo holding their government-issued ID beside their face; a vision model compares the two and returns a decision in seconds. The service is free: the model calls behind each check run on your own FreeRouter API key, so the only cost is inference at your provider's rate. This guide covers accounts and projects, the hosted page, direct submission, the result payload, status lookup, and signed webhooks.

Getting access

Everything is self-serve. There are two keys involved: your FreeRouter key pays for the model calls, and your ClearConsent key identifies a project (its theming and webhook).

  1. Sign up at freerouter.com and connect the provider or gateway you want the vision-model calls billed to. FreeRouter is free to use; your provider bills you for inference at their normal rate. Make sure the routing can serve a vision-capable model.
  2. Create a ClearConsent account at id.clearconsent.me/app/signup. Sign-in is by email and a one-time code; there is no password.
  3. Create a project. A project is one integration: a website, an app, an internal tool. Paste your FreeRouter key, optionally a webhook URL, and you get the project's ClearConsent key, shown once.
Why FreeRouter? ClearConsent talks to the model through FreeRouter's OpenAI-compatible endpoint. Routing the calls through a key you own means you choose the provider, you see the spend on a bill you already have, and there is no ClearConsent line item at any volume. FreeRouter is also made by Multify Inc.

Quickstart

The integration on your side is a redirect and a webhook handler. Everything between them runs on ours.

1. Redirect the user to the hosted page

Build the URL on your server and redirect the user to it. Your ClearConsent key is a query parameter, so build this URL server-side and never in the browser.

// Node / Express
app.get('/verify', (req, res) => {
  const url = new URL('https://id.clearconsent.me/verif/');
  url.searchParams.set('api_key', process.env.CLEARCONSENT_KEY);
  url.searchParams.set('user_identifier', req.user.id);
  url.searchParams.set('user_email', req.user.email);
  res.redirect(url.toString());
});

2. Receive the result on your webhook

When the check completes we POST the result to the project's webhook URL, signed with the project's signing secret. Respond with any 2xx within ten seconds. Deliveries are retried, so store check_id with the result and ignore a later delivery for the same check.

const crypto = require('crypto');
const SECRET = process.env.CLEARCONSENT_WEBHOOK_SECRET;

app.post('/webhooks/clearconsent', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = Object.fromEntries((req.get('X-ClearConsent-Signature') || '').split(',').map((kv) => kv.split('=')));
  const expected = crypto.createHmac('sha256', SECRET).update(`${sig.t}.${req.body}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
  if (!fresh || !sig.v1 || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1))) {
    return res.status(401).end();
  }

  const body = JSON.parse(req.body);
  if (await alreadyProcessed(body.check_id)) return res.status(200).json({ received: true });

  if (body.event === 'check.verified') {
    const level = body.verification_result.verificationAssessment.confidenceLevel;
    if (level === 'high') await markVerified(body.user_identifier, body.check_id);
    else await flagForReview(body.user_identifier, body.check_id, level);
  } else if (body.event === 'check.rejected') {
    await markRejected(body.user_identifier, body.check_id);
  } else if (body.event === 'check.error') {
    await offerRetry(body.user_identifier, body.check_id);
  }
  res.status(200).json({ received: true });
});

That is the whole integration. The sections below are the reference for each part.

API reference

Base URL: https://id.clearconsent.me. All endpoints are served over HTTPS.

Authentication

Every request carries a project's ClearConsent key as the api_key parameter: in the query string for the hosted page and the status lookup, and as a form field for direct submission. The key selects the project, and with it the FreeRouter key, theming, and webhook. Keep it on your server; the hosted-page URL should be assembled server-side and handed to the browser as a redirect. Rotate it from the project page at any time; the old key stops working immediately.

Hosted verification page

GET/verif/Serves the themed verification page
ParameterTypeRequiredDescription
api_keystringYesThe project's ClearConsent key.
user_identifierstringYesYour identifier for the person being verified. Returned unchanged in the result and the webhook. user_id is accepted as an alias.
user_emailstringNoStored with the check for your records.
user_phonestringNoStored with the check for your records.

The page walks the user through two capture steps and a processing step. Step one records a short video selfie (the page enforces a minimum of three seconds). Step two captures a photo of the user holding their ID beside their face, with a file-upload fallback for devices without a usable camera. The page then submits both files, shows the outcome to the user, and the result is delivered to your webhook.

Theming is per project: your logo, header and button colours, the step-one instruction text, and the success message. The logo URL is editable on the project page; for the other values, email info@clearconsent.me with your project id until the theme editor ships.

Direct submission

POST/verif/submitSubmit the two files from your own UI

If you prefer to own the capture experience, collect the video selfie and the ID photo yourself and post them as multipart/form-data. The response shape is the same one the hosted page receives, and the webhook fires the same way.

FieldTypeRequiredDescription
api_keystringYesThe project's ClearConsent key.
user_identifierstringYesYour identifier for the person.
selfie_videofileYesVideo selfie, any common format, up to 50MB.
id_photofileYesPhoto of the person holding their government-issued ID, JPG or PNG.
user_emailstringNoStored with the check.
user_phonestringNoStored with the check.
curl -X POST https://id.clearconsent.me/verif/submit \
  -F "api_key=$CLEARCONSENT_KEY" \
  -F "user_identifier=usr_3921" \
  -F "selfie_video=@selfie.webm" \
  -F "id_photo=@id-selfie.jpg"

Response payload

The submit response and the webhook carry the same result object. The example below is a successful check.

{
  "success": true,
  "check_id": "7e1c2f0a-…-a4b9",
  "result": {
    "sessionId": "7e1c2f0a-…-a4b9",
    "timestamp": "2026-09-26T14:02:11.000Z",
    "status": "verified",
    "faceComparison": {
      "isIdentical": true,
      "confidence": 0.85,
      "similarityScore": 85,
      "concerns": []
    },
    "verificationAssessment": {
      "overallScore": 85,
      "verificationStatus": "verified",
      "confidenceLevel": "high",
      "riskFactors": [],
      "recommendations": [],
      "summary": "Face comparison verified at 85% confidence (high)"
    },
    "model": { "name": "openai/gpt-5", "keySource": "project" },
    "files": {
      "selfieVideo": { "url": "https://…", "fileName": "…" },
      "idPhoto":     { "url": "https://…", "fileName": "…" }
    }
  }
}
FieldDescription
check_idUUID for this check. Use it as your idempotency key and quote it when contacting support.
result.statusverified or rejected.
result.faceComparison.confidence0 to 1. How confident the model is that the two faces are the same person.
result.faceComparison.similarityScore0 to 100. The same signal on a percentage scale.
result.verificationAssessment.overallScore0 to 100, derived from the confidence. 70 and above is verified.
result.verificationAssessment.confidenceLevelhigh (80+), medium (60 to 79), or low. See the statuses section for how to act on it.
result.modelWhich model ran and whether it used the project's own FreeRouter key (project) or, for legacy partners, the ClearConsent default (global).
result.filesSigned URLs for the uploaded files. They stop working when the files are deleted after seven days.

Statuses and confidence

Every check resolves to one of the rows below. The thresholds are yours to set; these are sensible defaults.

Status · confidenceMeaningTypical handling
verified · highThe face in the video matches the face on the ID with high confidence.Approve automatically.
verified · mediumThe faces match, with less margin.Approve, or flag for a person to look at, depending on what the check protects.
rejected · anyThe faces do not match, or the capture was unusable. At low confidence there was too little signal to decide.Decline, or offer a retry with clearer guidance. Treat low as "could not tell" rather than "definitely not".
errorProcessing failed before a decision (bad file, model unavailable, provider error). Delivered to the webhook as check.error.Offer a retry. If it repeats, check your FreeRouter routing serves the vision model.

Status lookup

GET/verif/status/:check_idLook up one check by id

Pass the project's key as api_key in the query string or as an X-Api-Key header. Only checks belonging to that project are returned.

curl "https://id.clearconsent.me/verif/status/7e1c2f0a-…-a4b9?api_key=$CLEARCONSENT_KEY"

{
  "success": true,
  "check_id": "7e1c2f0a-…-a4b9",
  "user_identifier": "usr_3921",
  "status": "verified",
  "score": 85,
  "confidence_level": "high",
  "created_at": "2026-09-26T14:01:40.000Z",
  "completed_at": "2026-09-26T14:02:11.000Z",
  "webhook": { "attempted_at": "2026-09-26T14:02:12.000Z", "success": true }
}

Webhooks

When a check completes we POST to the project's webhook URL. One URL receives every event; the event field says which.

EventWhen
check.verifiedThe decision is verified.
check.rejectedThe decision is rejected.
check.errorProcessing failed; the payload carries status: "error" and no result.
testSent from the project page's "Send test event" button. test: true in the payload.
POST https://yourapp.example/webhooks/clearconsent
Content-Type: application/json
User-Agent: IdentityVerification/1.0
X-ClearConsent-Event: check.verified
X-ClearConsent-Delivery: 0b1a…-…-…
X-ClearConsent-Timestamp: 1790000000
X-ClearConsent-Signature: t=1790000000,v1=5f1d…

{
  "event": "check.verified",
  "delivery_id": "0b1a…-…-…",
  "check_id": "7e1c2f0a-…-a4b9",
  "user_identifier": "usr_3921",
  "status": "verified",
  "verification_result": { … the result object from the response above … },
  "selfie_video_url": "https://…",
  "id_photo_url": "https://…",
  "completed_at": "2026-09-26T14:02:11.000Z"
}

Verifying the signature

Each project has a signing secret, shown on its page in the dashboard. The signature is an HMAC-SHA256 over the string <timestamp>.<raw body> using that secret, hex-encoded, in the v1 part of the header. Compute it over the exact bytes you received, before any JSON parsing, and compare with a constant-time function. Reject timestamps older than five minutes to defeat replays. The quickstart above has a complete Node example; the same algorithm works in any language.

Delivery and retries

File requirements

FileAcceptedGuidance
Video selfieAny common video format, up to 50MBAt least three seconds, face clearly visible, looking at the camera. The hosted page records WebM.
ID photoJPG or PNGThe person holding their government-issued photo ID beside their face, both in focus and well lit, with no glare over the ID photo.

Errors

StatusMessageWhat to do
400API key is requiredInclude api_key.
400User identifier is requiredInclude user_identifier (or user_id).
401Invalid API keyCheck the key against the project page; a rotated key stops working immediately.
400Both selfie video and ID photo are requiredSend both files in the multipart body.
404Check not foundThe check id does not exist or belongs to another project.
503This project has no FreeRouter key configuredAdd a FreeRouter key on the project page.
500Verification processing failedCheck file sizes and formats, then retry. The check shows as error in the dashboard with the reason.

Data handling

Go-live checklist

Support

Email info@clearconsent.me. Include the check_id and, for webhook problems, the delivery id from the check's page in the dashboard. A person reads the inbox.

Keep keys server-side. Never expose your ClearConsent key or your FreeRouter key in client-side code. Build the hosted-page URL on your backend and hand the browser a redirect.