# Build with AI (/docs/ai) Every page of these docs is also available as Markdown, so AI coding assistants (Claude Code, Cursor, Copilot and others) can read exactly what you read. | URL | What it is | | --- | --- | | [`/llms.txt`](/llms.txt) | An index of every page, with links | | [`/llms-full.txt`](/llms-full.txt) | All guides in one file | | `/docs/.md` | One page as Markdown, e.g. [`/docs/identity/login.md`](/docs/identity/login.md) | | [`/openapi/identity.yaml`](/openapi/identity.yaml) | The Identity API's OpenAPI spec | | [`/openapi/sign.yaml`](/openapi/sign.yaml) | The IDfier Sign API's OpenAPI spec | Each page also has a **Copy Markdown** button, to paste it into a chat. ## Example prompt [#example-prompt] ```text Add "Login with IDfier" to this Next.js app. Follow https://developers.idfier.com/docs/identity/login.md and the example in https://developers.idfier.com/docs/identity/login/nextjs.md. Keep the client secret on the server and bind the login to the browser with a cookie. ``` ## MCP server [#mcp-server] Connect your assistant to the IDfier docs MCP server and it can search these docs, read pages and fetch the OpenAPI specs while it writes your integration: ``` https://developers.idfier.com/mcp ``` It is read-only and needs no account or key. It never calls IDfier's APIs for you. ```bash claude mcp add --transport http idfier https://developers.idfier.com/mcp ``` In `.cursor/mcp.json`: ```json { "mcpServers": { "idfier": { "url": "https://developers.idfier.com/mcp" } } } ``` In `.vscode/mcp.json`: ```json { "servers": { "idfier": { "type": "http", "url": "https://developers.idfier.com/mcp" } } } ``` In Claude (web or desktop), go to **Settings → Connectors → Add custom connector**, name it IDfier and paste `https://developers.idfier.com/mcp`. Any client that supports MCP over Streamable HTTP: use the URL above, with no authentication. ### Tools [#tools] | Tool | What it does | | --- | --- | | `search_docs` | Searches the guides and API reference | | `get_page` | Reads one page as Markdown; API reference pages return the endpoint's OpenAPI operation | | `list_pages` | Lists every page, by section | | `get_openapi_spec` | The full spec of the Identity API or the IDfier Sign API | ### Prompts [#prompts] | Prompt | Starts | | --- | --- | | `add-login-with-idfier` | Adding Login with IDfier, for a framework you name | | `add-age-check` | An age check, for an age limit you name | | `add-verified-signing` | Integrating the IDfier Sign API | # Age verification (/docs/identity/age-verification) IDfier knows the customer's verified date of birth (from their ID document, BankID or EU Digital Identity Wallet) and sends you only the answer: ```json { "id": "u_3f9a…", "ageOver18": true } ``` Age verification isn't a separate API. It is a set of **age scopes** that you use with either way of verifying a customer: | | [Login with IDfier](/docs/identity/login) | [Verification links](/docs/identity/verification-links) | | --- | --- | --- | | How the customer verifies | Scans a QR code on your site, or taps a button on their phone | Opens a link you send (IDfier can email it) | | Which age check | Fixed for your business client (IDfier sets it up) | Chosen per link, e.g. `"scopes": ["ageOver18"]` | | Getting the answer | Poll the result, or get a webhook and then fetch it | Poll the link | | Good for | Checkout, sign-up, entry to an age-restricted page | Checking someone before a booking, rental or delivery | ## Age scopes [#age-scopes] | Scope | `true` when | | --- | --- | | `ageOver13`, `ageOver16`, `ageOver18`, `ageOver21`, `ageOver65` | The customer is at least that old | | `ageBetween{min}And{max}` | The customer's age is from `min` to `max`, both included, e.g. `ageBetween18And25` | - Only the five `ageOver` limits above exist. Any other number, such as `ageOver20`, is not an age check: verification links reject it, and the login API ignores it. Use `ageBetween20And130` for another limit. - `ageBetween18And25` is `true` from the 18th birthday until the day before the 26th. `min` must be lower than `max`. - If IDfier has no date of birth for the customer, the field is **left out**. Treat a missing field the same as `false`: not verified. - You get only the answer. The date of birth and the age are sent only if your business client also has the `dateOfBirth` or `age` scope, so ask for the answer alone if that's all you need. The answer is computed when you read the result, so it is correct on the day you ask. ## With Login with IDfier [#with-login-with-idfier] A login session's scopes come from your business client, so ask IDfier to set up a client for age checks, for example with the scopes `ageOver18` and `authLevel`. Then follow the [login flow](/docs/identity/login) and read the answer from the result: ```json { "status": 100, "data": { "user": { "id": "u_3f9a…", "ageOver18": true, "authLevel": "Gold" }, "session": { "referenceId": "550e8400-e29b-41d4-a716-446655440000", "status": "approved", "method": "idfier", "expiresAt": "2026-10-31T10:05:00.000Z", "createdAt": "2026-10-31T10:00:00.000Z" } } } ``` Grant access only when `user.ageOver18 === true`. Read the result on your server and keep the decision there: never trust an answer sent back from the customer's browser. Different checks for different products, say 18 for one and 21 for another? Ask IDfier for a business client per check, or use verification links, where you choose the check per link. ## With verification links [#with-verification-links] Choose the check when you create the link. Your business client needs at least one of the scopes `age`, `dateOfBirth` or an age scope; then any age check is allowed: ```bash curl -X POST https://api.idfier.com/api/v3/business/links \ -H "clientid: $IDFIER_CLIENT_ID" \ -H "clientsecret: $IDFIER_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{ "recipientName": "Anna Lindqvist", "recipientEmail": "anna@example.com", "message": "Please confirm your age before we hand over the keys.", "scopes": ["ageOver18"], "expiresInDays": 3 }' ``` Then poll `GET /business/links/:id` until `status` is `approved`: ```json { "status": 100, "data": { "id": "…", "status": "approved", "scopes": ["ageOver18"], "method": "idfier", "result": { "id": "u_3f9a…", "ageOver18": true } } } ``` Each link uses one verification credit; with none left you get `402`. ## What the customer sees [#what-the-customer-sees] The IDfier app shows your business name and logo and what you're asking for before the customer approves. They can decline; the session or link is then `denied`. ## Which `authLevel` to trust [#which-authlevel-to-trust] Add the `authLevel` scope to know how the customer's identity, and so their date of birth, was verified: | `authLevel` | Verified with | | --- | --- | | `Gold` | ID document with its chip read (NFC), or an IDfier profile created from an EU Digital Identity Wallet | | `Silver` | BankID | | `Bronze` | ID document scan without the chip | | `EU Wallet` | Shared directly from an EU Digital Identity Wallet (`method` is `eudi_wallet`) | | `Unknown` | None of the above | For goods with a legal age limit you may want to require `Gold`, `EU Wallet` or `Silver`. # Errors (/docs/identity/errors) The Identity API uses standard HTTP status codes. Every response body also has a numeric `status`; errors have the reason in `data.error`: ```json { "status": 101, "data": { "error": "Session not found" } } ``` ## Status codes [#status-codes] | HTTP | `status` | When | | --- | --- | --- | | `200` | `100` | Success | | `201` | `100` | Created: a login session or a verification link | | `400` | `105` | Missing or invalid input, e.g. `Missing referenceId`, `webhookSecret must be 16 to 255 characters`, `Scopes not allowed for this business: …` | | `401` | `103` | Missing, wrong or deactivated `clientid` / `clientsecret` | | `402` | `106` | No verification credits left (verification links) | | `404` | `101` | The session or link doesn't exist, or belongs to another business | | `409` | `105` | The action doesn't fit the current state, e.g. cancelling an approved link, or the EU wallet for a session that is no longer pending | | `500` | `105` | Something went wrong at IDfier | Branch on the HTTP status. `data.error` is a message for your logs, not meant to be matched on. ## Retrying [#retrying] - `5xx` and network errors: retry with a short backoff. - `4xx`: don't retry the same request; fix it first. A `404` for a login session usually means the `referenceId` is wrong or belongs to another business client. ## Session outcomes aren't errors [#session-outcomes-arent-errors] A session that exists returns `200`, with its state in `data.session.status`: `pending` and `started` mean keep polling, `denied` and `error` mean stop, and a `pending` or `started` session past `expiresAt` has expired. See [Login with IDfier](/docs/identity/login#steps). # EU Digital Identity Wallet (/docs/identity/eu-wallet) You receive the same fields, with the same names, as for an IDfier user. Nothing changes in how you read results. EU wallet support is turned on by IDfier. Ask [support@idfier.com](mailto:support@idfier.com) whether it is available for your business client. ## In your login flow [#in-your-login-flow] After [`POST /business/auth/request`](/docs/identity/login), you can also show a wallet code: ```bash curl -X POST https://api.idfier.com/api/v3/business/auth/wallet \ -H "clientid: $IDFIER_CLIENT_ID" \ -H "clientsecret: $IDFIER_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"referenceId":"550e8400-e29b-41d4-a716-446655440000"}' ``` ```json { "status": 100, "data": { "walletUrl": "haip-vp://?client_id=…&request_uri=…", "expiresAt": "2026-10-31T10:05:00.000Z" } } ``` Show `walletUrl` as a QR code next to the IDfier code or, on a phone, as a link: it opens the customer's wallet. Calling it again while the request is open returns the same `walletUrl`. A session that is no longer `pending` returns `409`. Then poll `POST /business/auth/result` as usual. When the customer shared from their wallet, `session.method` is `eudi_wallet`: ```json { "user": { "id": "u_…", "fullname": "Anna Lindqvist", "ageOver18": true }, "session": { "status": "approved", "method": "eudi_wallet" } } ``` The `authentication.completed` [webhook](/docs/identity/webhooks) also carries `"method": "eudi_wallet"`. A wallet answer only approves a session that is still `pending`: if the customer already scanned it with the IDfier app, the wallet answer is discarded. ## With verification links [#with-verification-links] The link page offers "Or use your EU Digital Identity Wallet" by itself. `GET /business/links/:id` returns `method` alongside `result`. ## Fields [#fields] | Scope | From the wallet's PID | Notes | | --- | --- | --- | | `fullname` | `given_name` + `family_name` | | | `dateOfBirth` | `birthdate` | | | `age`, `ageOver*`, `ageBetween*` | `birthdate` | Only the answer reaches you | | `nationality` | `nationalities[0]` | | | `country` | `address.country` | Optional in the PID | | `gender` | `sex` | `M` / `F`; other values are left out | | `email` | `email` | Optional in the PID | | `personalNo` | `personal_administrative_number` | Optional in the PID | | `documentType` | | `"EU Digital Identity Wallet"` | | `authLevel` | | `"EU Wallet"` | | `handle` | | Not available | A field the wallet's PID doesn't contain is missing from `user`. `id` is per business, as for IDfier users. It is stable for a person when `personalNo` is among your scopes; otherwise it is tied to the wallet's credential and can change when the wallet renews its PID. ## How IDfier verifies a wallet [#how-idfier-verifies-a-wallet] 1. IDfier builds an OpenID4VP 1.0 request (HAIP profile) for the PID (`urn:eudi:pid:1`, SD-JWT VC), asking only for the claims your scopes need. 2. The wallet shows the customer who is asking and what, and sends back an encrypted response. 3. IDfier checks the issuer's certificate chain against the PID providers on the EU trusted list, the issuer's signature, expiry, every selective disclosure, the key binding to this request, and revocation. 4. The session is approved and you read the result as usual. IDfier keeps the verified claims encrypted, for a limited time. # Getting started (/docs/identity/getting-started) ## Get credentials [#get-credentials] Write to [support@idfier.com](mailto:support@idfier.com) for a business client. IDfier sets which customer data the client may receive (its [scopes](/docs/identity/scopes)) and shows the `clientSecret` once: store it in your server's secret store. You need a backend that can make HTTPS requests, and a page that can show a QR code (or, on phones, a button that opens the IDfier app). ## Base URL [#base-url] ``` https://api.idfier.com/api/v3 ``` ## Authenticate [#authenticate] Send the credentials as headers on every request: ```http clientid: your-client-id clientsecret: your-client-secret Content-Type: application/json ``` Never put `clientSecret` in a mobile app, in browser code, or in an environment variable that reaches the browser (such as `NEXT_PUBLIC_…` in Next.js). ## Responses [#responses] The API uses standard HTTP status codes: `200` or `201` on success, `4xx` when the request can't be done, `5xx` when something went wrong at IDfier. The body is JSON with a numeric `status` and `data`: ```json { "status": 100, "data": { "referenceId": "550e8400-e29b-41d4-a716-446655440000" } } ``` Errors carry the reason in `data.error`: ```json { "status": 101, "data": { "error": "Session not found" } } ``` Branch on the HTTP status; see [Errors](/docs/identity/errors) for every code. ## Make a first call [#make-a-first-call] ```bash curl -X POST https://api.idfier.com/api/v3/business/auth/request \ -H "clientid: $IDFIER_CLIENT_ID" \ -H "clientsecret: $IDFIER_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{}' ``` A `201` with a `referenceId` means your credentials work. ## Testing [#testing] There is no sandbox yet: testing uses the production API, the IDfier app and a real, verified IDfier profile (your own). Ask IDfier for a separate business client for testing. ## Next [#next] # Identity API (/docs/identity) The Identity API gives your server verified data about a customer, after the customer approves in the IDfier app. There are two ways to ask: | | [Login with IDfier](/docs/identity/login) | [Verification links](/docs/identity/verification-links) | | --- | --- | --- | | How the customer verifies | Scans a QR code on your site, or taps a button on their phone | Opens a link you send (IDfier can email it) | | What they share | Your business client's scopes | Chosen per link | | How you get the answer | Poll the result, or get a [webhook](/docs/identity/webhooks) and fetch it | Poll the link | | Good for | Sign-up, login, checkout | Checking someone before a booking, rental or delivery | [Age verification](/docs/identity/age-verification) works with both: ask for an age scope such as `ageOver18` and you get `true` or `false`, never the date of birth. ## Key concepts [#key-concepts] - **Business client**: your company's credentials (`clientId`, `clientSecret`) and the [scopes](/docs/identity/scopes) it may receive. IDfier sets it up. - **Session**: one login, from QR code to approval, valid for 5 minutes, identified by its `referenceId`. - **Scopes**: which customer fields you receive, e.g. `fullname`, `email`, `ageOver18`, `authLevel`. - **`id`**: a stable identifier for a customer, unique to your business. Key your customer accounts on it. ## Start here [#start-here] Credentials, base URL, responses and testing. The integration guide, step by step. Every endpoint, generated from the OpenAPI spec. # Login with IDfier (/docs/identity/login) This is how IDfier's own products, IDfier Sign and Rezervoj, log people in. The [Next.js example](/docs/identity/login/nextjs) has the complete code. ## Flow [#flow] ``` Browser Your server IDfier API │ "Log in" │ │ │ ────────────────────────▶│ POST /business/auth/request │ │ │ ─────────────────────────────▶│ │ │ ◀── referenceId, qrReferenceSecret, createdAt │ ◀── the three values ────│ (and a cookie with referenceId) │ │ │ │ shows the QR code, refreshed every 2 s │ │ (on a phone: a button that opens the IDfier app) │ │ │ │ │ polls your server ──────▶│ POST /business/auth/result │ │ │ ─────────────────────────────▶│ │ │ ◀── session.status │ │ ... customer approves in the IDfier app ... │ │ "approved" ─────────────▶│ reads the result once more, │ │ │ checks the cookie, logs in │ │ ◀── your session ────────│ │ ``` ## Steps [#steps] ### Start a session (server) [#start-a-session-server] Your server calls [`POST /business/auth/request`](/docs/identity/reference/login/createLoginSession) and returns `referenceId`, `qrReferenceSecret` and `createdAt` to the browser. Also set an **httpOnly cookie with the `referenceId`**. Only the browser that started the login may finish it (step 4). Without this, anyone who learns a `referenceId`, for example by seeing the QR code over someone's shoulder, could finish that customer's login in their own browser. ### Show the QR code (browser) [#show-the-qr-code-browser] Build the QR payload from the three values and refresh it every ~2 seconds: the payload contains the seconds since `createdAt`, and the app accepts only a small time window. The format is in [QR code payload](/docs/identity/login/qr-code). On a phone, show a button instead that opens the app with the same payload: `https://www.idfier.com/auth?authData=`. ### Wait for approval (browser → your server → IDfier) [#wait-for-approval-browser--your-server--idfier] Poll your own server, which calls [`POST /business/auth/result`](/docs/identity/reference/login/getLoginResult). Start at about every 2 seconds and back off to about 10: | `session.status` | Do | | --- | --- | | `pending` | Not scanned yet; keep polling | | `started` | Scanned; stop refreshing the QR code and ask the customer to approve | | `approved` | Go to step 4 | | `denied`, `error` | Stop and tell the customer | | `expiresAt` passed | The session expired (after 5 minutes); offer a new one | Or set a [webhook](/docs/identity/webhooks) when you create the session. ### Log the customer in (server) [#log-the-customer-in-server] When the browser reports `approved`, don't take the customer's data from the browser. On your server: 1. Check that the `referenceId` matches the cookie from step 1. 2. Call `POST /business/auth/result` and check that `data.session.status` is `approved`. 3. Use `data.user` from that response: find or create the customer by `user.id`, and issue your own session. 4. Delete the cookie, so the login can't be reused. ## The result [#the-result] ```json { "status": 100, "data": { "user": { "id": "u_3f9a2c…", "fullname": "Anna Lindqvist", "email": "anna@example.com" }, "session": { "referenceId": "550e8400-e29b-41d4-a716-446655440000", "status": "approved", "method": "idfier", "expiresAt": "2026-10-31T10:05:00.000Z", "createdAt": "2026-10-31T10:00:00.000Z" } } } ``` - `user` is `null` until the session is `approved`. It always has `id`, plus the fields in your [scopes](/docs/identity/scopes) that IDfier has for the customer. - `session.method` is `idfier` (the IDfier app) or `eudi_wallet` (an [EU Digital Identity Wallet](/docs/identity/eu-wallet)), once approved. - A session belongs to the first person who scans it: only they can approve or deny it, and only before it expires. Its scopes are fixed when it is created, so the result has exactly what the customer was shown and approved. ## Recognise returning customers [#recognise-returning-customers] `user.id` is stable for a customer within your business, and different for every other business. Key the customer's account on it, not on email or name: a customer may not share them, and they can change. ## Checklist [#checklist] - [ ] `clientSecret` only on the server - [ ] The QR code refreshes every 2 seconds; a phone gets a button instead - [ ] The result is read on the server, never taken from the browser - [ ] The `referenceId` is tied to the browser with an httpOnly cookie - [ ] Accounts are keyed on `user.id` - [ ] Expired, denied and failed sessions offer a way to start again # Next.js example (/docs/identity/login/nextjs) Next.js App Router, with the [`qrcode`](https://www.npmjs.com/package/qrcode) package for the QR image. Set `IDFIER_CLIENT_ID` and `IDFIER_CLIENT_SECRET` as server-only environment variables. Each step is explained in the [integration guide](/docs/identity/login). ## Server: the IDfier client [#server-the-idfier-client] ```ts // lib/idfier.ts (server only) const IDFIER_API = "https://api.idfier.com/api/v3"; async function idfier(path: string, body: unknown): Promise { const response = await fetch(`${IDFIER_API}${path}`, { method: "POST", headers: { "Content-Type": "application/json", clientid: process.env.IDFIER_CLIENT_ID!, clientsecret: process.env.IDFIER_CLIENT_SECRET!, }, body: JSON.stringify(body), cache: "no-store", }); const json = await response.json().catch(() => null); if (!response.ok) { throw new Error(`IDfier ${response.status}: ${json?.data?.error ?? "request failed"}`); } return json.data as T; } export type IdfierSession = { referenceId: string; qrReferenceSecret: string; createdAt: string }; export type IdfierResult = { // id is always there; the other fields depend on your scopes user: ({ id: string } & Record) | null; session: { referenceId: string; status: "pending" | "started" | "approved" | "denied" | "error"; method: "idfier" | "eudi_wallet" | null; expiresAt: string; createdAt: string; }; }; export const createLoginSession = () => idfier("/business/auth/request", {}); export const getLoginResult = (referenceId: string) => idfier("/business/auth/result", { referenceId }); export const LOGIN_COOKIE = "idfier_login_ref"; ``` ## Server: start, poll and finish [#server-start-poll-and-finish] ```ts // app/api/idfier/start/route.ts import { NextResponse } from "next/server"; import { createLoginSession, LOGIN_COOKIE } from "@/lib/idfier"; export async function POST() { const session = await createLoginSession(); const response = NextResponse.json(session); // Only this browser may finish this login (see /finish) response.cookies.set(LOGIN_COOKIE, session.referenceId, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/api/idfier", maxAge: 60 * 10, }); return response; } ``` ```ts // app/api/idfier/status/route.ts: what the browser polls. Returns only the // status; the customer's data stays on the server. import { NextRequest, NextResponse } from "next/server"; import { getLoginResult, LOGIN_COOKIE } from "@/lib/idfier"; export async function GET(request: NextRequest) { const referenceId = request.cookies.get(LOGIN_COOKIE)?.value; if (!referenceId) return NextResponse.json({ status: "missing" }, { status: 400 }); const { session } = await getLoginResult(referenceId); const expired = new Date(session.expiresAt) < new Date() && session.status !== "approved"; return NextResponse.json({ status: expired ? "expired" : session.status }); } ``` ```ts // app/api/idfier/finish/route.ts: logs the customer in once approved import { NextRequest, NextResponse } from "next/server"; import { getLoginResult, LOGIN_COOKIE } from "@/lib/idfier"; export async function POST(request: NextRequest) { const referenceId = request.cookies.get(LOGIN_COOKIE)?.value; if (!referenceId) { return NextResponse.json({ error: "Start the login again" }, { status: 400 }); } // Read the result here, never from the browser const { user, session } = await getLoginResult(referenceId); if (session.status !== "approved" || !user) { return NextResponse.json({ error: "The login is not approved" }, { status: 401 }); } // Find or create the customer by user.id, then start your own session. // await signIn(await upsertCustomer({ idfierId: user.id, name: user.fullname })); const response = NextResponse.json({ ok: true }); response.cookies.delete({ name: LOGIN_COOKIE, path: "/api/idfier" }); return response; } ``` ## Browser: the QR code and polling [#browser-the-qr-code-and-polling] ```tsx // components/idfier-login.tsx "use client"; import QRCode from "qrcode"; import { useEffect, useState } from "react"; type Session = { referenceId: string; qrReferenceSecret: string; createdAt: string }; // The QR payload (see /docs/identity/login/qr-code) async function payload(session: Session) { const qrTime = String(Math.floor((Date.now() - new Date(session.createdAt).getTime()) / 1000)); const encoder = new TextEncoder(); const keyBytes = await crypto.subtle.digest("SHA-256", encoder.encode(session.qrReferenceSecret)); const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(qrTime))); const code = Array.from(mac, (byte) => byte.toString(16).padStart(2, "0")).join(""); return `idfier.${session.referenceId}.${qrTime}.${code}`; } export function IdfierLogin({ onLoggedIn }: { onLoggedIn: () => void }) { const [session, setSession] = useState(null); const [qr, setQr] = useState(""); const [appLink, setAppLink] = useState(""); const [status, setStatus] = useState("starting"); async function start() { setStatus("pending"); setSession(await (await fetch("/api/idfier/start", { method: "POST" })).json()); } useEffect(() => { void start(); }, []); // Refresh the QR code every 2 seconds until it has been scanned useEffect(() => { if (!session || status !== "pending") return; const render = async () => { const data = await payload(session); setQr(await QRCode.toDataURL(`idfier://auth?authData=${encodeURIComponent(data)}`)); setAppLink(`https://www.idfier.com/auth?authData=${encodeURIComponent(data)}`); }; void render(); const timer = setInterval(render, 2000); return () => clearInterval(timer); }, [session, status]); // Poll with backoff: 2 s, growing to 10 s useEffect(() => { if (!session) return; let delay = 2000; let timer: ReturnType; let stopped = false; const poll = async () => { const { status: next } = await (await fetch("/api/idfier/status")).json(); if (stopped) return; setStatus(next); if (next === "approved") { const finished = await fetch("/api/idfier/finish", { method: "POST" }); if (finished.ok) onLoggedIn(); return; } if (next === "pending" || next === "started") { delay = Math.min(delay * 1.5, 10000); timer = setTimeout(poll, delay); } }; timer = setTimeout(poll, delay); return () => { stopped = true; clearTimeout(timer); }; }, [session, onLoggedIn]); if (status === "started") return

Scanned. Approve the login in the IDfier app.

; if (status === "denied" || status === "error" || status === "expired") { return ; } return (
{qr ? Scan with the IDfier app : null} {/* On a phone: open the IDfier app directly */} {appLink ? Open the IDfier app : null}
); } ``` `crypto.subtle` needs a secure context (`https`, or `localhost` in development). # QR code payload (/docs/identity/login/qr-code) The API doesn't return a QR code or an app link: you build both in the browser from the three values of the session (`referenceId`, `qrReferenceSecret`, `createdAt`), and render the QR code again every ~2 seconds. ``` qrTime = floor((now - createdAt) / 1000) // seconds, as a decimal string key = SHA256(qrReferenceSecret) // the raw 32 bytes qrAuthCode = hex(HMAC_SHA256(key, qrTime)) // lowercase hex payload = "idfier.{referenceId}.{qrTime}.{qrAuthCode}" ``` | Where | Content | | --- | --- | | QR code | `idfier://auth?authData=` | | Button on a phone | `https://www.idfier.com/auth?authData=` (a universal link that opens the app) | URL-encode the payload in both. IDfier accepts a `qrTime` within about 10 seconds of its own clock, which is why the code has to be refreshed. ## TypeScript (browser) [#typescript-browser] ```ts type Session = { referenceId: string; qrReferenceSecret: string; createdAt: string }; export async function qrPayload(session: Session) { const qrTime = String(Math.floor((Date.now() - new Date(session.createdAt).getTime()) / 1000)); const encoder = new TextEncoder(); const keyBytes = await crypto.subtle.digest('SHA-256', encoder.encode(session.qrReferenceSecret)); const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(qrTime))); const code = Array.from(mac, (byte) => byte.toString(16).padStart(2, '0')).join(''); return `idfier.${session.referenceId}.${qrTime}.${code}`; } ``` `crypto.subtle` needs a secure context: `https`, or `localhost` in development. ## Python [#python] ```python import hashlib, hmac, math, time from datetime import datetime def qr_payload(reference_id: str, qr_reference_secret: str, created_at: str) -> str: created = datetime.fromisoformat(created_at.replace("Z", "+00:00")).timestamp() qr_time = str(math.floor(time.time() - created)) key = hashlib.sha256(qr_reference_secret.encode()).digest() code = hmac.new(key, qr_time.encode(), hashlib.sha256).hexdigest() return f"idfier.{reference_id}.{qr_time}.{code}" ``` The browser needs it to draw the QR code, but don't log or store it: with it, anyone can produce a valid code for the session. # Overview (/docs/identity/reference) Base URL `https://api.idfier.com/api/v3`. Send `clientid` and `clientsecret` headers on every request ([Getting started](/docs/identity/getting-started)). The machine-readable spec is [`/openapi/identity.yaml`](/openapi/identity.yaml). ## Login [#login] | Endpoint | | | --- | --- | | [`POST /business/auth/request`](/docs/identity/reference/login/createLoginSession) | Create a login session | | [`POST /business/auth/result`](/docs/identity/reference/login/getLoginResult) | Get a session's status and result | | [`POST /business/auth/wallet`](/docs/identity/reference/login/startWalletLogin) | Offer an EU Digital Identity Wallet | ## Verification links [#verification-links] | Endpoint | | | --- | --- | | [`POST /business/links`](/docs/identity/reference/verification-links/createLink) | Create a link | | [`GET /business/links`](/docs/identity/reference/verification-links/listLinks) | List links | | [`GET /business/links/:id`](/docs/identity/reference/verification-links/getLink) | Get a link and its result | | [`POST /business/links/:id/cancel`](/docs/identity/reference/verification-links/cancelLink) | Cancel an open link | | [`POST /business/links/:id/resend`](/docs/identity/reference/verification-links/resendLink) | Issue a new URL and resend | | [`DELETE /business/links/:id`](/docs/identity/reference/verification-links/deleteLink) | Delete a link | # Scopes (/docs/identity/scopes) Your business client's **scopes** decide which fields you receive. IDfier sets them up, for example `fullname,email,ageOver18,authLevel`. A login session uses all of them; a [verification link](/docs/identity/verification-links) can ask for fewer. Every field except `id` is sent only if it is in your scopes **and** IDfier has it for the customer. Treat a missing field as unknown. ## The `id` field [#the-id-field] Always included. `id` is a stable identifier for the customer **within your business**: the same customer always gets the same `id` from you, and a different `id` from every other business. Use it to recognise returning customers. It is not IDfier's internal user id and can't be used to match customers across businesses. ## Identity [#identity] | Field | Type | Scope | | --- | --- | --- | | `fullname` | string | `fullname` | | `handle` | string | `handle` | | `email` | string | `email` | | `personalNo` | string | `personalNo` | ## Age and birth [#age-and-birth] | Field | Type | Scope | | --- | --- | --- | | `age` | number | `age`: whole years today, computed from the verified date of birth | | `dateOfBirth` | string | `dateOfBirth` | | `ageOver13`, `ageOver16`, `ageOver18`, `ageOver21`, `ageOver65` | boolean | The same name | | `ageBetween{min}And{max}` | boolean | The same name, e.g. `ageBetween18And25` | The age checks answer yes or no without revealing the date of birth or age; see [Age verification](/docs/identity/age-verification). ## Document and location [#document-and-location] | Field | Type | Scope | | --- | --- | --- | | `documentType` | string | `documentType` | | `gender` | string | `gender` | | `nationality` | string | `nationality` | | `country` | string | `country` | ## How the customer was verified [#how-the-customer-was-verified] | Field | Type | Scope | | --- | --- | --- | | `authLevel` | string | `authLevel` | | `authLevel` | Verified with | | --- | --- | | `Gold` | ID document with its chip read (NFC), or an IDfier profile created from an EU Digital Identity Wallet | | `Silver` | BankID | | `Bronze` | ID document scan without the chip | | `EU Wallet` | Shared directly from an EU Digital Identity Wallet | | `Unknown` | None of the above | For customers sharing from an EU wallet, see which fields the wallet provides in [EU Digital Identity Wallet](/docs/identity/eu-wallet#fields). # Verification links (/docs/identity/verification-links) The customer opens the link on idfier.com, approves in the IDfier app (or their EU Digital Identity Wallet), and you read the result. Each link uses one verification credit. The endpoints use the same `clientid` / `clientsecret` headers as the rest of the [Identity API](/docs/identity/getting-started). ## Create a link [#create-a-link] ```bash curl -X POST https://api.idfier.com/api/v3/business/links \ -H "clientid: $IDFIER_CLIENT_ID" \ -H "clientsecret: $IDFIER_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{ "recipientName": "Anna Lindqvist", "recipientEmail": "anna@example.com", "message": "Please verify before we sign the lease.", "scopes": ["fullname", "ageOver18"], "expiresInDays": 7 }' ``` | Field | Description | | --- | --- | | `recipientName` | Optional, shown on the link page and in the email (max 200) | | `recipientEmail` | Optional. If set, IDfier emails the link unless `sendEmail` is `false` | | `message` | Optional message to the customer (max 1000) | | `scopes` | What to verify. Defaults to all your scopes; must be among them. Age checks are allowed if you have any age scope, `age` or `dateOfBirth` | | `expiresInDays` | 1–30, default 7 | | `sendEmail` | Default `true` | | `createdBy` | Optional, your own note of who created it | `201 Created`: ```json { "status": 100, "data": { "id": "…", "url": "https://www.idfier.com/v/…", "expiresAt": "2026-11-07T10:00:00.000Z", "emailSent": true } } ``` The `url` is only returned here and on resend: IDfier stores only a hash of it. With no credits left the response is `402`; scopes your business may not use give `400`. ## Read the result [#read-the-result] Poll [`GET /business/links/:id`](/docs/identity/reference/verification-links/getLink) until the status is final: ```json { "status": 100, "data": { "id": "…", "status": "approved", "scopes": ["fullname", "ageOver18"], "method": "idfier", "result": { "id": "u_3f9a…", "fullname": "Anna Lindqvist", "ageOver18": true } } } ``` `result` is set once the customer has approved. It has exactly the requested scopes plus an `id` unique to your business (see [Scopes](/docs/identity/scopes)). `method` says how they verified: `idfier` or `eudi_wallet`. | Status | Meaning | | --- | --- | | `sent` | Created, not opened yet | | `opened` | The customer opened the link page | | `approved` | The customer approved; `result` is available | | `denied` | The customer declined | | `cancelled` | You cancelled it | | `expired` | Not completed before `expiresAt` | To list links, `GET /business/links?limit=50` returns `{ links, nextBefore }`, newest first; pass `nextBefore` as `before` for the next page. ## Cancel, resend, delete [#cancel-resend-delete] - `POST /business/links/:id/cancel` closes an open link. - `POST /business/links/:id/resend`, with an optional `{ "expiresInDays": 7 }`, issues a new URL (the old one stops working), extends the expiry and emails it again if there is a `recipientEmail`. Works on `sent`, `opened` and `expired` links. - `DELETE /business/links/:id` deletes the link and its record of the customer. Cancelling or resending a link in a state that doesn't allow it (for example an approved link) returns `409`; a link that doesn't exist or isn't yours returns `404`. ## The link page [#the-link-page] `https://www.idfier.com/v/` shows who is asking and what will be shared. On a computer it shows a QR code to scan with the IDfier app; on a phone it opens the app. When EU wallets are enabled it also offers "Or use your EU Digital Identity Wallet". # Webhooks (/docs/identity/webhooks) Set a webhook when you create a login session: ```json title="POST /business/auth/request" { "webhookUrl": "https://example.com/idfier/webhook", "webhookSecret": "a-random-string-of-at-least-16-characters" } ``` | Field | Description | | --- | --- | | `webhookUrl` | An `https` URL, at most 500 characters | | `webhookSecret` | Required with `webhookUrl`, 16–255 characters; signs each event | ## Events [#events] IDfier `POST`s JSON to your URL: ```json { "event": "authentication.completed", "referenceId": "550e8400-…", "status": "approved" } ``` | `event` | `status` | When | | --- | --- | --- | | `authentication.started` | `started` | The customer scanned the QR code | | `authentication.completed` | `approved` | The customer approved. From an EU wallet, the event also has `"method": "eudi_wallet"` | | `authentication.denied` | `denied` | The customer declined | An event never contains the customer's data. Fetch it with [`POST /business/auth/result`](/docs/identity/login), as you would when polling, and keep the [browser binding](/docs/identity/login#steps) of your login flow. ## Verify the signature [#verify-the-signature] Each request has an `x-idfier-signature: sha256=` header: the HMAC-SHA256 of the raw request body with your `webhookSecret`. Check it before trusting the event: ```ts import { createHmac, timingSafeEqual } from 'node:crypto'; export function isFromIdfier(rawBody: string, header: string | null, secret: string) { const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`; return header !== null && header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected)); } ``` Compute it over the body exactly as received, before parsing the JSON. ## Delivery [#delivery] - Answer with a `2xx` within 15 seconds. - On a timeout, a network error or a `5xx`, IDfier tries again after 2 and 4 seconds: at most 3 attempts in all. A `4xx` is not retried. - Events can arrive late or not at all, so treat them as a hint: the result endpoint is the source of truth, and polling is a fine fallback. # Build with IDfier (/docs) IDfier verifies people once, with their ID document (chip read with NFC, or a scan) and a face check, and keeps a reusable verified profile in the IDfier app. Your product asks for what it needs; the customer approves in the app, and you receive exactly that. ## Products [#products] } title="Login with IDfier" href="/docs/identity/login"> Customers scan a QR code (or tap a button on their phone) and you log them in with verified data. } title="Age verification" href="/docs/identity/age-verification"> "Over 18" as a yes or no, without ever seeing the date of birth. } title="Verification links" href="/docs/identity/verification-links"> Send a customer a link to verify, with no integration in your own UI. } title="EU Digital Identity Wallet" href="/docs/identity/eu-wallet"> Let customers share from their EU wallet instead of the IDfier app. } title="IDfier Sign API" href="/docs/sign"> Contracts your users sign with a verified identity, inside your product's flow. } title="Build with AI" href="/docs/ai"> These docs as plain Markdown and `llms.txt`, for coding assistants. ## Which API do I need? [#which-api-do-i-need] | You want to | Use | Credentials | | --- | --- | --- | | Log customers in, or check who they are | [Identity API](/docs/identity): [Login](/docs/identity/login) | A business client from IDfier | | Check a customer's age | [Identity API](/docs/identity): [Age verification](/docs/identity/age-verification) | A business client from IDfier | | Verify someone you aren't online with | [Identity API](/docs/identity): [Verification links](/docs/identity/verification-links) | A business client from IDfier | | Have customers sign contracts | [IDfier Sign API](/docs/sign) | An API key from IDfier Sign | The two APIs are separate: the Identity API runs at `api.idfier.com` with a `clientid` and `clientsecret`, the IDfier Sign API at `sign.idfier.com` with an API key. ## Help [#help] Write to [support@idfier.com](mailto:support@idfier.com) for credentials, scopes or anything these docs don't answer. # IDfier Sign API (/docs/sign) One REST call creates the contract; IDfier verifies every signer and seals the result. It is built for platforms whose own customers sign: marketplaces, rentals, staffing. Your users don't need an IDfier Sign account of their own. ## How it works [#how-it-works] ### Create the contract [#create-the-contract] `POST` the PDF with the people who should sign it. You get each signer's personal link back. ### Signers verify and sign [#signers-verify-and-sign] They open the link, prove who they are with IDfier, read the document and sign. There is nothing to install on your side. ### They come back to you [#they-come-back-to-you] With a `returnUrl`, signers land back in your product, with the document id and the outcome in the URL. ### Collect the result [#collect-the-result] Poll the document until it is `completed`, then download the sealed PDF and its certificate. ## Base URL and access [#base-url-and-access] ``` https://sign.idfier.com/api/v1 ``` The API is part of the Business, Enterprise and API plans. A company owner or admin creates a key in IDfier Sign under **Company → API**; send it as `Authorization: Bearer isk_live_…`. See [Requests](/docs/sign/requests). Your first signed contract in four calls. Every endpoint, from the OpenAPI spec. Set up the API plan for your platform. # Quickstart (/docs/sign/quickstart) ### Get an API key [#get-an-api-key] In IDfier Sign, a company owner or admin creates a key under **Company → API**. They choose which person the key acts as and, if you like, one workspace it is limited to. The key is shown once: keep it on your server. ```bash export IDFIER_SIGN_KEY=isk_live_… ``` ### Create the contract [#create-the-contract] Upload the PDF with the signers: ```bash curl https://sign.idfier.com/api/v1/documents \ -H "Authorization: Bearer $IDFIER_SIGN_KEY" \ -H "Idempotency-Key: booking-1042" \ -F file=@contract.pdf \ -F title="Babysitting agreement" \ -F reference=booking-1042 \ -F returnUrl=https://yourapp.example/bookings/1042 \ -F 'signers=[{"email":"parent@example.com"},{"email":"sitter@example.com"}]' \ -F send=true ``` - `send=true` emails the signers their invitation. Leave it out to show each `signingUrl` in your own product instead. - `reference` is your own id for the document, e.g. a booking. - `returnUrl` is where signers go afterwards. `201 Created`: ```json { "id": "6654…", "title": "Babysitting agreement", "reference": "booking-1042", "status": "in_signature", "verificationMethods": ["idfier"], "signers": [ { "email": "parent@example.com", "status": "pending", "signingUrl": "https://sign.idfier.com/sign/e3cf…" }, { "email": "sitter@example.com", "status": "pending", "signingUrl": "…" } ], "delivery": { "sent": ["parent@example.com", "sitter@example.com"], "notSent": [] } } ``` ### Signers verify and sign [#signers-verify-and-sign] Each signer opens their link, verifies with IDfier, reads the contract and signs on one focused page. Then they return to your `returnUrl`: ``` https://yourapp.example/bookings/1042?idfiersign_document=6654…&idfiersign_status=signed ``` See [Signers and the signing page](/docs/sign/signing). ### Collect the signed PDF [#collect-the-signed-pdf] Look the document up by your reference (or by id) until its `status` is `completed`, then download `/signed-file`, sealed with the signers' verified identities, and `/certificate`: ```ts const headers = { Authorization: `Bearer ${process.env.IDFIER_SIGN_KEY}` }; const res = await fetch('https://sign.idfier.com/api/v1/documents?reference=booking-1042', { headers }); const { data } = await res.json(); if (data[0]?.status === 'completed') { const pdf = await fetch(`https://sign.idfier.com/api/v1/documents/${data[0].id}/signed-file`, { headers }); // Store the sealed PDF with the booking. } ``` # Overview (/docs/sign/reference) Base URL `https://sign.idfier.com/api/v1`. Send `Authorization: Bearer isk_live_…` on every request ([Requests](/docs/sign/requests)). The machine-readable spec is [`/openapi/sign.yaml`](/openapi/sign.yaml). | Endpoint | | | --- | --- | | [`POST /documents`](/docs/sign/reference/post-documents) | Upload a document, optionally with signers | | [`GET /documents`](/docs/sign/reference/get-documents) | List documents, newest first | | [`GET /documents/{id}`](/docs/sign/reference/get-documents-id) | A document, its signers and their progress | | [`POST /documents/{id}/signers`](/docs/sign/reference/post-documents-id-signers) | Add signers or viewers | | [`POST /documents/{id}/send`](/docs/sign/reference/post-documents-id-send) | Email the invitations that haven't gone out | | [`POST /documents/{id}/void`](/docs/sign/reference/post-documents-id-void) | Cancel a document that isn't completed | | [`GET /documents/{id}/file`](/docs/sign/reference/get-documents-id-file) | The original upload | | [`GET /documents/{id}/signed-file`](/docs/sign/reference/get-documents-id-signed-file) | The signed document | | [`GET /documents/{id}/certificate`](/docs/sign/reference/get-documents-id-certificate) | The signing certificate | | [`GET /templates`](/docs/sign/reference/get-templates) | Active templates | | [`GET /workspaces`](/docs/sign/reference/get-workspaces) | Workspaces the key can use | | [`GET /usage`](/docs/sign/reference/get-usage) | Verified signatures this month | # Requests (/docs/sign/requests) ## Authentication [#authentication] Company owners and admins create keys in IDfier Sign under **Company → API**. Send one on every request: ```http Authorization: Bearer isk_live_… ``` A key is shown once and can be revoked at any time. Keep it on your server. ## Permissions and workspaces [#permissions-and-workspaces] A key acts as one person in your company and can do exactly what they can. Limit a key to one workspace to keep integrations apart. `GET /workspaces` lists the workspaces a key can use, with its role in each. ## Safe retries [#safe-retries] Send an `Idempotency-Key` header (any unique string, at most 255 characters) on `POST`s. A retry with the same key returns the first response, marked `Idempotent-Replayed: true`, instead of creating a second document. ```bash curl https://sign.idfier.com/api/v1/documents \ -H "Authorization: Bearer $IDFIER_SIGN_KEY" \ -H "Idempotency-Key: booking-1042" \ -F file=@contract.pdf ``` Reusing a key for a different request gives `422 idempotency_key_reused`; a retry while the first is still running gives `409 request_in_progress`. ## Paging and lookup [#paging-and-lookup] Lists are newest first, 25 at a time (up to 100 with `limit`), with a `nextCursor`: pass it as `cursor` for the next page. Store your own id in `reference` when creating a document, and find it again with `GET /documents?reference=…`. ## Limits [#limits] - 120 requests per minute per key. Over that you get `429 rate_limited`, with `Retry-After`. - Uploads up to 20 MB (`413 file_too_large`). ## Errors [#errors] Every error has the same shape, with a stable `code` to branch on: ```json { "error": { "code": "document_not_open", "message": "…" } } ``` | HTTP | Codes and meaning | | --- | --- | | `400` | Invalid input | | `401` | Missing, wrong or revoked key | | `403` | Not allowed: `plan_has_no_api`, `acting_user_removed`, `not_allowed`, `workspace_read_only` | | `404` | Not found, or out of the key's reach | | `409` | Conflict: `request_in_progress`, `not_completed` | | `413` | `file_too_large` | | `422` | Can't be done in the document's state, or `idempotency_key_reused` | | `429` | `rate_limited` | | `500` | `internal_error` | # Signers and the signing page (/docs/sign/signing) ## Signers and viewers [#signers-and-viewers] Each entry in `signers` is a `SignerInput`: | Field | Description | | --- | --- | | `email` | Required | | `role` | `signer` (default) or `viewer`, who can read but doesn't sign | | `personalNumber` | Optional. Only the person with this personal identity number, as verified, can sign | | `message` | Optional message to this signer | Add more later with [`POST /documents/{id}/signers`](/docs/sign/reference/post-documents-id-signers). With a `templateId`, `signers` replaces the template's recipients. ## Delivering the links [#delivering-the-links] Two ways, per document: - **IDfier emails them**: `send=true` when creating, or `POST /documents/{id}/send` later. The response's `delivery` says who was emailed (`sent`) and who wasn't, with a reason (`notSent`). - **You show them**: each signer has a personal `signingUrl`. Put it behind a button in your own product, or in your own email. A `signingUrl` is personal: give each signer only their own. ## Verification [#verification] Signers verify with IDfier: an ID document and a face check in the IDfier app. Each document lists its `verificationMethods`; your company chooses them in IDfier Sign under **Company → API**. ## The signing page [#the-signing-page] The signer verifies, reads the document and signs (or declines) on one focused page, without IDfier Sign's workspace around it. ## Coming back: `returnUrl` [#coming-back-returnurl] With a `returnUrl` (an `https` URL), signers go back to your product afterwards, with two parameters added: | Parameter | Value | | --- | --- | | `idfiersign_document` | The document id | | `idfiersign_status` | `signed`, `declined` or `viewed` | ``` https://yourapp.example/bookings/1042?idfiersign_document=6654…&idfiersign_status=signed ``` Use it to show the right screen, not as proof: anyone can open that URL. Check the document's status with the API before acting on it. ## Following the document [#following-the-document] | Document `status` | Meaning | | --- | --- | | `draft` | Created, invitations not sent | | `in_signature` | Waiting for signers | | `completed` | Everyone signed; `/signed-file` and `/certificate` are available | | `declined` | A signer declined | | `void` | Cancelled with `POST /documents/{id}/void` | Each signer has its own `status`: `pending`, `accepted`, `signed`, `declined` or `revoked`, and `signingMethod` once signed. `/signed-file` returns `409` until the document is `completed`. # Usage and pricing (/docs/sign/usage) On the API plan you pay for **verified signatures**: each signature on your documents by a signer who verified their identity. The plan includes a monthly allowance; signatures beyond it are charged per signature. Signing is never blocked when you pass the allowance. `GET /usage` shows where you are this calendar month, or the previous one with `?month=previous`: ```json title="Example" { "plan": "api", "month": "2026-10", "verifiedSignatures": 412, "includedSignatures": 500, "overageSignatures": 0, "overageCost": 0, "basePrice": 0, "total": 0 } ``` `includedSignatures` is only set on the API plan. For prices and volumes, [talk to us](https://www.idfier.com/contact-us). # Create a login session (/docs/identity/reference/login/createLoginSession) Starts a login session, valid for 5 minutes. Build the QR code (or, on a phone, the app link) from the three values in the response; see the QR code payload guide. The session's scopes are fixed now, from your business client. POST endpoint; the full definition is in /openapi/identity.yaml # Get a session's status and result (/docs/identity/reference/login/getLoginResult) Poll this from your server (every 2 s, backing off to 10 s) until the status is `approved`, `denied` or `error`, or `expiresAt` has passed. `user` is set once approved: always `id`, plus the fields in your scopes that IDfier has for the customer. POST endpoint; the full definition is in /openapi/identity.yaml # Offer an EU Digital Identity Wallet (/docs/identity/reference/login/startWalletLogin) For a session that is still `pending`, returns a link that opens the customer's EU Digital Identity Wallet instead of the IDfier app. Show it as a QR code, or as a link on a phone. Calling it again while the request is open returns the same `walletUrl`. Only available when IDfier has enabled EU wallets. POST endpoint; the full definition is in /openapi/identity.yaml # List verification links (/docs/identity/reference/verification-links/listLinks) Newest first. Pass `nextBefore` as `before` for the next page. GET endpoint; the full definition is in /openapi/identity.yaml # Create a verification link (/docs/identity/reference/verification-links/createLink) Uses one verification credit. The `url` is only returned here and on resend: IDfier stores only a hash of it. POST endpoint; the full definition is in /openapi/identity.yaml # Get a link and its result (/docs/identity/reference/verification-links/getLink) `result` is set once the customer has approved: exactly the requested scopes, plus an `id` unique to your business. GET endpoint; the full definition is in /openapi/identity.yaml # Delete a link (/docs/identity/reference/verification-links/deleteLink) Deletes the link and its record of the customer. DELETE endpoint; the full definition is in /openapi/identity.yaml # Cancel an open link (/docs/identity/reference/verification-links/cancelLink) POST endpoint; the full definition is in /openapi/identity.yaml # Issue a new URL and resend (/docs/identity/reference/verification-links/resendLink) Issues a new URL (the old one stops working), extends the expiry and emails it again if the link has a `recipientEmail`. Works on `sent`, `opened` and `expired` links. POST endpoint; the full definition is in /openapi/identity.yaml # Verified signatures this month (or last), and their cost on the API plan (/docs/sign/reference/get-usage) GET endpoint; the full definition is in /openapi/sign.yaml # Workspaces the key can use (/docs/sign/reference/get-workspaces) GET endpoint; the full definition is in /openapi/sign.yaml # Active templates (/docs/sign/reference/get-templates) GET endpoint; the full definition is in /openapi/sign.yaml # List documents, newest first (/docs/sign/reference/get-documents) GET endpoint; the full definition is in /openapi/sign.yaml # Upload a document, optionally with signers (/docs/sign/reference/post-documents) POST endpoint; the full definition is in /openapi/sign.yaml # A document, its signers and their progress (/docs/sign/reference/get-documents-id) GET endpoint; the full definition is in /openapi/sign.yaml # Add signers or viewers (/docs/sign/reference/post-documents-id-signers) POST endpoint; the full definition is in /openapi/sign.yaml # Email the invitations that haven't gone out yet (/docs/sign/reference/post-documents-id-send) POST endpoint; the full definition is in /openapi/sign.yaml # Cancel a document that isn't completed (/docs/sign/reference/post-documents-id-void) POST endpoint; the full definition is in /openapi/sign.yaml # The original upload (/docs/sign/reference/get-documents-id-file) GET endpoint; the full definition is in /openapi/sign.yaml # The signed document (once completed; 409 before) (/docs/sign/reference/get-documents-id-signed-file) GET endpoint; the full definition is in /openapi/sign.yaml # The signing certificate (once issued) (/docs/sign/reference/get-documents-id-certificate) GET endpoint; the full definition is in /openapi/sign.yaml