Sign inCOMING SOON

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.

This documentation is a preview. Passwave accounts are not open yet, so API keys are not being issued and the calls described here do not reach a live API. More on what's still to open.

Introduction

The base URL is:

https://api.passwave.com

The 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.

Headershttp
Authorization: Bearer sk_test_••••••••••••••••••••••••
X-App-Auth: passwave-app
Accept: application/json

Libraries

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.

Browserv1.0.0 · script tag
<script src="https://js.passwave.com/v1.js"></script>

Drop-in widget. No build step. Render the QR code without server-side handling.

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.

Pythonv1.0.0 · pip
pip install passwave
PHPv1.0.0 · composer
composer require passwave/passwave-php
Node.jsv1.0.0 · npm
npm install @passwave/js

Initialize verification session

GET/api/verification/init

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

Field
Type
Description
Authorizationrequired
string
Bearer token using your project API key.
X-App-Authrequired
string
Fixed value: passwave-app.
Acceptrequired
string
application/json.

Response fields

Field
Type
Description
verification_url
string
Base URL for the deep link the QR code points to.
verification_link
string
The complete link to encode in the QR code. Compose nothing yourself: this is verification_url and access_code already joined.
access_code
string
Session identifier in Base32-Crockford (excludes I, L, O, U), hyphenated in fours for display. Sixteen characters, or eighteen when the deployment emits a two-character instance prefix. Case-insensitive on receipt, and the look-alikes are folded.
expires_at
string
ISO 8601 timestamp when the session expires.
expires_in
integer
Session TTL in seconds. Defaults to 600.

Example

Requestbash
curl https://api.passwave.com/api/verification/init \
  -H "Authorization: Bearer sk_test_..." \
  -H "X-App-Auth: passwave-app" \
  -H "Accept: application/json"
Response · 200 OKjson
{
  "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

GET/api/verification/retrieve

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

Field
Type
Description
Authorizationrequired
string
Bearer token using your project API key.
Access-Keyrequired
string
The access_code returned by /init. Identifies the session being polled.
X-App-Authrequired
string
Fixed value: passwave-app.
Acceptrequired
string
application/json.

Query parameters

Field
Type
Tier
Description
include_age_band
boolean
Premium or Custom
Include the user's age band.
include_over_21
boolean
Premium or Custom
Include whether the user is 21 or older.
include_over_25
boolean
Premium or Custom
Include whether the user is 25 or older.
include_legal_gender
boolean
Premium or Custom
Include the user's legal gender.
include_country
boolean
Premium or Custom
Include the country or countries that issued the user's documents.

Response fields

Field
Type
Tier
Description
scanned
boolean
True once the QR code has been scanned by the Passwave app.
pending
boolean
True until the user submits via the app. When false the verifications object is populated.
status
enum
Final state once pending is false. Currently: completed.
access_code
string
The session's access code, echoed back.
created_at
string
ISO 8601 timestamp when the session was created.
expires_at
string
ISO 8601 timestamp when the session expires.
completed_at
string
ISO 8601 timestamp when the user completed the submission.
verifications
object
The answers this project asked for. Always present once pending is false.
site_token
string
Stable pseudonymous token for this user at your project. The same user always yields the same token here and a different one at every other project, so you can recognise returning users but cannot correlate them across relying parties. 64 lowercase hex characters.
verified
boolean
Whether the user holds a completed Passwave verification.
verification
object
The answers themselves, listed in the fields below.
verification.status
enum
State of the verification. Currently: verified.
verification.country
array
Premium or Custom
Every country that issued a document this user verified with. Each entry carries name, iso_code and emoji_flag.
verification.date_expires
string
ISO 8601 date the verifying ID document expires.
verification.legal_gender
enum
Premium or Custom
Legal gender the user's verified documents state. One of the values listed under Legal gender below. Never null: undefined means no document states one, and the key is absent altogether when your plan is not entitled to the answer or did not ask for it.
verification.over_18
boolean
Whether the user is 18 or older.
verification.over_21
boolean
Premium or Custom
Whether the user is 21 or older.
verification.over_25
boolean
Premium or Custom
Whether the user is 25 or older.
verification.age_band
enum
Premium or Custom
Age range the user falls in. One of the bands listed under Age bands below. Verification is refused below 16, so null means the band is absent rather than a younger user.
verifications.capability_code
string
Lets your service ask follow-up questions about this user; see Ask whether a name matches. Returned once, on every plan, with every completed share. If you lose it, ask the user for a new share. 32 characters, Base32-Crockford (no I, L, O, U).
verifications.capabilities_version
integer
Version of the capability table this code was issued under. Currently 1.
verifications.capabilities
array
What the code can ask and until when: one entry per capability with capability, window_hours, window_ends_at (ISO 8601, measured from the share) and lifetime_cap. Read it rather than hard-coding the table.
preferences
null
Reserved for future use. Always null; the API never populates it today.

Example

Requestbash
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"
Response · 200 OK · pendingjson
{
  "status": {
    "success": true,
    "code": 200,
    "message": "Waiting for user to complete request via Passwave app."
  },
  "data": {
    "scanned": false,
    "pending": true
  }
}
Response · 200 OK · completed (Premium, every include_ flag set)json
{
  "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 bandstext
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 record
Legal gendertext
legal_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 set

Ask whether a name matches

POST/api/capability/name-match

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

Field
Type
Description
Authorizationrequired
string
Bearer token using your project API key.
X-App-Authrequired
string
Fixed value: passwave-app.
Acceptrequired
string
application/json.
Content-Typerequired
string
application/json.

Request body

Field
Type
Description
coderequired
string
The capability_code from /retrieve. 32 characters; a malformed code is a 400, not a refusal.
namerequired
string
The name as you hold it, up to 255 characters.

Response fields

Field
Type
Tier
Description
result
enum
Premium or Custom
One of match, partial, no_match. Present only when the question was evaluated.
refused
boolean
true when the question could not be evaluated; result is then absent.
reason
enum
Why, as one of the slugs under Refusal reasons. A refusal is never a no_match, and refusals do not count toward the five.

Example

Requestbash
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" }'
Response · 200 OK · evaluatedjson
{
  "status": {
    "success": true,
    "code": 200,
    "message": "Name match evaluated"
  },
  "data": {
    "result": "match"
  }
}
Response · 200 OK · refusedjson
{
  "status": {
    "success": true,
    "code": 200,
    "message": "Name match refused"
  },
  "data": {
    "refused": true,
    "reason": "window_closed"
  }
}
What a service can ask latertext
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, lifetime
Refusal reasonstext
unknown_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.

Errors

Code
Type
Description
400
validation_error
Invalid request parameters. Check the request format.
401
auth_error
Missing or invalid API key.
403
forbidden
Invalid X-App-Auth header. Use passwave-app.
404
not_found
Session expired or not found. Reinitialize via /init.
409
conflict_error
Verification already completed. Cannot resubmit.
429
rate_limit_exceeded
Too many requests. Inspect X-RateLimit-Reset and apply exponential backoff.
500
server_error
Internal server error. Retry with backoff.