Passwave API
Verify age and identity attributes without handling ID documents. The API is REST over HTTPS, returns JSON, and is driven by a QR-code handoff to the Passwave mobile app.
Introduction
The base URL is:
https://api.passwave.comThe full contract is also published as a machine-readable OpenAPI specification: openapi.yaml
A verification flow has three steps: initialize a session and display the returned QR code, the user scans and submits via the Passwave app, then your service polls for the result. You never see the ID documents; the user submits them to Passwave directly.
A completed share also hands your service a capability code: a 32-character key that lets you ask Passwave one kind of follow-up question about that user for a limited time, without a new share. Today the one capability is name match, for 24 hours after the share and at most five times. The code is returned once, on every plan, and is inert without your API key. The user sees every question you ask and can revoke the code at any time, so a code that worked yesterday can be refused today; handle that. Capabilities are versioned and read from the response, not hard-coded, so a new capability can arrive without reissuing codes.
Authentication
Every request carries your project API key in the Authorization header alongside the fixed X-App-Auth identifier and Accept header. The retrieve endpoint additionally requires an Access-Key header carrying the session's access code.
Authorization: Bearer sk_test_••••••••••••••••••••••••
X-App-Auth: passwave-app
Accept: application/jsonLibraries
Official client libraries. Each library ships only this product; other Passwave products have their own packages so you pull in what you need. Source is MIT-licensed on GitHub, with direct downloads as signed release artifacts mirrored from the same tags.
Frontend script
Drop-in browser script that renders the QR code and handles the deep-link handoff. Used alongside a backend SDK, not in place of one.
Backend SDKs
Server-side libraries that sign requests with your API key, initialise sessions, and return a typed verification result. Pick one for your stack.
Initialize verification session
Creates a verification session and returns the access code your service uses to render the QR code. Sessions expire after 10 minutes by default.
Request headers
Response fields
Example
curl https://api.passwave.com/api/verification/init \
-H "Authorization: Bearer sk_test_..." \
-H "X-App-Auth: passwave-app" \
-H "Accept: application/json"{
"status": {
"success": true,
"code": 200,
"message": "Verification session created"
},
"data": {
"verification_url": "https://passwave.com/open",
"verification_link": "https://passwave.com/open/AB3X-7K9M-2PQ5-4WZ8",
"access_code": "AB3X-7K9M-2PQ5-4WZ8",
"expires_at": "2026-04-29T12:10:00Z",
"expires_in": 600
}
}Retrieve verification data
Polls for verification completion and returns the submitted data once the user has finished. Recommended interval is 5–10 seconds; sessions expire after 10 minutes. Every completed share also returns a capability_code and the capabilities it carries, on every plan.
Request headers
Query parameters
Response fields
Example
curl https://api.passwave.com/api/verification/retrieve \
-H "Authorization: Bearer sk_test_..." \
-H "Access-Key: AB3X-7K9M-2PQ5-4WZ8" \
-H "X-App-Auth: passwave-app" \
-H "Accept: application/json"{
"status": {
"success": true,
"code": 200,
"message": "Waiting for user to complete request via Passwave app."
},
"data": {
"scanned": false,
"pending": true
}
}{
"status": {
"success": true,
"code": 200,
"message": "Verification data found"
},
"data": {
"status": "completed",
"created_at": "2026-04-29T12:00:00Z",
"expires_at": "2026-04-29T12:10:00Z",
"completed_at": "2026-04-29T12:05:00Z",
"access_code": "AB3X-7K9M-2PQ5-4WZ8",
"verifications": {
"site_token": "a3f82c917d4e5b60c8f1d92e7a45b3c6091e8d74f2ab5c30e6d19f84b7c25a01",
"verified": true,
"verification": {
"status": "verified",
"country": [
{ "name": "United Kingdom", "iso_code": "GB", "emoji_flag": "🇬🇧" }
],
"date_expires": "2029-01-01T00:00:00.000000Z",
"legal_gender": "male",
"over_18": true,
"over_21": true,
"over_25": true,
"age_band": "25-35"
},
"capability_code": "7Q3KX9M2P4R8T6V1W5Y0Z2A3B4C5D6E7",
"capabilities_version": 1,
"capabilities": [
{ "capability": "name_match", "window_hours": 24, "window_ends_at": "2026-04-30T12:05:00Z", "lifetime_cap": 5 }
]
},
"preferences": null
}
}age_band covers ages
──────── ───────────
16-17 16 to 17
18-20 18 to 20
21-24 21 to 24
25-35 25 to 35
36-45 36 to 45
46-60 46 to 60
61-75 61 to 75
76+ 76 and over
null no band on recordlegal_gender means
──────────── ─────
male a verified document states male
female a verified document states female
other a verified document states a third value
undefined no verified document states a legal gender
(absent) not entitled, or include_legal_gender was not setAsk whether a name matches
Asks whether a name your service holds matches the name on the user's verified documents, using the capability_code from /retrieve. Available for 24 hours after the share and at most five times per code. Premium and Custom plans. The name you send is normalised, compared and discarded: it is never stored and never written to any log, including the user's own history, which records only that you asked and what the answer was.
Send the name as you hold it. Order, case, accents, hyphens, apostrophes and titles are normalised on our side, and common short forms are recognised (a card reading BILL SMITH matches a passport reading WILLIAM SMITH), so do not pre-process it. The answer is match, partial or no_match. partial means the family name on the document agreed; the given names did not all agree. It is the case to step up on, not to decline on.
A result is only as trustworthy as the name you sent. A card-present read carries the issuer's embossed name; a card-not-present checkout carries whatever the customer typed, and a standard authorisation does not check the cardholder name at all. Someone holding a stolen card and their own verified Passwave account can type their own name and receive match. Treat a match on a customer-typed name as a signal, not a control. Run the check before capturing payment and gate on it; prefer an issuer-confirmed name; and where your acquirer offers an issuer name check (Visa Account Name Inquiry, Mastercard Account Validation), run it first. An issuer-confirmed name and a Passwave match together are a control no single name can satisfy except the genuine cardholder's.
The capability covers the 24 hours after the share. Do not build recurring billing on it; a returning customer needs a fresh share.
Request headers
Request body
Response fields
Example
curl -X POST https://api.passwave.com/api/capability/name-match \
-H "Authorization: Bearer sk_test_..." \
-H "X-App-Auth: passwave-app" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{ "code": "7Q3KX9M2P4R8T6V1W5Y0Z2A3B4C5D6E7", "name": "SMITH/JOHN MR" }'{
"status": {
"success": true,
"code": 200,
"message": "Name match evaluated"
},
"data": {
"result": "match"
}
}{
"status": {
"success": true,
"code": 200,
"message": "Name match refused"
},
"data": {
"refused": true,
"reason": "window_closed"
}
}Capability table · version 1 · a code lives 90 days from the share
capability window after share answer cap per code
name_match 24 hours match · partial · no_match 5, lifetimeunknown_code Never issued, or already deleted at 90 days. Ask the user for a new share.
wrong_service Issued to a different project. A code is inert outside the project it was issued to; check the API key you present.
revoked The user cut your service off. Final for this share.
expired Past its 90-day life. Ask for a new share.
window_closed The 24-hour name-match window after the share has closed. Ask for a new share.
cap_exhausted All five name-match queries on this code are used. Stop asking.
service_rate_limited Your project is over its rolling 24-hour query ceiling. Back off and retry later.
no_document The user holds no verified document to compare against. Unanswerable, not a no_match.
name_unusable The name could not be evaluated: fewer than two parts, no Latin letters, or more than ten parts. Fix the input; not a no_match.Webhooks
Coming soon.