Sylmera Identity

The Sylmera Identity API

You create a link. Your customer opens it and photographs their document. You read the decision, or we push it to you. That is the whole surface.

Everything is JSON over HTTPS. There is no SDK, nothing to install and no version header to pin. The base address is https://sylmera.com.

Your first verification

Create an account in the console, generate a sandbox key, and run this. It works before anyone has approved your account.

curl https://sylmera.com/api/verify/sessions.php \
  -H "X-Sylmera-Key: syl_test_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{
    "client_reference": "user-42",
    "checks": ["document", "face"],
    "return_url": "https://your-company.example/done"
  }'

You get back the link. Send it by email, by SMS, or open it in a window.

{
  "success": true,
  "session": {
    "id":               "3f9c1b7a-2e4d-4c58-9a11-7e0d5b83c642",
    "url":              "https://sylmera.com/v/9c2f8a1d…",
    "kind":             "person",
    "checks":           ["document", "face"],
    "environment":      "sandbox",
    "status":           "created",
    "client_reference": "user-42",
    "created_at":       "2026-09-15 11:04:22",
    "expires_at":       "2026-09-22 11:04:22"
  }
}
Keep the link off your own pages

Call this from your server, never from a browser. The key must not travel inside a page — anyone who reads it can create verifications that you pay for.

API keys

Send the key in X-Sylmera-Key, or as Authorization: Bearer … if that suits your HTTP client better. Both are read.

PrefixEnvironmentWhat it does
syl_test_sandbox Works as soon as you create it. Nothing is charged and no real decision is ever made.
syl_live_live Works once your account is approved and has credit. Real decisions, real charges.

A key is shown once, when it is created. We store only its SHA-256 fingerprint, so we cannot show it to you again and neither can anyone who steals our database. Lose it and you create another; revoking is instant.

Sandbox

Sandbox is the same code path as live: the same screens, the same engine, the same responses. The differences are that nothing is billed, and the decision is not treated as a real one by anything downstream.

Photograph any document you have to hand. If you want to see a rejection, photograph something expired, or a document belonging to someone under eighteen.

Create a session

POST /api/verify/sessions.php
FieldTypeMeaning
client_referencestring Your own identifier for this person — a user id, an order number. Up to 120 characters. It comes back on every read and in every webhook. Optional but strongly advised.
checksarray Any of document, face, address, aml, company. Defaults to ["document","face"]. A person check always includes document: the face is compared with its photo.
documentsarray The documents the person may use: passport, id_card, driving_licence, residence_permit, other. Defaults to the first three. If the machine-readable zone shows another type or country than the one the person chose, the session reports what the document says; a type you do not accept is rejected.
capturestring camera (guided camera, default), upload (a photo or scan) or both. The face check is always live.
return_urlstring Where to send your customer once they finish. Must be https. Optional.
expires_in_minutesinteger How long the link stays usable. Between 15 and 43200. Defaults to 10080, which is seven days.
kindstring person (default) or business. A business check asks for the registration number and reads the official register (United Kingdom today).
localestring Language hint for the hosted page, such as en. Optional.

Responds 201 with the session, link included.

Read a session

GET /api/verify/sessions.php?id=<id>
{
  "success": true,
  "session": {
    "id":               "3f9c1b7a-…",
    "status":           "approved",
    "decision":         "approved",
    "client_reference": "user-42",
    "completed_at":     "2026-09-15 11:06:02",
    "person": {
      "name":             "ANGELA ZOE SPECIMEN",
      "date_of_birth":    "1981-06-16",
      "document_country": "GB"
    }
  }
}

The person block appears only on an approved session. On a rejected or held session you get reasons instead — the reasons a customer is allowed to read. Scores, fraud signals and the photographs never leave through this endpoint; they live in your portal and in our audit log.

List sessions

GET /api/verify/sessions.php

The 25 most recent, newest first, plus this month's usage. Narrow it with status, client_reference, environment or limit. This is for a dashboard, not for polling — use a webhook for that.

Statuses and decisions

StatusWhat it means
createdLink made, not opened yet.
openYour customer has opened it.
submittedPhotographs are in, the engine is working.
approvedPassed. This is the one you act on.
rejectedFailed on something conclusive.
reviewA person needs to look. Not a failure.
expiredThe link ran out before it was used.
cancelledStopped from the portal.
Treat review as pending, not as a refusal

With the standard rules, only a few findings reject a person on their own: being under eighteen, a document edited in software, a document type you do not accept, and a proof of address re-saved in an editor. Everything else — including a face that does not match and a name close to a sanctions list — goes to a human, after the person has been asked to retake unclear photos. If a check cannot run at all, that too becomes a review, never a rejection. You can change this in your decision rules.

Decision rules

In the console, Settings → Decision rules lets an owner choose what each kind of finding does: standard, send to review, reject, or ignore. You can also set the risk score from which a case goes to a person (30 by default), a score from which it is rejected outright (off by default), and whether a case that would wait for a person is rejected instead. The rules apply to every check that finishes after you save them, whether it came from a link, the API or the console. Being under eighteen and a sanctions match can be sent to review or rejected, never ignored. A failure on our side is never turned into a rejection.

Manual review in the webhook

When someone on your team decides a case from the console, the verification.completed event carries a review block. The internal note written with the decision never leaves the console.

"review": {
  "decided_by": "reviewer",
  "decision":   "reject",
  "labels": [
    { "code": "face_mismatch", "label": "Face does not match the document" }
  ]
}
Reason codeMeaning
document_unreadableDocument unreadable or incomplete
document_expiredDocument expired
document_not_acceptedDocument type not accepted
document_tamperedDocument edited or forged
face_mismatchFace does not match the document
not_livePhoto not taken live
duplicatePerson already verified under another account
sanctions_confirmedSanctions match confirmed
underageUnder the minimum age
data_mismatchDetails differ from what the person gave you
otherOther reason (a note is required)

These codes never change; new ones may be added.

Webhooks

Add an address in your portal, one per environment, and we post to it the moment a verification finishes. Two events exist today: verification.completed and verification.submitted.

POST https://your-company.example/hooks/sylmera
X-Sylmera-Event:     verification.completed
X-Sylmera-Delivery:  8d1c…
X-Sylmera-Signature: t=1789459562,v1=4f3a…

{
  "id":          "8d1c…",
  "type":        "verification.completed",
  "created":     1789459562,
  "environment": "live",
  "data": {
    "session": { /* the same object as a read */ }
  }
}

Answer 2xx quickly and do your work afterwards. Anything else is retried, with the gaps widening: one minute, five, fifteen, one hour, three, six. After that the delivery is abandoned and stays visible in your portal.

The same event can arrive twice — a timeout on your side that still processed the payload looks identical to a failure from ours. Store id and ignore one you have already handled.

No server yet? Use the Sylmera test receiver

In Test mode, the Webhooks screen of your portal can point your test address at a receiver we host. Every delivery it gets is listed there, with its headers, its body and whether the signature is valid — what your own server will receive, before you have one. It accepts test results only and keeps them for seven days. Never use a placeholder or a public request-bin as your address: an approved person arrives with the name and date of birth read on their document.

Verifying a signature

The header is t=<unix seconds>,v1=<hex>. The signed value is the timestamp, a full stop, then the raw request body. HMAC-SHA256, with your endpoint secret as the key.

<?php
$corps  = file_get_contents('php://input');
$entete = $_SERVER['HTTP_X_SYLMERA_SIGNATURE'] ?? '';

parse_str(strtr($entete, ',', '&'), $p);

$t  = (int) ($p['t'] ?? 0);
$v1 = (string) ($p['v1'] ?? '');

// Un message rejoue la semaine prochaine ne doit pas passer
if (abs(time() - $t) > 300) { http_response_code(400); exit; }

$attendu = hash_hmac('sha256', $t . '.' . $corps, VOTRE_SECRET);

// hash_equals : la comparaison ne doit pas fuir par sa duree
if (!hash_equals($attendu, $v1)) { http_response_code(400); exit; }

http_response_code(200);
Sign the raw bytes

Read the body before any framework parses it. A body that has been decoded and re-encoded is no longer byte-for-byte what we signed, and every signature will fail for reasons that look like a bug in our code.

Errors

Every failure has the same shape: success: false, a stable error string, and a message written for a human reading a log. Branch on error, never on the message.

CodeHTTPWhat to do
UNAUTHORIZED401 The key is missing, revoked or wrong. Not retryable.
ACCOUNT_PENDING403 Live key on an account nobody has approved yet. Your sandbox key still works.
ACCOUNT_SUSPENDED403 Talk to us.
INSUFFICIENT_CREDIT402 Out of credit. Top up in the portal. Sandbox is unaffected.
NOT_FOUND404 No session with that id belongs to you.
VALIDATION_ERROR422 A field is wrong; the message says which. Fix it and resend.
RATE_LIMITED429 Slow down, then retry.
STORAGE_ERROR500 Ours. Safe to retry.

Rate limits

120 requests a minute per key, which is far more than creating links needs. If you are anywhere near it you are probably polling — take a webhook instead and the problem disappears.

Going live

Something here wrong, missing or ambiguous? Tell us and it gets fixed — the documentation is part of the product.

Back to the overview  ·  Open the console