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).
- 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.
- Create a ClearConsent account at id.clearconsent.me/app/signup. Sign-in is by email and a one-time code; there is no password.
- 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.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
api_key | string | Yes | The project's ClearConsent key. |
user_identifier | string | Yes | Your identifier for the person being verified. Returned unchanged in the result and the webhook. user_id is accepted as an alias. |
user_email | string | No | Stored with the check for your records. |
user_phone | string | No | Stored 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
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.
| Field | Type | Required | Description |
|---|---|---|---|
api_key | string | Yes | The project's ClearConsent key. |
user_identifier | string | Yes | Your identifier for the person. |
selfie_video | file | Yes | Video selfie, any common format, up to 50MB. |
id_photo | file | Yes | Photo of the person holding their government-issued ID, JPG or PNG. |
user_email | string | No | Stored with the check. |
user_phone | string | No | Stored 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": "…" }
}
}
}
| Field | Description |
|---|---|
check_id | UUID for this check. Use it as your idempotency key and quote it when contacting support. |
result.status | verified or rejected. |
result.faceComparison.confidence | 0 to 1. How confident the model is that the two faces are the same person. |
result.faceComparison.similarityScore | 0 to 100. The same signal on a percentage scale. |
result.verificationAssessment.overallScore | 0 to 100, derived from the confidence. 70 and above is verified. |
result.verificationAssessment.confidenceLevel | high (80+), medium (60 to 79), or low. See the statuses section for how to act on it. |
result.model | Which model ran and whether it used the project's own FreeRouter key (project) or, for legacy partners, the ClearConsent default (global). |
result.files | Signed 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 · confidence | Meaning | Typical handling |
|---|---|---|
verified · high | The face in the video matches the face on the ID with high confidence. | Approve automatically. |
verified · medium | The faces match, with less margin. | Approve, or flag for a person to look at, depending on what the check protects. |
rejected · any | The 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". |
error | Processing 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
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.
| Event | When |
|---|---|
check.verified | The decision is verified. |
check.rejected | The decision is rejected. |
check.error | Processing failed; the payload carries status: "error" and no result. |
test | Sent 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
- Respond with any status in the range 200 to 299 within ten seconds. Anything else, or a timeout, counts as a failed attempt.
- A failed attempt is retried after about one minute, then ten minutes, then one hour: four attempts in total. After that the delivery is marked as given up and shows as such on the check's page in the dashboard.
- Because of retries, the same event can arrive more than once. Treat
check_id(ordelivery_id) as an idempotency key and make your handler safe to run twice. - Deliveries are sent over HTTPS only. The URL must start with
https://; the dashboard refuses anything else. - Do heavy work after you have responded. Acknowledge first, then process.
- Use the "Send test event" button on the project page to confirm your endpoint and signature check before going live.
File requirements
| File | Accepted | Guidance |
|---|---|---|
| Video selfie | Any common video format, up to 50MB | At least three seconds, face clearly visible, looking at the camera. The hosted page records WebM. |
| ID photo | JPG or PNG | The person holding their government-issued photo ID beside their face, both in focus and well lit, with no glare over the ID photo. |
Errors
| Status | Message | What to do |
|---|---|---|
400 | API key is required | Include api_key. |
400 | User identifier is required | Include user_identifier (or user_id). |
401 | Invalid API key | Check the key against the project page; a rotated key stops working immediately. |
400 | Both selfie video and ID photo are required | Send both files in the multipart body. |
404 | Check not found | The check id does not exist or belongs to another project. |
503 | This project has no FreeRouter key configured | Add a FreeRouter key on the project page. |
500 | Verification processing failed | Check file sizes and formats, then retry. The check shows as error in the dashboard with the reason. |
Data handling
- Selfie videos and ID photos are deleted automatically seven days after upload. The signed file URLs in the result stop working at that point, so copy anything you need to keep before then.
- The decision, scores, and
check_idare retained so that results can be looked up in the dashboard and by the status endpoint. - Your FreeRouter key is encrypted at rest and decrypted only for the duration of a check's model calls. The dashboard shows only its last four characters.
- The model calls behind each check run through your FreeRouter key to the provider you connected. Review that provider's data handling terms as you would for any other inference traffic.
- The service is for identity verification and fraud prevention only. It must not be used for unrelated profiling, surveillance, or decision-making, and the terms of use say so.
Go-live checklist
- FreeRouter key created and a provider with a vision-capable model connected at freerouter.com
- ClearConsent project created with that key; webhook URL saved and a test event delivered
- Hosted-page URL built server-side; the ClearConsent key never appears in client code
- Webhook handler verifies the signature, responds 2xx within ten seconds, and is idempotent on
check_id - Handling defined for
check.verifiedat high and medium confidence,check.rejected, andcheck.error - Results stored on your side, since file URLs expire after seven days
- Privacy policy updated to mention identity verification and the retention period
- End-to-end test completed on a phone and on a laptop
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.