Skip to content

Verifications API

A verification asks a person to prove one or more things. You create it, send its link, and read the answers, each signed by Prson. The person answers in the Prson app and decides what to share.

MethodPathScopeWhat it does
POST/v1/verificationsverifications:writeCreate a verification; reserves its price
POST/v1/verifications/quoteverifications:readWhat creating this body would reserve now; creates nothing
GET/v1/verificationsverifications:readList, newest first (?status, ?external_ref, ?limit, ?cursor)
GET/v1/verifications/{id}verifications:readOne verification with every answer so far
GET/v1/verifications/{id}/proofs/{proof_id}/{file}verifications:readAn answer's file: photo, video, sensors or signed_pdf
POST/v1/verifications/{id}/cancelverifications:writeStop it; the unspent reserve goes back
POST/v1/verifications/documentsverifications:writeUpload a PDF for a document_signature requirement
GET/v1/verifications/requirementsnone (public)What can be asked for today, with prices

Prices

A flat price per source, not per requirement: each is paid once per verification, however many requirements use it.

SourceWhat it coversRegularUntil 2027-03-31
BaseAny verification. Includes live photos, videos, selfies and liveness, and email.0.500.25
ID check: yes/no factsAny number of facts from the person's passport or ID card chip: age, nationality, document validity, issuing country, verification level.+0.50+0.25
ID check: detailsAny value from the chip: names, date of birth, nationality, document. Includes every fact; instead of the facts price, never both.+1.00+0.50
PDF signatureThe person signs your PDF with their verified name. However many PDFs.+1.00+0.50
PremiumsEach premium requirement: bank account 1.00, bank balance 0.50, bank income 1.50, address 10.00, phone 0.50. Never discounted, never covered by a free plan.per itemper item

Amounts in your balance's currency, the same number in EUR, USD and GBP. Examples at launch prices: Age only 0.50; Name, nationality and age 0.75; Name and a PDF signature 1.25; Name and phone number 1.25. Where each check works: coverage by country.

How the price is reserved and spent: How you are charged.

Authentication

Send your key as Authorization: Bearer prs_live_… (or prs_sandbox_…) to https://api.prson.net. Keys come from the developer console and default to the scopes verifications:read and verifications:write. Keep keys on your server. AI agents can also connect with OAuth: see MCP.

Create a verification

POST /v1/verifications reserves the price from your balance and returns the verification with its share_url (201). Send the link to the person yourself.

curl -X POST https://api.prson.net/v1/verifications \
  -H "Authorization: Bearer $PRSON_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: booking-4821" \
  -d @- <<'JSON'
{
  "title": "Confirm you're over 18 and show your ID to rent a car",
  "requirements": [
    { "type": "fact", "fact": "age_gte_18" },
    { "type": "property", "property": "family_name" },
    { "type": "document", "require_document_verification": true, "document_types": ["passport", "national_id"] },
    { "type": "selfie", "optional": true }
  ],
  "expires_in_hours": 48,
  "external_ref": "booking-4821",
  "metadata": { "branch": "Lisbon airport" }
}
JSON

Body

FieldTypeMeaning
titlestring, requiredWhat the person sees: who is asking and why. Up to 200 characters.
requirementsarray, required1 to 10 requirements. See What you can ask for.
min_verification_level0 to 6The person's minimum identity level. 5: identity read from a passport or ID chip.
allowed_identity_typesarraypersonal, organization. Default: any.
expires_in_hoursinteger1 to 168, default 72.
external_refstringYour own id (an order, a customer). List can filter on it.
metadataobjectYour own data, returned as given. At most 4 KB as JSON.
currencyISO 4217The balance to pay from. Default: EUR if you hold EUR, else your largest balance.

A requirement

Every requirement takes these, plus the fields of its type:

typeOne of: fact, property, selfie, document, selfie_with_document, photo, video, document_signature.
nameA short label the person sees (up to 80 characters). Default: one made from the type.
descriptionWhat to show or why (up to 300 characters).
optionaltrue: the person may skip it.
TypeFields
fact
  • fact The fact id, from this reference.
property
  • property The property type, from this reference.
  • reveal_value true (default): share the value itself. false: share only that a verified value exists.
  • document_types Accepted document kinds: passport, national_id.
  • issuing_countries Accepted issuing countries, ISO 3166-1 alpha-3 in upper case (PRT, DEU, USA).
selfie
  • require_location The capture must carry the place it was taken (GPS).
  • require_document_verification The person must have an identity document Prson verified from its chip. Priced as a document requirement.
  • issuing_countries Accepted issuing countries, ISO 3166-1 alpha-3 in upper case (PRT, DEU, USA).
  • required_facts Facts their verified document must meet, checked when they answer (document facts and levels only).
document
  • document_types Accepted document kinds: passport, national_id.
  • require_location The capture must carry the place it was taken (GPS).
  • require_document_verification The person must have an identity document Prson verified from its chip. Priced as a document requirement.
  • issuing_countries Accepted issuing countries, ISO 3166-1 alpha-3 in upper case (PRT, DEU, USA).
  • required_facts Facts their verified document must meet, checked when they answer (document facts and levels only).
selfie_with_document
  • require_location The capture must carry the place it was taken (GPS).
  • require_document_verification The person must have an identity document Prson verified from its chip. Priced as a document requirement.
  • issuing_countries Accepted issuing countries, ISO 3166-1 alpha-3 in upper case (PRT, DEU, USA).
  • required_facts Facts their verified document must meet, checked when they answer (document facts and levels only).
photo
  • require_location The capture must carry the place it was taken (GPS).
  • require_document_verification The person must have an identity document Prson verified from its chip. Priced as a document requirement.
  • min_photos Must be 1: one photo per requirement (add a requirement for each photo).
video
  • require_location The capture must carry the place it was taken (GPS).
  • require_document_verification The person must have an identity document Prson verified from its chip. Priced as a document requirement.
document_signature
  • document_id The id POST /v1/verifications/documents returned.
  • signature_position Optional visible signature: { page, x, y, width, height } in PDF points from the page's bottom left (page is 1-based). Without it the signature is invisible.
  • min_level The signer's verification level: 5 (default, chip-read document) or 6 (chip plus face match).

Every fact id, property type and capture, with an example: What you can ask for. Send Idempotency-Key to make retries safe: the same key returns the same verification (200, replayed: true) instead of a second one.

Quote

POST /v1/verifications/quote takes the same body (title optional) and says what creating it would reserve now, for your key's app: discounts, a free plan's allowance, sandbox. It creates nothing.

quote.sh
curl -X POST https://api.prson.net/v1/verifications/quote \
  -H "Authorization: Bearer $PRSON_API_KEY" -H "Content-Type: application/json" \
  -d '{ "requirements": [{ "type": "fact", "fact": "age_gte_18" }] }'

# → { "quote": { "regular_cents": 100, "discount": { "kind": "launch", "percent": 50 … },
#                "price_cents": …, "reserved_cents": …, "test": false, "sources": { "document": "facts" },
#                "source_cents": { "base": …, "document": …, "signature": 0 },
#                "requirements": [{ "requirement": 0, "source": "document_facts", "premium_cents": 0 }] } }

Get, list and cancel

GET /v1/verifications/{id} returns the verification with every answer. Status moves open → partially_fulfilled (some requirements answered) → fulfilled (one person answered every required one), or ends cancelled or expired. Several people can answer the same link.

more.sh
# Newest first, 25 a page; pass next_cursor back as cursor.
curl "https://api.prson.net/v1/verifications?status=fulfilled&limit=25" -H "Authorization: Bearer $PRSON_API_KEY"

# Cancel: the link stops working, released_cents goes back to your balance.
curl -X POST https://api.prson.net/v1/verifications/$ID/cancel -H "Authorization: Bearer $PRSON_API_KEY"

# A file from an answer (names in the response's "files")
curl -o selfie.jpg https://api.prson.net/v1/verifications/$ID/proofs/$PROOF_ID/photo -H "Authorization: Bearer $PRSON_API_KEY"

Responses

Each item of verification.responses is one answer to one requirement:

respondent_idThe person, as rsp_…: the same for your organisation every time, different for any other.
slot_id, requirement_name, typeWhich requirement this answers.
statusuploaded, verified or rejected.
verdict_levelHow strongly Prson verified it.
fact{ id, value, description, expires_at }: a yes or no.
property{ type, value, value_disclosed, verified_at, expires_at }: value is null when reveal_value was false.
document{ document_type, issuing_country, expires_on, verification_level, facts }
signatureA signed PDF: signer_name, document hashes, certificate, signed_at, download_path, legal_type advanced_electronic_signature.
filesDownloadable files: photo, video, sensors, signed_pdf.
location{ lat, lng, accuracy_m } when the requirement asked for it.
checksFor captures: device_attestation, motion_consistent, network_location_consistent.
verdict_signature, verdict_disclosuresPrson's signature over the answer, and what it commits to (see Checking a verdict).

The verification also has charge (reserved_cents, spent_cents, any discount or allowance) and test: true in sandbox.

Documents to sign

Upload a PDF, then ask for a document_signature. The person reads every page in the Prson app and signs with a certificate naming them as their chip-read passport or ID card does. You get the signed PDF (PAdES B-T, timestamped): your file plus the signature, so its first bytes hash to the sha256 you uploaded. It is an advanced electronic signature under eIDAS, not a qualified one.

sign.sh
# 1. Upload the PDF (at most 12 MB, not encrypted)
curl -X POST "https://api.prson.net/v1/verifications/documents?name=rental-agreement.pdf" \
  -H "Authorization: Bearer $PRSON_API_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @rental-agreement.pdf
# → { "document": { "id": "vdoc_…", "sha256": "…", "size": 48213, "pages": 3, "unused_expires_at": … } }

# 2. Ask for a signature on it
curl -X POST https://api.prson.net/v1/verifications \
  -H "Authorization: Bearer $PRSON_API_KEY" -H "Content-Type: application/json" \
  -d '{ "title": "Sign the rental agreement",
        "requirements": [{ "type": "document_signature", "document_id": "vdoc_…",
                           "signature_position": { "page": 3, "x": 72, "y": 96, "width": 180, "height": 48 } }] }'

You can also send JSON { "content_base64", "name" }. An upload no verification uses is deleted after a week; the signed PDF is deleted 30 days after the verification expires.

Webhooks

Add an endpoint and Prson POSTs events to it as they happen, instead of you polling.

webhook.sh
curl -X POST https://api.prson.net/v1/org/webhooks \
  -H "Authorization: Bearer $PRSON_API_KEY" -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/prson/webhook", "events": ["verification.completed", "verification.slot_verified"] }'
# → { "endpoint": { "id": "whe_…", … }, "secret": "whsec_…" }   the secret is shown once
EventWhen
verification.answeredSomeone answered a requirement (data.response)
verification.slot_verifiedAn answer was verified
verification.slot_rejectedAn answer was rejected
verification.completedOne person answered every required requirement
verification.expiredIt expired
verification.cancelledYou cancelled it

Each delivery is JSON { id, type, created_at, api_version, data } with two headers: Prson-Event-Id (an idempotency key: a delivery can arrive twice) and Prson-Signature. Check the signature and that t is recent. Reply 2xx within 10 seconds; failures are retried with backoff for about a day.

webhook.mjs
import { createHmac, timingSafeEqual } from "node:crypto";

// Prson-Signature: t={unix seconds},v1={hex HMAC-SHA256(secret, "{t}.{raw body}")}
export function isFromPrson(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(String(header).split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(parts.v1 ?? "", "hex");
  return got.length === expected.length && timingSafeEqual(got, expected);
}

// Express: keep the raw body for the check.
app.post("/prson/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!isFromPrson(req.body.toString("utf8"), req.get("Prson-Signature"), process.env.PRSON_WEBHOOK_SECRET)) return res.sendStatus(400);
  const event = JSON.parse(req.body);          // { id, type, created_at, api_version, data: { verification, response? } }
  // Prson-Event-Id is an idempotency key: a delivery can arrive twice.
  res.sendStatus(200);
});

Checking a verdict

Every answer carries verdict_signature: { envelope, signature, kid }: Prson's RS256 signature over the canonical JSON of the envelope, which names the verification, the requirement, the level and the time, and commits to each disclosed value. verdict_disclosures holds the values as [salt, kind, id, value]. Keep both to prove later, offline, exactly what Prson vouched for.

verdict.mjs
import { verifyVerdict } from "@prson/login-verifier";

const keys = await fetch("https://api.prson.net/v1/company/public-key").then((r) => r.json());
const r = response; // one item of verification.responses
const result = verifyVerdict(r.verdict_signature.envelope, r.verdict_signature.signature, r.verdict_disclosures, {
  kid: r.verdict_signature.kid,
  keys: keys.verificationKeys,
});
// result.ok: Prson signed this answer, and every disclosed value is covered by the signature.

Errors

Errors are JSON: { "error": "code", "message": "…" }, sometimes with more fields.

StatuserrorMeaning
400invalid_requestThe body is malformed; issues lists each problem.
400invalid_requirementOne requirement is wrong; requirement says which.
400email_not_supportedSend share_url yourself; Prson doesn't email people.
401invalid_tokenMissing, unknown or revoked key.
403missing_scopeThe key lacks the scope.
402insufficient_balanceTop up; required_cents says how much is needed.
402allowance_exhaustedA free plan's monthly allowance is used up.
404not_foundNo such verification for your organisation.
409Cancelling one that is already cancelled or expired.
413document_too_largeA PDF over 12 MB.
422requirement_not_availablePrson can't verify this yet (bank, tax and some phone facts).
422requirement_unavailablePhone or address verification is off here: see capabilities.
422document_unsupportedAn encrypted or unreadable PDF.
429too_many_requestsSlow down, then retry.

Rate limits and limits

  • Too many requests in a short time get 429 too_many_requests: wait and retry with backoff. Some routes have daily limits, named in the error.
  • 1 to 10 requirements per verification; a title up to 200 characters; metadata up to 4 KB.
  • A link works 1 to 168 hours (default 72).
  • A key can have a spend cap: past it, creating is refused with 422.

Sandbox and production

Apps in sandbox create free verifications marked test; they can't email people. Answer them with the Prson app like any other. Production needs a verified developer and a topped-up balance or an approved free plan; the console shows what's missing. Phone and address requirements work only where Prson can verify them: capabilities in GET /v1/verifications/requirements says so, and asking for one that is off gets 422 requirement_unavailable.

How you are charged

Creating a verification reserves its price from your prepaid balance. It is spent as answers verify: the base with the first verified answer, the ID check with the first verified answer that used the chip, the signature with the signed PDF, each premium with its requirement's answer. The rest goes back when you cancel or it expires. Nobody answers: nothing is charged. POST /v1/verifications/quote prices a request without creating it.

A capture that is checked against the chip (require_document_verification or required_facts) uses the ID check's facts price; a selfie, photo or document photo that isn't uses only the base. The larger of the launch and volume discounts applies, never both. Volume discounts: 25% from the 1,001th production verification of a month; 40% from the 10,001th production verification of a month. Open-source apps get 1,000 verifications a month free, non-commercial ones 100. Which price each requirement uses is in What you can ask for; the live prices are in GET /v1/verifications/requirements.