Overview
Confirm who is behind every account.
Use Upsky Verify to check a person's identity before they use your app. It has verification tiers, from a selfie with an age estimate to an official ID matched to the person's face.
How it works
- Your server creates a session.
- You send the person to the session's
url, a page onverify.upsky.devthat works on any phone. You can also start one from the dashboard, which gives you a QR code and a link to share. - They agree to the notice, photograph their ID if the tier needs it, and take a selfie.
- Upsky runs the checks and sends a webhook with the result. You can also read the session at any time.
You're charged only when a session reaches a decision. A link that expires or that you cancel costs nothing.
Create a session
curl https://api.upsky.dev/v1/verify/sessions \
-H "Authorization: Bearer $UPSKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tier": "identity", "reference": "user_123", "return_url": "https://example.com/done" }'| Field | Default | What it does |
|---|---|---|
tier | identity | age or identity |
reference | Your own ID for the person | |
return_url | Where the person goes after submitting | |
minimum_age | 18 | From 13 to 25 |
allow_selfie_upload | false | Let the person send a selfie from their photos |
show_result | false | Show the person the result on the link |
expires_in | 86400 | Seconds the link works, from an hour to seven days |
project_id | One of your projects, to group sessions | |
metadata | Up to 20 keys of your own | |
options | Accepted IDs, outcome rules and photo retention | |
expected | What you expect the ID to say |
Selfies
By default the selfie has to be taken with the camera on the page, there and
then. Set allow_selfie_upload to true to also let the person choose a photo
they already have. The session returns the setting as allow_selfie_upload,
and the dashboard has the same switch when you start a verification.
Live sessions always take the selfie with the camera, through the liveness check, whatever this is set to.
Showing the result
By default the link only thanks the person, and you tell them the result
yourself. Set show_result to true to show it on the link instead: right
after they send their photos, the page waits for the checks and says whether
they were verified, weren't, or are being reviewed. It never says why, so the
page can't be used to learn what gets past the checks. The dashboard has the
same switch when you start a verification.
If a session in review is later approved or rejected, the link shows the new result the next time it's opened.
Approval thresholds
How sure each check must be is your team's choice, in the dashboard under Verify → Settings. Pick a preset, or set each number yourself with Custom:
| Preset | Liveness pass | Liveness review | Face match pass | Face match review | Age margin |
|---|---|---|---|---|---|
| Relaxed | 65 | 40 | 90 | 80 | 2 years |
| Balanced | 80 | 50 | 95 | 85 | 3 years |
| Strict | 90 | 70 | 98 | 92 | 5 years |
A score at or above a pass mark passes. From the review mark up to the pass
mark, the session goes to review (liveness_inconclusive or
face_match_inconclusive), so a person can look. Below the review mark it's
rejected (liveness_failed or face_mismatch). On the age tier, a selfie
alone passes when the person looks at least minimum_age plus the age margin:
the middle of the estimated range, returned as checks.age.estimated_age. For
an 18+ check, that's from looking 20 on Relaxed, 21 on Balanced and 23 on
Strict. A range entirely under minimum_age is rejected, and anything between
goes to review.
Balanced is the default. A project can have its own thresholds, chosen in the
same place: a session with that project_id uses them, and every other session
uses your team's. Each session keeps the thresholds it was created
with, returned as thresholds, so changing your settings never changes how an
existing session is decided. When a liveness check scores under the pass mark,
the page suggests trying again, within the link's three tries.
Accepted IDs
On the identity tier, you choose which IDs a person may use and which
countries they may come from, in the dashboard under Verify → Settings or
per session:
curl https://api.upsky.dev/v1/verify/sessions \
-H "Authorization: Bearer $UPSKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"options": {
"documents": {
"allowed_types": ["passport", "mx_ine"],
"allowed_countries": ["MEX", "USA"]
}
}
}'allowed_types is any of mx_ine, passport, us_drivers_license and
id_card. The person only sees the ones you accept, and isn't asked to choose
when there's just one. allowed_countries takes ISO 3166-1 alpha-3 codes; leave
it empty to accept any country. An INE only counts as MEX and a US driver's
license as USA, so a request that accepts neither a matching country nor
another ID is refused.
The ID is read as soon as its photos are in. If it's blurry, isn't the kind the
person chose, has expired or comes from a country you don't accept, the page
says so and asks for new photos, up to max_attempts times (3 by default, 1 to
5). After the last try the person sends the photos they have, and your outcome
rules decide.
Outcome rules
Each problem a check can find leads to one of three outcomes: reject the
session, send it to review, or ignore it. Set them in Verify → Settings,
or per session in options.rules:
| Rule | Default | Can be |
|---|---|---|
liveness_inconclusive | review | reject, review |
age_inconclusive | review | reject, review |
document_unreadable | reject | reject, review |
document_expired | reject | reject, review, ignore |
document_checksum_failed | reject | reject, review |
document_type_mismatch | review | reject, review |
document_country_not_accepted | reject | reject, review |
date_of_birth_missing | review | reject, review |
face_match_inconclusive | review | reject, review |
age_estimate_mismatch | review | reject, review, ignore |
name_mismatch | review | reject, review, ignore |
date_of_birth_mismatch | review | reject, review, ignore |
A failed liveness check, a selfie with no face, a face that isn't the ID's and an age under the minimum always reject.
Expected data
If you already know who the person should be, send it as expected and the ID
is compared with it:
{
"tier": "identity",
"expected": {
"first_name": "Ana",
"last_name": "García",
"date_of_birth": "1992-04-15"
}
}Names match whatever their accents, case or middle names: "Ana" matches "ANA
MARIA". The result is in checks.data_match, field by field: match,
mismatch, or not_read when the ID gave nothing to compare. A difference
sends the session to review unless you change the name_mismatch or
date_of_birth_mismatch rule.
Branding
The page a link opens can carry your name, your logo, your color and a short
message of your own. Set them in Verify → Settings, for your team or for a
project; a session with that project_id uses the project's. Changes reach
links you've already sent.
Data retention and redaction
Photos are deleted 30 days after a session ends. Change that in Verify →
Settings, or per session with options.retention_days (1 to 365). A session
waiting for your review keeps its photos at least 7 days.
To delete one person's data sooner, redact the session:
curl -X POST https://api.upsky.dev/v1/verify/sessions/$SESSION_ID/redact \
-H "Authorization: Bearer $UPSKY_API_KEY"This deletes the photos, what was read from the ID, the expected data, and
your reference, metadata and return_url, right away. The status, reasons
and checks stay as your record, and redacted_at says when it happened. It
can't be undone, only works on a session that has ended, and sends a
verify.session.redacted webhook.
Every setting above follows the same order: what the request sets, then the
project's settings, then your team's. Each session keeps the options it was
created with, returned as options.
Session status
| Status | Meaning |
|---|---|
pending | The link hasn't been opened |
in_progress | The person is taking their photos |
processing | Submitted; the checks are running |
approved | Every check passed |
rejected | A check failed. reasons says which |
needs_review | The checks couldn't decide. Approve or reject it in the dashboard |
expired | The link wasn't finished in time. Not charged |
canceled | You canceled it. Not charged |
A decided session has checks (liveness, age, document, face match and, when
you sent expected, data match, with their scores) and, for the identity tier,
verified_outputs: the name, date of
birth and document details read from the ID.
Test mode
Sessions made with a test key run the whole flow with simulated checks and are never charged. On the last step, the person picks the result they want, so you can test approved, rejected and needs-review without real IDs.
Live mode
Sessions made with a live key run real checks and are charged when they reach a
decision. Live sessions are open for the age and identity tiers.
The person gets three tries at the liveness check on each link. After the third, the link can't continue: create a new session for them. A link that runs out of tries and expires isn't charged.