Create
Your backend calls POST /api/v1/verifications with your API key and chosen liveness_tier, and receives a verification_id and a one-time hosted url.
Soliton Identity Verification
Your server creates a verification, your user completes it on our hosted page, your server fetches the verdict. You never handle video, biometrics, or capture UI.
01
Base URL https://api.soliton.id. Every server-to-server call carries your API key.
Your backend calls POST /api/v1/verifications with your API key and chosen liveness_tier, and receives a verification_id and a one-time hosted url.
Redirect your user to the url. (Prefer to keep them on your page? The Web SDK embeds the same flow instead.)
The user completes a short camera capture on the hosted page. Medium adds sound, and High adds sound and light. Up to three attempts within the link's lifetime.
Your backend fetches GET /api/v1/verifications/{id}. The verdict is only ever released server-to-server against your API key.
Authorization: Bearer sk_live_...02
One POST mints a one-time hosted URL for your user.
curl -X POST https://api.soliton.id/api/v1/verifications \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"liveness_tier": "high",
"client_reference_id": "user-8412",
"redirect_url": "https://app.example.com/verify/done"
}'{
"verification_id": "1de9a648-...",
"liveness_tier": "high",
"url": "https://soliton.id/v/6f2c9be1...",
"expires_at": "2026-07-16T18:30:00Z",
"environment": "production",
"result_source": "biometric",
"test_fixture_scenario": null
}liveness_tier (optional, defaults to high): standard, medium, or high. Choose on your server for this verification; all three work with both key types and both integration methods. The response echoes your choice. Create a new verification to change it.client_reference_id (optional, max 256 chars): your identifier for the user or case; echoed back on status reads.redirect_url (optional): where we send the user afterwards. Its host must be equal to a redirect domain registered for the environment of the key you are using, or be its subdomain (ask us to register yours). Redirect domains and Web SDK iframe origins are separate allowlists; registering one does not register the other. If omitted, the user sees a “you may close this window” screen instead.test_fixture_scenario (sandbox only, optional): pass, fail, or partial. Omit it and a sandbox key creates an ordinary capture; rejected with an sk_live_ key.environment: sandbox or production, decided by your key's prefix. Echoed on every status read and webhook.result_source: biometric for a real capture, synthetic_test_fixture for a test fixture. Present here, on every status read, and on every webhook, so a synthetic result can never be mistaken for a real one. It is independent of environment: a sandbox verification is biometric unless you asked for a fixture.url is single-purpose and expires after 1 hour. Create a fresh verification per attempt; do not reuse links across users.The same endpoint selects liveness for server + redirect and Web SDK integrations. Set liveness_tier in the JSON creation body, then redirect to the returned URL or pass it to open({ url, onComplete }). Do not add a tier to the URL or SDK options. Existing SDK installations already open every tier.
Send {"liveness_tier":"standard"} for a camera-only verification with no sound or light emitted. Use medium to add sound reflection to the camera check. The default, high, uses sound and light reflection.
Both sandbox and production keys accept all three tiers. See the metrics comparison to choose one. Hosted links remain /v/<token> and open /capture. Company demos open /sandbox/<company>/capture; first-party demos open /demo_v1/capture. A copied demo capture link with no saved setup returns to its liveness chooser. The tier is carried in session state and enforced by the backend, without a tier query parameter in the capture link.
All tiers use positioning, anti-spoof and deepfake checks, and capture-integrity checks. Standard requests only the camera, uploads no audio, and uses targeted visual processing. Medium and High also request microphone access and provide speaker-volume guidance.
Standard returns either verified or not verified, with partial_verification: false in every completed result. Passing its required model and capture-integrity checks with sufficient footage gives a verified result; unavailable or inconclusive camera-control evidence does not downgrade it.
Missing or failed required models, fewer than 30 decoded frames, less than two seconds for Standard (1.8 seconds for Medium and High), and fraud vetoes block verification in every tier. Medium and High retain their liveness rules and partial outcomes, including High’s camera-only passes. Those partial results use verified: true, partial_verification: true and remain your risk-policy decision.
The stored tier applies to every retake and appears on create, claim, status, results, fixtures and webhooks. Conflicting browser inputs receive 409; invalid values receive 422. Hosted redirects and SDK messages are unchanged. Usage is reported by tier and environment.
Store the requested tier with the verification id on your backend. Before accepting a completed result, require its liveness_tier to match that stored value; reject missing or mismatched tiers. Read it from the authenticated status endpoint or a signature-verified completion webhook. The backend refuses to complete a hosted verification with a different capture tier.
03
On completion (pass or final fail) we send the user to {redirect_url}?vid={verification_id}.
The redirect carries no verdict, only the id. Treat it purely as a “check now” signal: anyone could type that URL, so never grant access based on the redirect alone. In Web SDK embed mode there is no redirect; the onComplete callback is the equivalent signal, under the same rules.
04
Poll from your backend, typically after the return redirect. Cheap and rate-limited per key at 60 requests/minute.
curl https://api.soliton.id/api/v1/verifications/1de9a648-... \
-H "Authorization: Bearer sk_live_..."{
"verification_id": "1de9a648-...",
"client_reference_id": "user-8412",
"status": "completed",
"liveness_tier": "high",
"verified": true,
"partial_verification": false,
"created_at": "2026-07-16T17:30:00",
"completed_at": "2026-07-16T17:32:41",
"environment": "production",
"result_source": "biometric",
"test_fixture_scenario": null,
"portrait_available": true
}createdThe link has not been opened yet.
in_progressThe user is in the flow.
completedTerminal. verified and partial_verification are now set.
expiredThe hour passed without completion.
verified: true, partial_verification: falseThe tier’s liveness rule and required model checks passed, with no fraud veto.
verified: true, partial_verification: trueSome liveness evidence passed, but the result did not meet the selected tier’s full verification criteria. Treat partial results according to your risk policy.
verified: falseCould not verify a live user. Includes detected spoofs and injection attacks; the reason is deliberately not disclosed.
portrait_available: whether a portrait of the user can be fetched (below). false until status is complete, and stays false when no acceptable portrait frame existed. In the first seconds after completion it can read false while extraction is still running; once status is complete you can simply try the portrait endpoint (it finishes the extraction on demand) and treat a 404 after a short retry as permanently unavailable.
05
One JPEG frame of the user, selected server-side for quality, for your own records or manual review.
curl https://api.soliton.id/api/v1/verifications/1de9a648-.../portrait \
-H "Authorization: Bearer sk_live_..." \
-o portrait.jpgReturns image/jpeg: one portrait frame of the user, selected server-side under neutral lighting (before the light challenge starts). The exact pipeline, so you can calibrate against it:
Resolution is whatever the user’s browser and camera delivered, typically 1280×720. It can be lower on constrained devices; it is not higher, because the capture requests 720p. Expect roughly 60–160 KB per frame depending on scene detail.
One thing to weigh if you run forensic analysis on these frames: the portrait is extracted from the recorded video, so it has already been through the client’s H.264 encode before this stage. The pipeline is deliberately near-lossless on top of that — measured against the decoded frame it retains ~99% of high-frequency energy — but it cannot recover detail the video codec removed. Also usable simply for your own records or manual review, e.g. matching against an ID document.
200The JPEG body.
404Portrait not available: the verification is not yet complete, or extraction found no acceptable face frame. Once complete, a fetch is always safe; a 404 that persists after one short retry means no portrait exists for this verification.
401 / 429Same auth and rate-limit behavior as the status endpoint.
06
Be told the moment a verification reaches its final verdict, instead of polling.
Ask us to enable webhooks for your account: we generate a signing secret, show it to you once, and activate delivery only after you confirm your receiver accepts it. Your endpoint must be https with a publicly valid certificate.
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
User-Agent: Soliton-Webhooks/1.0
X-Soliton-Event: verification.completed
X-Soliton-Event-Id: 8f14e45f-...
X-Soliton-Timestamp: 1785636000
X-Soliton-Signature: v1=6b3a...
{
"event_id": "8f14e45f-...",
"type": "verification.completed",
"occurred_at": "2026-07-16T17:32:41Z",
"data": {
"verification": {
"verification_id": "1de9a648-...",
"client_reference_id": "user-8412",
"status": "completed",
"liveness_tier": "high",
"verified": true,
"partial_verification": false,
"created_at": "2026-07-16T17:30:00Z",
"completed_at": "2026-07-16T17:32:41Z",
"environment": "production",
"result_source": "biometric",
"test_fixture_scenario": null
}
}
}data.verification describes the verification. The only top-level keys are event_id, type, occurred_at, and data.event_id is the same value as the X-Soliton-Event-Id header, and is what you deduplicate on. The verification id lives one level down, at data.verification.verification_id.occurred_at is when the event happened, which is not the same as data.verification.created_at (when the verification was created).client_reference_id is echoed back from your create call, so it is usually the cleanest way to join an event to your own record without a second lookup.verification.completed is currently the only event type. Ignore types you do not recognise rather than erroring, so we can add events without breaking you.
X-Soliton-Signature is v1= followed by the hex HMAC-SHA256 of {timestamp}.{raw request body}, keyed with your whsec_ secret. Sign the raw bytes, before any JSON parsing: re-serializing the parsed body changes key order and whitespace and will not match. Reject anything that fails — unsigned or wrongly-signed requests are not from us.
const crypto = require("crypto");
// rawBody must be the unparsed request body. Re-serializing the parsed
// JSON changes key order and whitespace, and will never match.
function verify(secret, rawBody, timestamp, signature) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
"v1=" +
crypto.createHmac("sha256", secret)
.update(timestamp + ".").update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}import hashlib, hmac, time
# raw_body must be the unparsed request body.
def verify(secret: str, raw_body: bytes, timestamp: str, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300: # reject replays
return False
expected = "v1=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature) # constant timeX-Soliton-Event-Id are byte-identical on every attempt.X-Soliton-Event-Id and make your handler idempotent. Do not use verification_id as the dedupe key.3xx counts as a failure. Register the final URL.Ask us to rotate the signing secret at any time. We show you the new secret first and activate it only once you confirm — but keep accepting the previous secret for a few minutes afterwards, because a delivery already in flight can still arrive signed with the old one. Undelivered events are re-queued under the new secret with their original event ids. Disabling cancels every event that has not been delivered, including ones that already exhausted their 24-hour window, so re-enabling later never floods you with stale completions.
07
A full, separate copy of the product pointed at your staging configuration.
sk_test_ credentials run the sandbox environment. A create call with no test_fixture_scenario behaves exactly like a production one — camera capture, liveness and anti-deepfake processing, a real portrait — but validated against your sandbox redirect domains and sandbox iframe origins, and delivered to your sandbox webhook endpoint.
curl -X POST https://api.soliton.id/api/v1/verifications \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"liveness_tier": "standard",
"client_reference_id": "user-8412",
"redirect_url": "https://dev.example.com/verify/done"
}'This example selects Standard. Replace it with medium or high to test those tiers. The response echoes the tier with environment: "sandbox". To go live, use your production key and registered production origins while keeping the same tier and integration method.
Sandbox is also the only place you can ask for a fixed outcome, which is how you regression-test the branches that are awkward to trigger with a real capture.
curl -X POST https://api.soliton.id/api/v1/verifications \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"liveness_tier": "medium",
"client_reference_id": "user-8412",
"test_fixture_scenario": "partial"
}'passverified: true, partial_verification: false
partialverified: true; partial_verification: true for Medium / High, false for Standard
failverified: false, partial_verification: false
A fixture's url opens a hosted page that never requests camera or microphone permission, never records anything, and completes to the outcome you asked for. It is clearly marked as synthetic on screen and carries synthetic_test_fixture everywhere. Redirects, Web SDK embedding, onComplete, status reads, portraits, and webhooks all behave exactly as they do for a real capture, so you can exercise your whole integration — including the partial branch, which is awkward to trigger with a real capture. The portrait endpoint returns a fixed, visibly synthetic image, never a person.
The separation is enforced by the API and fails closed: sk_live_ with a scenario returns 403, because fixtures are sandbox-only; a production key reading a sandbox verification or a sandbox key reading a production one returns 404 even for the same account; a fixture link cannot enter the capture pipeline; and a redirect_url valid in one environment is rejected in the other unless it is registered there too.
08
Uniform across the v1 API.
401Missing or invalid API key.
404Unknown verification id (or one belonging to another customer, or to your other environment).
400On create: invalid redirect_url — it must be https and on a domain registered for this key's environment.
403On create: an sk_live_ key asked for a test_fixture_scenario. Fixtures are sandbox-only; production credentials cannot produce synthetic results.
422On create: request body failed validation (invalid liveness_tier, malformed JSON, client_reference_id over 256 characters, redirect_url over 2048).
429Rate limit exceeded; back off and retry.
09
What keeps the verdict trustworthy.
Single verification, 1-hour expiry, dead after completion. Create a fresh verification per attempt; never reuse links across users.
Never to your page, the SDK callbacks, or the redirect. The hosted screen tells the end user how their own attempt went, with no detail about which checks ran or why; your integration must treat only the server-to-server verdict as real.
Anti-injection and anti-deepfake defenses update continuously without any deployment on your side.