Public API · Passport & ID card (eMRTD) · CodeB Wallet

Passport & ID card reading API.

Read the chip of a passport or ID card through CodeB Wallet with the server as the reading terminal, then open or confirm an account from a genuine document. This page lists every endpoint of emrtd-relay.ashx: the relay protocol, the pages, invitations, the account check, the self-service guest account and the admin endpoints.

Honest scope. Document check only: the chip is genuine and unchanged, read on an attested phone. There is no face comparison, so an account opened this way is a document-checked guest account, not a verified identity. Designed to follow ICAO Doc 9303 and ETSI TS 119 461; not certified. See also Passport & ID chip check and account opening.

How a reading works

  1. A session is created: by an admin (test or invitation), by the signed-in user (account-check), by the sign-in page, or by CodeB Wallet itself (self-session). Each session has a link https://<host>/wallet/emrtd/session?id=<sid> that CodeB Wallet opens as an App Link.
  2. The wallet posts op: info to /wallet/emrtd/relay.ashx and gets the session's challenge, who asks and why, and the data groups to read.
  3. The wallet posts op: start with its Key Attestation chain (and a Play Integrity token when the server requires it) and the access key the person typed (CAN, or the MRZ fields). The attested key is bound to the session; every later request is signed with it (X-CodeB-Signature).
  4. The server is the terminal: it answers with op: apdu commands, the wallet sends each one to the chip over NFC and posts the chip's answer back as op: response. The server runs PACE or BAC, chip or active authentication, and passive authentication of the security object against the CSCA master list and CRLs.
  5. The last answer is op: done with the verdict genuine, not_verified or not_genuine and the reasons. Raw chip files are not kept; the evidence log keeps keyed hashes only.

Design target: ICAO Doc 9303 (reading) and ETSI TS 119 461 (identity-proofing evidence, extended level) — designed to follow, not certified. The CAN and the MRZ are never stored or logged.

Relay and pages

POST /wallet/emrtd/relay.ashx Open #

Relay protocol v1 between CodeB Wallet and the server's reading terminal. One endpoint for the whole reading. Every request is a JSON object with v: 1, op and session; every answer is HTTP 200 with a JSON object whose op says what comes next (info, apdu, aa_challenge, done, cancelled or error with code). The first start carries the phone's Key Attestation chain (and a Play Integrity token when the server requires it); its key is then bound to the session and every later request must carry X-CodeB-Signature made with it. Ops: info (challenge, requester, data groups), start (attestation, access key {type:'can',can} or {type:'mrz',documentNumber,dateOfBirth,dateOfExpiry}, app and card details), response (seq, rapdu base64url, ms), card_error (tag_lost), cancel, evidence (local reading fallback, only when the server allows it). Limits: 5 starts per session, 3 wrong access keys, per-IP rate limit, body cap. Rewritten to emrtd-relay.ashx?p=relay.

Parameters

  • X-CodeB-Signature (header) — Required on every request after the first attested start.

Request (application/json)

{
  "v": 1,
  "op": "info",
  "session": "...",
  "attestation": {},
  "access": {},
  "mode": "relay",
  "seq": 0,
  "rapdu": "...",
  "error": "..."
}

Responses

  • 200 — Next step of the protocol (always 200; errors as op=error with code such as session_unknown, session_expired, signature_invalid, attestation_failed, integrity_failed, wrong_key, protocol)
  • 405 — Not POST
  • 413 — Body too large
  • 415 — Not application/json
  • 429 — Rate limit

200 example

{
  "op": "...",
  "code": "...",
  "message": "...",
  "challenge": "...",
  "apdu": "...",
  "seq": 0,
  "result": {
    "verdict": "genuine",
    "reasons": [
      "..."
    ],
    "mode": "..."
  }
}

GET /wallet/emrtd/session Open #

App Link landing page of a reading session (shown when the wallet is not installed). HTML page (HTTP 200, no redirect) behind the App Link https://<host>/wallet/emrtd/session?id=<sid>; CodeB Wallet opens the link directly. noindex, strict CSP; the session id is not echoed.

Parameters

  • id (query, required) — string

Responses

  • 200 — HTML page

GET /wallet/emrtd/status Open #

State of a sign-in reading, polled by the browser that started it. Only for sign-in readings and only from the browser that holds the session's codeb_emrtd cookie.

Parameters

  • sid (query, required) — string

Responses

  • 200 — State
  • 404 — Unknown session or wrong browser

200 example

{
  "state": "waiting",
  "code": "...",
  "verdict": "..."
}

Guest account from CodeB Wallet (self-service)

Flow in the app

self-info (is it switched on?) → self-session → relay info / start / … / done → self-code (mail a 6-digit code to the address the person typed) → the person types the code → self-finish. The account is a document-checked guest account: genuine chip on an attested phone, confirmed e-mail address, one account per document and per phone; no face comparison, so it is not a verified identity. The PID and Photo ID offered afterwards are test credentials. Guests sign at most 5 documents per UTC day with the CodeB signing service; signing as an invited person does not count. The guest can delete the account in the app at any time (self-delete).

Two answer shapes. Final answers of a reading are {"status","reason","message"} and fixed for that reading (a repeat returns the same answer). Answers the app can recover from are {"error","error_description"}.

GET /emrtd-relay.ashx?p=self-info Open #

Is opening an account in the app switched on?. The wallet hides 'Create a CodeB account' when self_service is false. Also in POST /rqes/csc/v2/info. 120 per minute per IP.

Responses

  • 200 — Switch and limits
  • 429 — Rate limit

200 example

{
  "self_service": true,
  "account_role": "guest",
  "guest_signatures_per_day": 5,
  "session_minutes": 15
}

POST /emrtd-relay.ashx?p=self-session Open #

Start a reading session for a new guest account. Creates a reading session of kind self (15 minutes; it ends after 5 minutes if no phone sent start). The device check runs in the relay start: Key Attestation, the wallet's issuance-key proof and, when the server has it configured, a Play Integrity verdict ok; otherwise the relay answers attestation_failed / integrity_failed and reads nothing. 10 per minute and 30 per hour per network.

Request (application/json)

{
  "lang": "en"
}

Responses

  • 200 — Session
  • 400 — invalid_request
  • 429 — rate_limited or daily_cap
  • 503 — self_service_off or busy

200 example

{
  "session": "...",
  "relay_url": "https://phone.aloaha.com/...",
  "session_url": "https://phone.aloaha.com/...",
  "expires_in": 900
}

POST /emrtd-relay.ashx?p=self-code Open #

Mail a one-time code to confirm the e-mail address. After the reading ended (op: done) and within 15 minutes of self-session. Checks the reading and the limits first, so a final refusal can already come here. The 6-digit code is valid for 10 minutes and kept only in memory as an HMAC. At most 3 addresses or code mails per reading (an address already in use counts too; the 4th try ends the reading), at most 3 codes per address and hour.

Parameters

  • X-CodeB-Signature (header, required) — Signature over the raw request body made with the relay key the wallet bound in the relay start of this session (same scheme as on the relay requests).

Request (application/json)

{
  "session": "...",
  "email": "person@example.com"
}

Responses

  • 200 — Code sent, or the reading's final answer
  • 400 — invalid_email or invalid_request
  • 403 — signature_invalid
  • 404 — session_unknown
  • 409 — email_in_use (counts as a try) or not_ready
  • 410 — session_expired
  • 429 — rate_limited (codes per address) or a final refusal (status refused)
  • 503 — self_service_off or temporarily_unavailable

200 example

{
  "status": "code_sent",
  "email": "...",
  "expires_in": 0,
  "sends_left": 0,
  "message": "..."
}

POST /emrtd-relay.ashx?p=self-finish Open #

Open the guest account with the confirmed address. Release rule self_service: genuine chip, document not expired and not expiring today, no account for this document, attested phone with issuance key, limits (one account per phone, 10 openings per network and hour, daily cap, 24 h after a delete on the same phone or document), code right for this address, address neither a login nor another account's e-mail. Five wrong codes per reading end it. The final answer is fixed per reading: repeats return it again and create or mail nothing. Accepted until 25 minutes after self-session.

Parameters

  • X-CodeB-Signature (header, required) — Signature over the raw request body made with the relay key the wallet bound in the relay start of this session (same scheme as on the relay requests).

Request (application/json)

{
  "session": "...",
  "email": "person@example.com",
  "code": "..."
}

Responses

  • 200 — created / exists / refused
  • 400 — code_required, code_invalid (with attempts_left), code_expired, invalid_request
  • 403 — signature_invalid
  • 404 — session_unknown
  • 409 — not_ready, or a final refusal email_in_use
  • 410 — session_expired
  • 429 — Final refusal: device_limit, recently_deleted, rate_limited, daily_cap, too_many_attempts
  • 503 — self_service_off or temporarily_unavailable

200 example

{
  "status": "created",
  "reason": "document_has_account",
  "message": "...",
  "login": "...",
  "released": true,
  "offers": {
    "pid": "https://phone.aloaha.com/...",
    "photo_id": "https://phone.aloaha.com/..."
  }
}

POST /emrtd-relay.ashx?p=self-delete Bearer #

The signed-in guest deletes the own account (GDPR Art. 17). Access token of the site's own sign-in (client codeb-admin), role guest only. Removes the account, photos, every document binding, open offers, the account's signing requests and signing key, revokes the PID / Photo ID test credentials already issued (status list) and revokes the access token used. The same phone and document can open a new account after 24 hours. 3 per minute per account.

Responses

  • 200 — Deleted
  • 401 — sign_in_required
  • 403 — not_allowed (not a guest account)
  • 404 — not_found
  • 409 — not_deleted
  • 429 — rate_limited
  • 503 — temporarily_unavailable (nothing changed)

200 example

{
  "deleted": true,
  "credentials_revoked": true
}

Invitations, account check and offers

POST /emrtd-relay.ashx?p=invite-info Open #

What an invitation link allows (label, expiry, readings left). Used by /wallet/emrtd/try. The invitation token travels in the body, never in a URL. 300 requests per minute per IP.

Request (application/json)

{
  "i": "..."
}

Responses

  • 200 — Invitation
  • 404 — Unknown or used up
  • 429 — Rate limit

200 example

{
  "label": "...",
  "expires": "2026-10-16T10:00:00Z",
  "readings_left": 0,
  "type": "try",
  "account_created": true
}

POST /emrtd-relay.ashx?p=invite-new Open #

Start a reading session from an invitation.

Request (application/json)

{
  "i": "..."
}

Responses

  • 200 — Session
  • 404 — Unknown invitation
  • 409 — No readings left
  • 429 — Rate limit
  • 503 — Too many open readings

200 example

{
  "sid": "...",
  "view": "...",
  "link": "https://phone.aloaha.com/...",
  "expires": "2026-10-16T10:00:00Z",
  "readings_left": 0
}

POST /emrtd-relay.ashx?p=invite-session Open #

Live state and result of an invitation reading. Polled by the invitee page every 2 s. Shows state, verdict, a preview of the chip data to the person who read it, and the outcome for the account (created, released, offers).

Request (application/json)

{
  "sid": "...",
  "view": "..."
}

Responses

  • 200 — Session view
  • 404 — Unknown session or wrong view token

200 example

{}

POST /emrtd-relay.ashx?p=account-check Bearer #

Start a passport / ID card check of the signed-in user's own account. Answers with the reading link the page opens on the phone. 5 per minute per account.

Request (application/json)

{
  "expected_family_name": "..."
}

Responses

  • 200 — Reading link
  • 401 — Not signed in
  • 429 — Rate limit

200 example

{}

GET /emrtd-relay.ashx?p=pid-offer Open #

Credential offer (authorization code) for the PID test credential. The link in the release mail. Contains no secrets: the wallet signs in to collect.

Responses

  • 200 — OpenID4VCI credential offer

200 example

{
  "credential_issuer": "https://phone.aloaha.com/...",
  "credential_configuration_ids": [
    "..."
  ],
  "grants": {}
}

Admin endpoints

Bearer access token of an admin of this site (aud codeb-admin, with a scope; refresh and id tokens are refused).

POST /emrtd-relay.ashx?p=admin-new Bearer (admin) #

Admin: new test reading session (emrtd-test.html).

Request (application/json)

{
  "forceLocalAa": true
}

Responses

  • 200 — Session
  • 401 — Admin sign-in required
  • 409 — Local fallback switched off
  • 413 — Body over 1 KB
  • 503 — Too many open sessions

200 example

{
  "sid": "...",
  "link": "...",
  "expires": "..."
}

GET /emrtd-relay.ashx?p=admin-session Bearer (admin) #

Admin: state, verdict, evidence summary and preview of a test session.

Parameters

  • sid (query, required) — string

Responses

  • 200 — Session view
  • 401 — Admin sign-in required
  • 404 — Unknown or expired

200 example

{}

GET /emrtd-relay.ashx?p=admin-evidence Bearer (admin) #

Admin: latest evidence records, hash-chain check and configuration.

Responses

  • 200 — Evidence summary
  • 401 — Admin sign-in required

200 example

{}

GET /emrtd-relay.ashx?p=admin-invites Bearer (admin) #

Admin: list invitations.

Responses

  • 200 — Invitations
  • 401 — Admin sign-in required

200 example

{
  "invites": [
    {}
  ]
}

POST /emrtd-relay.ashx?p=admin-invite-new Bearer (admin) #

Admin: new invitation (try, account opening or account check). type try (chip check only), onboard (open an account; e-mail required) or verify (check an existing account; account and expected_family_name). Mails the link when an e-mail is given.

Request (application/json)

{
  "type": "try",
  "label": "...",
  "days": 0,
  "readings": 0,
  "email": "person@example.com",
  "account": "...",
  "expected_family_name": "...",
  "expected_birthdate": "..."
}

Responses

  • 200 — Invitation with link
  • 400 — Invalid
  • 401 — Admin sign-in required

200 example

{}

POST /emrtd-relay.ashx?p=admin-invite-revoke Bearer (admin) #

Admin: withdraw an invitation.

Request (application/json)

{
  "id": "..."
}

Responses

  • 200 — Withdrawn
  • 401 — Admin sign-in required
  • 404 — Unknown

200 example

{}

GET /emrtd-relay.ashx?p=admin-proofing Bearer (admin) #

Admin: accounts waiting for an identity-proofing decision.

Responses

  • 200 — List
  • 401 — Admin sign-in required

200 example

{}

POST /emrtd-relay.ashx?p=admin-proofing-decide Bearer (admin) #

Admin: release or reject an account's document check. A release offers the PID and Photo ID test credentials to the wallet that read the document.

Request (application/json)

{
  "user": "...",
  "decision": "release",
  "note": "..."
}

Responses

  • 200 — Decided
  • 400 — Invalid
  • 401 — Admin sign-in required
  • 404 — Unknown account

200 example

{}

Frequently asked questions

How does CodeB check a passport or ID card?

CodeB Wallet only forwards the chip commands over NFC; the CodeB server is the reading terminal. It runs PACE or BAC with the CAN or MRZ the person typed, chip or active authentication, and passive authentication of the document security object against the CSCA master list and the CRLs, and returns genuine, not_verified or not_genuine.

Are the CAN, MRZ or chip data stored?

No. The CAN and MRZ are never stored or logged. Raw chip files are not kept after the reading; the evidence log contains keyed hashes only. Account data (names, date of birth, chip photo) is kept only when the reading opens or confirms an account.

Can a new user open an account from the app?

Yes, as a document-checked guest account: CodeB Wallet reads a genuine passport or ID card on an attested phone, the e-mail address is confirmed with a mailed 6-digit code, and the account opens with the PID and Photo ID test credentials offered to that wallet. One account per document and per phone; no face comparison.

Is this certified?

No. The reading follows ICAO Doc 9303 and the evidence is designed to follow ETSI TS 119 461 (extended level of identity proofing); nothing is certified. PID and Photo ID are test credentials.

How does a guest delete the account?

With POST /emrtd-relay.ashx?p=self-delete and the access token of the site's sign-in. It removes the account, photos, document bindings, open offers, signing requests and the signing key, revokes the credentials already issued, and revokes the token used.

Machine-readable: /openapi.json (OpenAPI 3.1, also at /.well-known/openapi.json) · All public APIs · Questions