{"openapi":"3.1.0","info":{"title":"Touchque Authenticator Partner API","version":"5.2.0","summary":"Push, passkey and offline approval for your sign-ins and transactions.","description":"Server-to-server API for Touchque Authenticator: enroll users' phones, send approval requests (push with number matching, passkeys, offline QR codes) and receive signed results.\n\nThe official SDKs (`@touchque/node`, `touchque` for Python, PHP and Go) implement signing, retries and the step-up flow; this document describes the wire protocol they use.\n\n## Authentication\nEvery partner endpoint requires four headers — `x-api-key`, `x-timestamp`, `x-nonce`, `x-signature` — where the signature is an HMAC-SHA256 over `METHOD:PATH_WITH_QUERY:TIMESTAMP:NONCE:SHA256_HEX(BODY)` (details under the `Signature` security scheme). The only exceptions are the browser telemetry endpoint (Bearer `telemetryToken`), the Mirror Alarm canary the browser loads (`/c/{token}.gif`, `/c/{token}`) and the public `/ping`, `/health` and `/openapi.json`.\n\nIdempotency: legacy (non-`tq_`) keys may send `Idempotency-Key`; with `API_ENFORCE_IDEMPOTENCY=true` a repeat within 10 minutes answers 409 `IDEMPOTENCY_CONFLICT`. For `tq_` keys the server reads `x-idempotency-key` but does not replay or reject duplicates — rely on the per-endpoint behaviour documented below (e.g. login request de-duplication, `consume`).\n\n## Errors\nErrors are JSON with an `error` field; newer endpoints add a stable `code` (see the `Error` schema). Branch on `code` when present.\n\n## Rate limits\nA WAF limits each client IP per endpoint and in total (429 `RATE_LIMITED`); costly endpoints additionally have a per-workspace quota and some have per-user limits. See the `TooManyRequests` response.","contact":{"name":"Touchque","url":"https://touchque.com"}},"servers":[{"url":"https://api.touchque.com","description":"Production"}],"security":[{"ApiKey":[],"Timestamp":[],"Nonce":[],"Signature":[]}],"tags":[{"name":"Enrollment","description":"Link a user's phone (QR) and manage recovery codes."},{"name":"Login","description":"Approval requests: create, poll, consume, recover, report."},{"name":"Offline Sign","description":"QR challenge and typed codes for when the phone has no connection."},{"name":"QR Pairing","description":"Pair a browser to a user by a QR code scanned with the Touchque app, bound to the browser's own key."},{"name":"Recovery","description":"Never Locked Out: move a user whose phone is lost, dead or replaced to a new phone, with colleagues vouching and a cooling-off anyone can veto."},{"name":"Help Desk","description":"IT working on a person's account in the open: the person's phone knows who is on it, and confirms sensitive actions with the number the agent reads out."},{"name":"Risk","description":"Silent sign-ins: may a sign-in skip the phone approval, and the browsers that make it possible."},{"name":"Device Risk","description":"A visitor ID for every browser on your sign-in, sign-up, password-reset and approval pages, matched on Touchque's side, with signals the risk engine decides and that only ever ask for more (a paid add-on, off by default)."},{"name":"Safe Guard","description":"Safe multisig transactions read and approved on approvers' phones and enforced on chain by TouchQueSafeGuard; the workspace's protected Safes, read from chain."},{"name":"WebAuthn","description":"Passkeys: registration, passkey approval and passwordless sign-in (relayed through your backend)."},{"name":"Action Types","description":"The actions users approve (sign-in, payment, ...)."},{"name":"Actions","description":"Touchque Actions: your own code at points of the approval pipeline (before the push, the silent sign-in decision, after an approval, fraud reports, device and recovery events), sandboxed, versioned, testable against real recent decisions."},{"name":"Agents","description":"AI agents as identities of their own: registered public keys, an accountable owner, the action types they may ask approvals for, a review date and a kill switch. Agents authenticate themselves with private_key_jwt and DPoP-bound tokens."},{"name":"Touchque Login","description":"Management API for Touchque Login, the hosted sign-in for your own customers: the tenant, apps, APIs, connections, customer accounts (KVKK/GDPR export and erasure, bulk import with old password hashes), roles, organizations, custom domains. The sign-in itself is OpenID Connect on {slug}.login.touchque.com."},{"name":"Mobile SDK","description":"Your own iOS/Android app as the approver of your users (white-label), with the same hardware-signed, end-to-end encrypted approvals as the Touchque app."},{"name":"Users","description":"Enrolled users."},{"name":"Devices","description":"Device trust and the cross-workspace Trust Network."},{"name":"Logs","description":"Workspace event log."},{"name":"Mirror Alarm","description":"Called by the end user's browser on the page that shows the sign-in, to tell a phishing proxy from your own site."},{"name":"Meta","description":"Public health and discovery endpoints."},{"name":"Webhooks","description":"Signed callbacks Touchque sends to your `callbackUrl`."}],"paths":{"/auth/generate-secret":{"post":{"tags":["Enrollment"],"operationId":"generateSecret","summary":"Create an enrollment secret (QR) for a user","description":"Issues a one-time device-pairing secret plus 10 recovery codes. The user scans `qrCodeDataUrl` with the Touchque app within 5 minutes; you then receive a `DEVICE_LINKED` event webhook. If the user has an unlinked secret whose 5 minutes have passed, it is rotated in place (new secret, new recovery codes). WAF limit: 15/min per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalUsernameRequest"}}}},"responses":{"200":{"description":"Enrollment secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeneratedSecret"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found...` Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`Secret already linked to a device` (use `POST /auth/secret/reset`) or `Secret already generated` (still within its 5 minutes; `expiresAt` and `ttlRemainingMs` say how long).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretConflict"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/auth/secret/reset":{"post":{"tags":["Enrollment"],"operationId":"resetSecret","summary":"Reset a user's enrollment","description":"Rotates the user's secret in place and returns the new one immediately: unlinks the phone, lifts a freeze, clears the rejection counter and issues 10 new recovery codes (old ones stop working). The user must link again within 5 minutes. WAF limit: 5/min per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalUsernameRequest"}}}},"responses":{"200":{"description":"New enrollment secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetSecret"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found...` Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`No secret found for this user`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/login/request":{"post":{"tags":["Login"],"operationId":"createLoginRequest","summary":"Ask the user to approve a sign-in or action","description":"Sends an approval request (push + socket) to the user's linked phone and returns its `requestId`. Poll `GET /login/status/{id}` or wait for the webhook; the request expires after 30 seconds.\n\n**Step-up fields.** `challengeCode` (number matching) is present when matching is required; `requiresPasskey: true` means no push was sent and the request must be approved with a passkey; `telemetryToken` enables the behavioral widget; `canary` is the Mirror Alarm probe for the page that shows the sign-in.\n\n**Vishing Shield.** When the user taps Approve during a call (or with the screen shared or remote-controlled), the request can turn into a passkey request mid-flight (`GET /login/status` shows `requiresPasskey: true`), be refused (`reason: vishing_suspected`), or be approved as `provisional`. This is on for every workspace.\n\n**Silent sign-ins.** Send the end user's `browserProof`: once a real second factor approves this request, the browser is known and `POST /risk/check` can let its later sign-ins skip the phone.\n\n**De-duplication.** An identical request (same user, type, `referenceId` and `details`) still PENDING from the last 30 seconds is returned instead of a new one (`status` included, no new push, no `telemetryToken`).\n\n**Risk.** A risk score ≥ 0.8 stores the request as REJECTED and answers 403 `blocked` / `risk` (no webhook is sent). Geo policy and access-policy rules can block (403) or force number matching.\n\nLimits: WAF 10/min per IP; 600/min per workspace; at most 5 approval requests per user per 10 minutes that were not approved (still waiting, rejected or expired; approved ones do not count; `PUSH_LIMIT_PER_USER`, push-bombing guard).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLoginRequest"}}}},"responses":{"200":{"description":"Request created (or an identical pending one reused).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequestCreated"}}}},"400":{"description":"`invalid_request` (missing `externalUsername` / `type`), `invalid_details`, `unknown_action`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`integration_not_found`; `action_disabled`; `device_keys_lost` (the linked phone reported that its security keys were destroyed because its passcode or screen lock was turned off; no push is sent, the user links the app again or uses a recovery code); `passkey_not_registered` (`error: phishing_resistant_required`, `passkeyRegistered: false` — policy demands a passkey and the user has none); `blocked` with `reason` `geo_policy` (+`countryCode`), `access_policy` (+`ruleName`), `risk` (+`requestId`; the score is not disclosed — `GET /login/status` shows `rejectedBy: risk_engine`) `provisional` (a critical action while an approval this user gave during a call waits to be re-confirmed on the phone, at most 12 hours; see `provisional` on `GET /login/status`) or `action` (one of your Actions refused it: +`action`, its slug; `error` is the reason it gave). `passkey_not_registered` with `reason: action` when an Action asked for a passkey the user does not have. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"description":"`device_not_linked` — the user has no linked phone (enroll with `POST /auth/generate-secret`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"423":{"description":"`frozen` — the user's 2FA is frozen. Automatic freezes (after repeated rejections) lift at `frozenUntil` and send Retry-After; a manual freeze has `retryAfter: null`.","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CodedError"}],"properties":{"retryAfter":{"type":["integer","null"]},"frozenUntil":{"type":["string","null"],"format":"date-time"}}}}}},"429":{"description":"`rate_limited` (`error: too_many_login_requests`) — too many approval requests for this user — or the workspace quota / WAF limit (see `TooManyRequests`).","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"503":{"description":"`policy_unavailable` — a critical action could not be checked against the access policy; retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/login/status/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Login request id.","schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Login"],"operationId":"getLoginStatus","summary":"Get a login request's status","description":"Returns the state of a request of your workspace plus what it is bound to (user, action, reference, details), so you can check an approval is for THIS user and transaction. `assurance` and `approvalProof` are only set for CONFIRMED requests. This path bypasses the WAF entirely (no per-IP rate limit), so polling is safe.","responses":{"200":{"description":"Request status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginStatusResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found.` Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`Failed to check login status`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/login/{id}/consume":{"parameters":[{"name":"id","in":"path","required":true,"description":"Login request id (`requestId`).","schema":{"type":"string","pattern":"^[0-9a-fA-F-]{36}$"}}],"post":{"tags":["Login"],"operationId":"consumeLoginRequest","summary":"Use an approval exactly once (step-up guard)","description":"Atomically marks a CONFIRMED request as used and returns what was approved. The first call wins; every later call answers 409 `already_used` (replayed token, second tab, retried form post). Call it right before running the protected action. No request body. There is no time limit once a request is CONFIRMED — an approval can be consumed at any later time.","responses":{"200":{"description":"Approval consumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsumeResponse"}}}},"400":{"description":"`invalid_request` — malformed id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`integration_not_found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`already_used`, `rejected`, `expired` or `not_approved` (still pending).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsumeConflict"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/login/{id}/record":{"parameters":[{"name":"id","in":"path","required":true,"description":"Login request id (`requestId`).","schema":{"type":"string","pattern":"^[0-9a-fA-F-]{36}$"}}],"get":{"tags":["Login"],"operationId":"getLoginRecord","summary":"What a platform request did (Touchque platform only)","description":"For Touchque's own SSO: a request it sent for a customer organization (header `x-touchque-on-behalf-of`, honoured only for a platform organization's key), as that organization's case view shows it: status, how it was answered, the number the sign-in page showed once the request is over, the phone's platform, the risk score and reasons, and the phone-side case code. Any other caller, or another organization, gets 404.","responses":{"200":{"description":"The request's record.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/login/{id}/report-fraud":{"parameters":[{"name":"id","in":"path","required":true,"description":"Login request id (`requestId`).","schema":{"type":"string","pattern":"^[0-9a-fA-F-]{36}$"}}],"post":{"tags":["Login"],"operationId":"reportLoginFraud","summary":"Report a request as fraud after the fact","description":"For a sign-in or transaction you later learned was fraudulent (chargeback, account takeover, social engineering). Recorded in the audit log and, when a person acted on the request (see `trainingLabel`), sent to the risk engine as a strong training label. Does not change the request's status. Idempotent (`alreadyReported: true` on repeats). At most 60 reports per minute per workspace; WAF limit: 120/min per IP.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportFraudRequest"}}}},"responses":{"200":{"description":"Reported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportFraudResponse"}}}},"400":{"description":"`invalid_request`, `invalid_reason`, `invalid_note`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`integration_not_found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`integration_quota_exceeded` — more than 60 reports in a minute for this workspace (Retry-After) — or the WAF limit (see `TooManyRequests`).","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/login/recovery":{"post":{"tags":["Login"],"operationId":"approveWithRecoveryCode","summary":"Approve a pending request with a recovery code","description":"Lower-assurance fallback when the phone is unavailable: confirms a PENDING request (`confirmedVia: RECOVERY_CODE`) and burns the code; fires the SUCCESS webhook. Refused for passkey-only requests and, unless `TenantPolicy.allowRecoveryForCritical`, for critical action types. The request's `expiresAt` is not checked — any request still in status PENDING can be approved. 10 attempts per user per 15 minutes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryRequest"}}}},"responses":{"200":{"description":"Request approved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessMessage"}}}},"400":{"description":"`requestId and code are required` or `Invalid or already used recovery code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`phishing_resistant_required` (passkey-only request, or one the Vishing Shield moved to a passkey), the critical-action refusal, or `step_up_required`: the request was number-matched for a reason a recovery code cannot answer (its `riskFactors` carry `require_number_match`, `repeated_failures`, `policy_step_up`, `policy_unavailable`, `geo_unverified`, `post_approval_step_up`, `vishing_suspected` or `phishing_origin`); it needs the phone. A request number-matched only because the workspace always asks, because the risk engine was unreachable or because of the lost phone's own state is still accepted. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`Pending login request not found` or `Active 2FA secret not found for user`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many recovery code attempts for this user (Retry-After), or WAF limit.","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/login/{requestId}/telemetry":{"parameters":[{"name":"requestId","in":"path","required":true,"description":"Login request id from `POST /login/request`.","schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Login"],"operationId":"submitBehavioralTelemetry","summary":"Submit behavioral telemetry (browser widget)","description":"Called from the END USER's browser by the `@touchque/web` behavioral widget — not by your backend and not HMAC-signed. Authorised by the request's `telemetryToken`. When the browser sends an `Origin` and the workspace's organization has verified domains (Organization → Domains), the origin's host must be one of them or under one (`localhost` outside production); one that is not even on the same site counts as Mirror Alarm evidence, like the canary. Advisory: may raise the request's risk score, never changes its status. Always answers 200 once authorised (`accepted: false` when ignored). WAF limit: 20/min per IP.","security":[{"TelemetryToken":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelemetryRequest"}}}},"responses":{"200":{"description":"Processed (or ignored).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TelemetryAccepted"}}}},"401":{"description":"`TELEMETRY_TOKEN_REQUIRED` — missing `Authorization: Bearer` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`TELEMETRY_TOKEN_INVALID` (unknown, expired or wrong token) or `ORIGIN_NOT_ALLOWED`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/offline/challenge":{"post":{"tags":["Offline Sign"],"operationId":"createOfflineChallenge","summary":"Create an offline-sign QR challenge","description":"For when the phone is offline: returns a QR encrypted for the user's device. The user scans it, the app shows a 7-character code, you check it with `POST /offline/verify`. Requires `TenantPolicy.offlineSignEnabled` and a phone enrolled for offline sign. Critical actions need `offlineAllowedForCritical` and `details`. At most 5 open challenges per user. The risk engine, the workspace's country rules, its access rules and the post-approval step-up mark apply as for a push, also when `requestId` names one: a refusal is a 403, a step-up puts a number on the QR (`challengeCode`, or the linked push's own number). `requireNumberMatch` is read as on `POST /login/request` (`\"true\"` and `1` count). WAF limit: 20/min per IP; 600/min per workspace.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineChallengeRequest"}}}},"responses":{"200":{"description":"Challenge created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineChallenge"}}}},"400":{"description":"Missing/invalid field, `details are required for critical actions`, `details_too_large` (QR would exceed 2000 characters), `requestId must be a request id`, or `Invalid action type`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found`, `offline_sign_disabled`, action type disabled, `phishing_resistant_required`, `passkey_required` (the linked push now waits for a passkey, or its page is a phishing proxy), `offline_not_allowed_for_critical`, `device_integrity_failed`, `{ code: 'device_keys_lost' }` (the phone's keys were destroyed when its passcode or screen lock was turned off), blocked by the risk engine (`riskScore`), or `{ code: 'blocked', reason: 'geo_policy' }` (a country rule, +`countryCode`) / `reason: 'access_policy'` (an access rule, +`ruleName`). Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`request_not_found` (linked `requestId`) or `No linked device found for this user`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`request_rejected`; `device_not_enrolled_for_offline` (the phone has not enrolled yet: the app enrols when it is next opened online, or, for a phone linked after its first day, right after its next approval, signed by its hardware key); `device_needs_reenrollment` (this server's offline key was rotated: the phone re-binds when it is next opened online, no action from the user).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"423":{"description":"User's 2FA is frozen (`frozenUntil`).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"}],"properties":{"frozenUntil":{"type":["string","null"],"format":"date-time"}}}}}},"429":{"description":"`too_many_pending_challenges`, or the workspace quota / WAF limit.","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}},"500":{"description":"`device_secret_unavailable` or unexpected error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Offline sign is not configured on this server (`offline_sign_not_configured`), or `{ code: 'policy_unavailable' }`: the access policy could not be read for a critical action; try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/offline/verify":{"post":{"tags":["Offline Sign"],"operationId":"verifyOfflineChallenge","summary":"Verify the code typed for an offline challenge","description":"Checks the 7-character code. 5 attempts per challenge; wrong codes also count towards a per-user lockout (`TenantPolicy.maxFailedChallenges`, default 5 per hour). On success, PENDING push requests of the same user and action type are expired (superseded). It does NOT confirm the linked push request and sends no webhook — treat `approved: true` as the approval. WAF limit: 30/min per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineVerifyRequest"}}}},"responses":{"200":{"description":"Code accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineVerified"}}}},"400":{"description":"`challengeId is required` / `code is required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Wrong code (`invalid_code`, or `locked` after the last attempt) with `attemptsLeft`. Authentication failures also use 401 — see `Unauthorized`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"403":{"description":"`Integration not found`; or an `OfflineResult` with `passkey_required` (the linked push now waits for a passkey, or its page is a phishing proxy). Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/OfflineResult"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`unknown_challenge`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"409":{"description":"`device_not_enrolled` — the phone's offline key is missing or from a rotated server key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"410":{"description":"`used`, `locked`, `expired` or `request_rejected`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"429":{"description":"`too_many_failures` for this user, or the workspace quota / WAF limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/OfflineNotConfigured"}}}},"/offline/totp/verify":{"post":{"tags":["Offline Sign"],"operationId":"verifyOfflineTotp","summary":"Verify a typed time-based offline code","description":"Lower-assurance fallback without a QR: the user opens the app and types its rolling 7-character code. Requires `offlineSignEnabled` and `offlineTotpEnabled`; refused when the workspace requires phishing resistance, and for critical action types (the linked push's type, or `type`; with a linked push a critical `type` is refused too). Each time step can be used once. A correct code is still a sign-in: the risk engine, the workspace's country rules, its access rules and the post-approval step-up mark apply as for a push, also when `requestId` names one. A code has no number to match, so any step-up answers `step_up_required` (show the QR instead), as does a linked push that was number-matched for a reason a typed code cannot answer (see `riskFactors`). An unreachable risk engine alone, and `alwaysRequireChallenge`, do not refuse it. On success, PENDING push requests of the user (of `type`, or of every type when omitted) are expired. No webhook. WAF limit: 30/min per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineTotpVerifyRequest"}}}},"responses":{"200":{"description":"Code accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineTotpVerified"}}}},"400":{"description":"`externalUsername is required`, `code is required`, `requestId must be a request id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_code` (wrong or already used). Authentication failures also use 401 — see `Unauthorized`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"403":{"description":"`offline_totp_disabled`, `phishing_resistant_required`, `offline_totp_not_allowed_for_critical` (`Error`); or an `OfflineResult` with `passkey_required` (the linked push now waits for a passkey, or its page is a phishing proxy), `step_up_required` (scan the QR instead), `blocked` (the risk engine, or a workspace rule named in `blockedBy`) or `unavailable` (no risk verdict). Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/OfflineResult"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`no_linked_device` (`OfflineResult`) or `request_not_found` (`Error`).","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/OfflineResult"},{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"`device_not_enrolled`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"410":{"description":"`request_rejected`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"423":{"description":"`frozen` (`frozenUntil`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"429":{"description":"`too_many_failures`, or the workspace quota / WAF limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineResult"}}}},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/OfflineNotConfigured"}}}},"/offline/totp/verifiers":{"post":{"tags":["Offline Sign"],"operationId":"offlineTotpVerifiers","summary":"Digests of the next time-based codes, for checking codes during a Touchque outage","description":"For your own server to check a typed time-based code while it cannot reach Touchque (the Touchque RADIUS proxy uses it): salted, slow digests of each user's codes for the next steps, never a key. Requires `offlineSignEnabled`, `offlineTotpEnabled` and `outageVerifiersEnabled`; refused under a passkey-only policy. Users who cannot use time-based codes now get no entries: drop them. Fetch again every few minutes from the step you have, so removed and frozen users fall out within that time. Up to 480 steps (4 hours) ahead and 24,000 user-steps per call (50 users for 4 hours, 1,000 for 12 minutes); 30 calls a minute per workspace.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineTotpVerifiersRequest"}}}},"responses":{"200":{"description":"The digests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfflineTotpVerifiers"}}}},"400":{"description":"`externalUsernames: 1 to 50 user names`, or `steps` / `fromStep` out of range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`outage_verifiers_disabled`, `phishing_resistant_required`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The workspace quota.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"},"503":{"$ref":"#/components/responses/OfflineNotConfigured"}}}},"/risk/check":{"post":{"tags":["Risk"],"operationId":"checkSilentSignIn","summary":"May this sign-in skip the phone approval?","description":"Silent sign-ins (re-prompt fatigue): call it before `POST /login/request` with the end user's `browserProof`. `prompt: false` means the sign-in may go on without asking the phone: the browser proved its device-bound key, passed a real second factor (a push that was not provisional, a passkey or an offline QR) within the allowed window, on the phone linked now, from a country and network the user approved from before, and the risk engine sees nothing new or risky. Otherwise call `POST /login/request` as usual, with the same `browserProof`, so the approval makes the browser known.\n\nNever silent for a critical action, while the user's approvals are frozen, for a phone that lost its keys, under a passkey-only or always-challenge policy, after a post-approval step-up, under an access or country rule that applies, with a Vishing Shield signal in the last days, or when the risk engine cannot be asked. Sends nothing to the phone. Each answer is logged (`RISK_DECISION`, phase `check`). An internal error answers `prompt: true`.\n\nLimits: the WAF default per IP; the same per-workspace quota as `POST /login/request`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskCheckRequest"}}}},"responses":{"200":{"description":"The answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskCheckResult"}}}},"400":{"description":"`invalid_request` (missing `externalUsername`, bad `type` or `maxAgeSeconds`) or `unknown_action`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`integration_not_found`; `action_disabled`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/risk/browsers":{"get":{"tags":["Risk"],"operationId":"listKnownBrowsers","summary":"List a user's known browsers","description":"The browsers that may sign this user in silently (forgotten ones are not listed), most recently seen first, at most 100.","parameters":[{"name":"externalUsername","in":"query","required":true,"description":"Your identifier for the user.","schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"Known browsers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowserKeyList"}}}},"400":{"description":"`invalid_request` — missing `externalUsername`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`integration_not_found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/risk/browsers/forget":{"post":{"tags":["Risk"],"operationId":"forgetKnownBrowsers","summary":"Forget a user's known browsers","description":"A forgotten browser asks the phone again at its next sign-in; a new real approval makes it known again. Without `thumbprint` every browser of the user is forgotten (e.g. after a password reset or a reported takeover).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgetBrowsersRequest"}}}},"responses":{"200":{"description":"Forgotten.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgetBrowsersResult"}}}},"400":{"description":"`invalid_request` — missing `externalUsername` or a malformed `thumbprint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`integration_not_found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/qr-pairings":{"post":{"tags":["QR Pairing"],"operationId":"createQrPairing","summary":"Register a browser pairing code","description":"Pair a browser (an extension, a desktop app) to a user by a QR code they scan with the Touchque app, like signing in to a messenger on the web. Put the pairing id, a one-time nonce and the browser key's thumbprint in the code: `touchque://pair-browser?v=1&p=<pairingId>&n=<nonce>&k=<keyThumbprint>`.\n\nThe phone that scans it sends the nonce and the thumbprint (both must match), shows the person what asks to be paired (browser, OS, place and address, the key's fingerprint) and warns when it is on another network or in another country. The person goes on, declines, or says someone told them to scan it (Vishing Shield: their 2FA is frozen). Poll `GET /qr-pairings/{id}`; at `confirmed`, run your own checks and ask with `POST /login/request` `qrPairing`, or refuse with `POST /qr-pairings/{id}/outcome`.\n\nOne scan per code; a code lives `ttlSeconds` (2 minutes) and the flow after the scan 5 minutes. Limits: 60 codes per 10 minutes per browser address, 12 per browser key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrPairingCreate"}}}},"responses":{"201":{"description":"Registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrPairingCreated"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`integration_not_found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`rate_limited`.","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/qr-pairings/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Pairing id.","schema":{"type":"string"}}],"get":{"tags":["QR Pairing"],"operationId":"getQrPairing","summary":"A pairing's state","description":"Where a pairing stands; once scanned, the account the phone holds with your workspace and how near the phone was to the browser. Poll it every second or two while the code is shown.","responses":{"200":{"description":"The pairing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrPairing"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found` (not yours, or gone).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/qr-pairings/{id}/outcome":{"parameters":[{"name":"id","in":"path","required":true,"description":"Pairing id.","schema":{"type":"string"}}],"post":{"tags":["QR Pairing"],"operationId":"refuseQrPairing","summary":"Refuse a scanned pairing","description":"Your own checks refused a pairing that was scanned (an organization rule, a computer an administrator must approve first, too many browsers): the phone shows `message`, and the case or device code.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrPairingOutcome"}}}},"responses":{"200":{"description":"Refused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrPairing"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`not_open`: it was not scanned, or it already ended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/recovery/vouchers":{"get":{"tags":["Recovery"],"operationId":"getRecoveryVouchers","summary":"A user's recovery vouchers","description":"The colleagues the user named to vouch for a move to a new phone, and whether each can vouch today (`eligible`, or `reason`: `no_phone`, `no_hardware_key`, `keys_lost`, `frozen`, `on_probation`, `same_phone`). `probationUntil` is set while the user's own account is on probation after a vouch recovery.","parameters":[{"name":"externalUsername","in":"query","required":true,"description":"Your identifier for the user.","schema":{"type":"string","maxLength":255}}],"responses":{"200":{"description":"The vouchers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryVouchers"}}}},"400":{"description":"`invalid_request` — missing `externalUsername`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`user_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Recovery"],"operationId":"setRecoveryVouchers","summary":"Name a user's recovery vouchers","description":"Replaces the list (at most 5 other users of this workspace; the user themself and duplicates are dropped). Ask for the user's phone (or a passkey) before you call it: whoever can change the list chooses who may move the account. A list changed in the last 72 hours asks a recovery for one more voucher.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryVouchersRequest"}}}},"responses":{"200":{"description":"The vouchers as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryVouchers"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`user_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/recovery/start":{"post":{"tags":["Recovery"],"operationId":"startRecovery","summary":"Start moving a user to a new phone (Vouch)","description":"For a user whose phone is lost, dead or replaced, after you checked the first factor (password, upstream IdP). Show `bindUri` as a QR: the Touchque app on the new phone scans it and binds its hardware key to this recovery. Then each voucher gets a number-matched approval on their own phone; the numbers appear in `vouchers[].code` (show them to the user, who reads one to each colleague on a call). The old phone, if alive, gets a request too: approving moves the account at once, rejecting stops it.\n\nOnce `required` vouchers approved with their hardware keys, a cooling-off (`coolOffMinutes`) starts in which a veto (`POST /recovery/{id}/veto`, a voucher saying no, the old phone saying no) stops the move and freezes the user's 2FA (a partner veto does not freeze). Then the account moves; for 24 hours its critical actions answer 403 `blocked` / `recovery_probation`.\n\nThe risk engine sees the attempt: its BLOCK answers 403 `blocked`; a hard signal, a recent \"not me\", a recovery in the last day or vouchers changed in the last 72 hours add one voucher and double the cooling-off (`elevated`, `reasons`). `required` and `coolOffMinutes` can only raise the floors (2 vouchers, 15 minutes). `fallbackVouchers` (e.g. the organization's admins) fill up a short named list. One running recovery per user, three per day.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryStartRequest"}}}},"responses":{"201":{"description":"Started; waiting for the new phone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryStarted"}}}},"400":{"description":"`invalid_request`, `invalid_details`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`blocked` (`reason: risk`). Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"description":"`device_not_linked`: no phone to recover; link one with a new QR.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`recovery_in_progress` (`recoveryId`), `not_enough_vouchers` (`required`, `eligible`, `vouchers`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"423":{"description":"`frozen` (`frozenUntil`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`rate_limited`: three recoveries today.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/recovery":{"get":{"tags":["Recovery"],"operationId":"listRecoveries","summary":"List recoveries","description":"A user's recoveries (also completed transfers, `method: transfer`), newest first, or with `open=true` every running one of the workspace (e.g. for an admin's veto screen). At most 50.","parameters":[{"name":"externalUsername","in":"query","required":false,"description":"Your identifier for the user.","schema":{"type":"string","maxLength":255}},{"name":"open","in":"query","required":false,"description":"`true`: only running recoveries.","schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"Recoveries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryList"}}}},"400":{"description":"`invalid_request` — pass `externalUsername` or `open=true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/recovery/{id}":{"get":{"tags":["Recovery"],"operationId":"getRecovery","summary":"Recovery status","description":"Moves the recovery on from what the phones answered, then returns it with the receipt chain. Poll it every few seconds while the user waits; the codes are present only while they can be used.","parameters":[{"name":"id","in":"path","required":true,"description":"The recovery id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The recovery.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recovery"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`recovery_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/recovery/{id}/veto":{"post":{"tags":["Recovery"],"operationId":"vetoRecovery","summary":"Stop a running recovery","description":"E.g. an organization admin who does not believe it. Does not freeze the user's 2FA.","parameters":[{"name":"id","in":"path","required":true,"description":"The recovery id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryVetoRequest"}}}},"responses":{"200":{"description":"Vetoed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recovery"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`recovery_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`recovery_not_open`: already completed, vetoed, expired or failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/action-types":{"get":{"tags":["Action Types"],"operationId":"listActionTypes","summary":"List action types","description":"All action types of the workspace, including disabled ones.","responses":{"200":{"description":"Action types.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ActionType"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}},"post":{"tags":["Action Types"],"operationId":"upsertActionType","summary":"Create or update an action type","description":"Idempotent upsert by `type` — safe to call on every start-up. Never re-enables a type an admin disabled in the Dashboard; `description` and `critical` change only when sent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertActionType"}}}},"responses":{"200":{"description":"The stored action type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionType"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`internal_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/actions/triggers":{"get":{"tags":["Actions"],"operationId":"listActionTriggers","summary":"Triggers, decisions, capabilities and limits","description":"Every trigger with the decisions its Actions can make, the loosening capabilities, the notification types and a sample event.","responses":{"200":{"description":"The catalogue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionCatalog"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/templates":{"get":{"tags":["Actions"],"operationId":"listActionTemplates","summary":"No-code templates","description":"Templates for the common Actions (alerts; block, step up or require a passkey by country, local time, role, amount, action or risk; always ask the phone when). A template renders to plain code you can edit.","responses":{"200":{"description":"Templates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionTemplateList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/templates/{id}/render":{"post":{"tags":["Actions"],"operationId":"renderActionTemplate","summary":"Preview a template as code","parameters":[{"name":"id","in":"path","required":true,"description":"Template id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["trigger"],"properties":{"trigger":{"$ref":"#/components/schemas/ActionTrigger"},"params":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"The code and what it needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderedTemplate"}}}},"400":{"description":"`invalid_request`, `invalid_template`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/secrets":{"get":{"tags":["Actions"],"operationId":"listActionSecrets","summary":"List secret names","description":"Names only: a value is never returned once set.","responses":{"200":{"description":"Secret names.","content":{"application/json":{"schema":{"type":"object","properties":{"secrets":{"type":"array","items":{"$ref":"#/components/schemas/ActionSecret"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/secrets/{name}":{"put":{"tags":["Actions"],"operationId":"setActionSecret","summary":"Set a secret","description":"Encrypted at rest (AES-256-GCM, bound to the workspace and the name). Only versions that declare the name can read it. At most 100 secrets of 4 KB.","parameters":[{"name":"name","in":"path","required":true,"description":"Secret name.","schema":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,63}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["value"],"properties":{"value":{"type":"string","maxLength":4096}}}}}},"responses":{"200":{"description":"Changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionSecret"}}}},"201":{"description":"Added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionSecret"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`too_many_secrets`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Actions"],"operationId":"deleteActionSecret","summary":"Delete a secret","parameters":[{"name":"name","in":"path","required":true,"description":"Secret name.","schema":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,63}$"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`secret_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/events":{"get":{"tags":["Actions"],"operationId":"listActionEvents","summary":"Recorded events","description":"The events triggers fired with (and, for pre triggers, what was decided), newest first, kept 30 days. Use an `id` with the test runner.","parameters":[{"name":"trigger","in":"query","required":false,"description":"Only this trigger.","schema":{"$ref":"#/components/schemas/ActionTrigger"}},{"name":"limit","in":"query","required":false,"description":"1-100 (default 20).","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionEventList"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/test":{"post":{"tags":["Actions"],"operationId":"testAction","summary":"Run an Action once (test runner)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionTestRequest"}}}},"responses":{"200":{"description":"What the run did.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionTestResult"}}}},"400":{"description":"`invalid_request`, `invalid_template`, `action_invalid` (does not compile).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`event_not_found`, `action_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`rate_limited` (120 runs per 10 minutes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/dry-run":{"post":{"tags":["Actions"],"operationId":"dryRunAction","summary":"Replay a draft against recent real decisions","description":"How many recent decisions the draft would have changed and in which direction, how many notifications it would have sent, how often it fails and how long it takes. Nothing is sent; `api.fetch` is off; asked-for capabilities are assumed approved.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDryRunRequest"}}}},"responses":{"200":{"description":"The report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDryRunResult"}}}},"400":{"description":"`invalid_request`, `invalid_template`, `action_invalid`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"`rate_limited` (30 runs per 10 minutes) or `dry_run_running` (one dry run per workspace at a time).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions":{"get":{"tags":["Actions"],"operationId":"listActions","summary":"List Actions","responses":{"200":{"description":"Actions.","content":{"application/json":{"schema":{"type":"object","properties":{"actions":{"type":"array","items":{"$ref":"#/components/schemas/Action"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/{slug}":{"put":{"tags":["Actions"],"operationId":"deployAction","summary":"Deploy an Action (create or new version)","description":"Creates the Action or adds a version and makes it live at once (instant rollback with `POST /actions/{slug}/rollback`). The same code and settings again change nothing.\n\nGuard rails: an Action may tighten freely (`api.deny`, `api.stepUp`, `api.requirePasskey`, `api.prompt`, notifications, `api.freezeUser`). Loosening calls (`api.skipNumberMatch`, `api.overrideBlock`, `api.allowSilent`) count only with the matching capability, and a version that asks for one waits for an admin's phone approval in the dashboard. No capability loosens a frozen account, lost phone keys, recovery probation, the Vishing Shield, a passkey-only policy, a country block, a critical action's number matching or push fatigue.","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployAction"}}}},"responses":{"200":{"description":"A new or unchanged version of an existing Action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDeployResult"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDeployResult"}}}},"400":{"description":"`invalid_request`, `invalid_template`, `action_invalid` (does not compile or export onExecute).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`trigger_mismatch`, `too_many_actions`, `too_many_versions` (200 per Action).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`rate_limited` (60 deploys per 10 minutes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"tags":["Actions"],"operationId":"getAction","summary":"An Action, its code and versions","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}}],"responses":{"200":{"description":"The Action.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`action_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Actions"],"operationId":"updateAction","summary":"Rename, turn on or off, reorder, change onFailure","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAction"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"action":{"$ref":"#/components/schemas/Action"}}}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`action_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Actions"],"operationId":"deleteAction","summary":"Delete an Action and its versions","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`action_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/{slug}/rollback":{"post":{"tags":["Actions"],"operationId":"rollbackAction","summary":"Roll back to an earlier version","description":"Without `version`: the newest runnable version before the active one. Takes effect at once.","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"integer"}}}}}},"responses":{"200":{"description":"Rolled back.","content":{"application/json":{"schema":{"type":"object","properties":{"action":{"$ref":"#/components/schemas/Action"},"version":{"$ref":"#/components/schemas/ActionVersion"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`action_not_found`, `version_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`nothing_to_roll_back_to`, `version_not_runnable` (waiting for approval).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/{slug}/versions/{number}":{"get":{"tags":["Actions"],"operationId":"getActionVersion","summary":"One version with its code","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}},{"name":"number","in":"path","required":true,"description":"Version number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The version.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionVersion"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`action_not_found`, `version_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/{slug}/logs":{"get":{"tags":["Actions"],"operationId":"listActionLogs","summary":"Runs of an Action","description":"Every live run: decision, notifications and their delivery, console output, error, duration. Newest first, kept 30 days.","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}},{"name":"limit","in":"query","required":false,"description":"1-200 (default 50).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"before","in":"query","required":false,"description":"Runs before this time (paging).","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Runs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionLogList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`action_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/actions/{slug}/metrics":{"get":{"tags":["Actions"],"operationId":"getActionMetrics","summary":"Metrics of an Action","parameters":[{"name":"slug","in":"path","required":true,"description":"The Action's slug.","schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$"}},{"name":"window","in":"query","required":false,"description":"1h, 24h (default), 7d or 30d.","schema":{"type":"string","enum":["1h","24h","7d","30d"]}}],"responses":{"200":{"description":"Metrics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionMetrics"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`action_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/users":{"get":{"tags":["Users"],"operationId":"listUsers","summary":"List enrolled users","description":"Every enrollment of the workspace, newest first. Not paginated.","responses":{"200":{"description":"Users.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"Error message of the failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/users/{username}":{"parameters":[{"name":"username","in":"path","required":true,"description":"The `externalUsername`, URL-encoded. Note: the server URL-decodes it a second time after routing, so a literal `%` in a username must be double-encoded (`%2525`), and a malformed escape answers 500.","schema":{"type":"string"}}],"get":{"tags":["Users"],"operationId":"getUser","summary":"Get one enrolled user","responses":{"200":{"description":"User.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`User not found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"Error message of the failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webauthn/register/options":{"post":{"tags":["WebAuthn"],"operationId":"webauthnRegisterOptions","summary":"Start passkey registration","description":"Returns registration options for the browser. Your backend relays the ceremony: browser → your backend → Touchque (signed). The challenge is valid for 5 minutes. Uses the workspace's own relying party (domain) when configured. A user with a linked phone first approves a `PASSKEY_ENROLL` request: a passkey approves requests on its own, so a stolen session must not be able to add one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnRegisterOptionsRequest"}}}},"responses":{"200":{"description":"Registration options.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicKeyCredentialCreationOptionsJSON"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`enrollment_approval_required` (no `approvalRequestId`), `enrollment_approval_invalid` (unknown, not approved, another user or action, or older than 10 minutes) or `enrollment_approval_used`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/register/verify":{"post":{"tags":["WebAuthn"],"operationId":"webauthnRegisterVerify","summary":"Finish passkey registration","description":"Verifies the browser's attestation and stores the credential. When `TenantPolicy.requireVerifiedAuthenticator` is on, the authenticator must be listed in the FIDO Metadata Service. A stored passkey sends the `PASSKEY_REGISTERED` event webhook.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnRegisterVerifyRequest"}}}},"responses":{"200":{"description":"Credential registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnRegistered"}}}},"400":{"description":"Missing fields, `Registration session not found or expired`, `Verification failed` (+`message`), `Registration could not be verified`, or `unverified_authenticator`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/login/options":{"post":{"tags":["WebAuthn"],"operationId":"webauthnLoginOptions","summary":"Start passkey approval of a pending request","description":"Authentication options scoped to one PENDING login request (e.g. one created with `requiresPasskey: true`). Valid for 5 minutes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnLoginOptionsRequest"}}}},"responses":{"200":{"description":"Authentication options.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicKeyCredentialRequestOptionsJSON"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`No PENDING login request found` or `No WebAuthn credential registered for this user`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/login/verify":{"post":{"tags":["WebAuthn"],"operationId":"webauthnLoginVerify","summary":"Approve a pending request with a passkey","description":"Verifies the assertion and confirms the request (`confirmedVia: WEBAUTHN`, phishing-resistant), then fires the SUCCESS webhook. The request's `expiresAt` is not checked — a request still in status PENDING can be approved.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnLoginVerifyRequest"}}}},"responses":{"200":{"description":"Request approved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnLoginApproved"}}}},"400":{"description":"Missing fields, `Login session not found or expired`, `Verification failed` (+`message`), `Login could not be verified`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`No PENDING login request found` or `This credential is not registered for this user`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/authenticate/primary/availability":{"get":{"tags":["WebAuthn"],"operationId":"webauthnPrimaryAvailability","summary":"Is passkey sign-in on","description":"Whether passwordless passkey sign-in is on for the workspace, so a sign-in page shows its passkey button and passkey autofill only when it works (otherwise `POST /webauthn/authenticate/primary/options` answers 403 `passwordless_login_disabled`). Read-only.","responses":{"200":{"description":"Whether passkey sign-in is on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnPrimaryAvailability"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/authenticate/primary/options":{"post":{"tags":["WebAuthn"],"operationId":"webauthnPrimaryOptions","summary":"Start passwordless passkey sign-in","description":"Passwordless sign-in (no password, no phone). Requires `TenantPolicy.passwordlessLoginEnabled`. User verification is required only when `passwordlessRequireUserVerification` is on. With `externalUsername` the options list that user's passkeys (404 when there are none). Without it they accept any discoverable passkey of the site (empty `allowCredentials`), for passkey autofill (WebAuthn conditional mediation): the passkey the user picks names the account, and its `userHandle` must match the one it was registered with.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnPrimaryOptionsRequest"}}}},"responses":{"200":{"description":"Attempt id and authentication options.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnPrimaryOptions"}}}},"400":{"description":"`externalUsername must be a string`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`passwordless_login_disabled`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`no_passkey_registered` (only with `externalUsername`) — fall back to your password sign-in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/authenticate/primary/verify":{"post":{"tags":["WebAuthn"],"operationId":"webauthnPrimaryVerify","summary":"Finish passwordless passkey sign-in","description":"Verifies the assertion, then runs the risk engine and the workspace's country and access rules on the end user's `clientIp`. Success records a CONFIRMED login request (`confirmedVia: WEBAUTHN_PRIMARY`) and fires the SUCCESS webhook. If the engine answers CHALLENGE or asks for number matching (also when it is unreachable), an access rule requires step-up, the access policy cannot be read, or the country cannot be verified under a country rule, answers 200 with `requiresStepUp: true` — continue with `POST /login/request`. `alwaysRequireChallenge` does not step up a passkey. The attempt id is single-use even when verification fails.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnPrimaryVerifyRequest"}}}},"responses":{"200":{"description":"Signed in, or step-up required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebAuthnPrimaryResult"}}}},"400":{"description":"Missing fields, `Login session not found or expired`, `Verification failed` (+`message`), `Login could not be verified`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`passwordless_login_disabled`, `Login blocked by access policy` (+`ruleName`), `{ code: 'blocked', reason: 'geo_policy' }` (a country rule, +`countryCode`) or `{ code: 'blocked', reason: 'risk' }` (the risk engine). Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`This credential is not registered for this user`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/credentials":{"get":{"tags":["WebAuthn"],"operationId":"listWebAuthnCredentials","summary":"List a user's passkeys","description":"Labels and metadata only, no key material. Lists credentials of every relying party of the workspace.","parameters":[{"name":"externalUsername","in":"query","required":true,"description":"The user.","schema":{"type":"string"}}],"responses":{"200":{"description":"Credentials.","content":{"application/json":{"schema":{"type":"object","required":["credentials"],"properties":{"credentials":{"type":"array","items":{"$ref":"#/components/schemas/WebAuthnCredentialSummary"}}}}}}},"400":{"description":"`externalUsername query parameter is required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/webauthn/credentials/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Credential record `id` from `GET /webauthn/credentials` (not the WebAuthn `credentialId`).","schema":{"type":"string"}}],"delete":{"tags":["WebAuthn"],"operationId":"deleteWebAuthnCredential","summary":"Remove a passkey","description":"A removed passkey sends the `PASSKEY_DELETED` event webhook.","parameters":[{"name":"externalUsername","in":"query","required":true,"description":"The user the passkey belongs to. Required since 3.3.0: a credential of another user is `404`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","required":["deleted"],"properties":{"deleted":{"type":"boolean","const":true}}}}}},"400":{"description":"`externalUsername is required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`Credential not found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device/trust":{"get":{"tags":["Devices"],"operationId":"listTrustedDevices","summary":"List a user's trusted devices","description":"Devices the user chose to trust for N days, unexpired only, newest first. Access policy rules see trust as `device.trusted`; it does not waive number matching.","parameters":[{"name":"externalUsername","in":"query","required":true,"description":"The user.","schema":{"type":"string"}}],"responses":{"200":{"description":"Trusted devices.","content":{"application/json":{"schema":{"type":"object","required":["devices"],"properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/TrustedDevice"}}}}}}},"400":{"description":"`externalUsername query parameter is required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device/trust/revoke":{"post":{"tags":["Devices"],"operationId":"revokeDeviceTrust","summary":"Revoke device trust","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeTrustRequest"}}}},"responses":{"200":{"description":"Number of revoked trust records.","content":{"application/json":{"schema":{"type":"object","required":["revoked"],"properties":{"revoked":{"type":"integer","minimum":0}}}}}},"400":{"description":"`externalUsername is required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`Integration not found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device/flag-device":{"post":{"tags":["Devices"],"operationId":"flagDevice","summary":"Flag a device as fraudulent (Trust Network)","description":"Shares a fraud signal so other opted-in workspaces score that phone as risky. Requires `TenantPolicy.trustNetworkShareEnabled`, and the device must have confirmed at least one login for this user in your workspace. Note the reason values are upper case here (unlike `report-fraud`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagDeviceRequest"}}}},"responses":{"200":{"description":"Flagged.","content":{"application/json":{"schema":{"type":"object","required":["flagged","deviceId"],"properties":{"flagged":{"type":"boolean","const":true},"deviceId":{"type":"string"}}}}}},"400":{"description":"Missing fields or invalid `reason`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`TRUST_NETWORK_NOT_ENABLED`, `NO_LOGIN_HISTORY` (each with `message`), or `Integration not found`. Authentication failures (unknown, revoked or expired key, a key whose creator left its organization, invalid signature, IP not allowed, WAF block) also answer 403 — see the `Forbidden` response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/logs":{"get":{"tags":["Logs"],"operationId":"listLogs","summary":"Fetch the workspace's event log","description":"The 500 most recent log entries, newest first, optionally filtered. With `format=csv`, a CSV attachment (`logs.csv`) with the columns `type,username,ip,userAgent,createdAt,note`.","parameters":[{"name":"type","in":"query","required":false,"description":"Exact event type (e.g. `LOGIN_CONFIRM`).","schema":{"type":"string"}},{"name":"ip","in":"query","required":false,"description":"Exact IP address.","schema":{"type":"string"}},{"name":"username","in":"query","required":false,"description":"Exact `externalUsername` (named `username` here).","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"`csv` for a CSV download; anything else returns JSON.","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"Log entries.","content":{"application/json":{"schema":{"type":"array","maxItems":500,"items":{"$ref":"#/components/schemas/LogEntry"}}},"text/csv":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"`Failed to fetch logs`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/ping":{"get":{"tags":["Meta"],"operationId":"ping","summary":"Liveness check","security":[],"responses":{"200":{"description":"Alive.","content":{"application/json":{"schema":{"type":"object","required":["pong"],"properties":{"pong":{"type":"boolean","const":true}}}}}}}}},"/health":{"get":{"tags":["Meta"],"operationId":"health","summary":"Health check (database + security controls)","security":[],"responses":{"200":{"description":"Healthy (security controls may still report `degraded`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"500":{"description":"Database unreachable (`status: degraded`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}},"/c/{token}.gif":{"parameters":[{"name":"token","in":"path","required":true,"description":"The token in `canary.url` from `POST /login/request`.","schema":{"type":"string"}}],"get":{"tags":["Mirror Alarm"],"operationId":"getMirrorCanary","summary":"Mirror Alarm canary image (end user's browser)","description":"Loaded by the page that shows the sign-in, as a hidden image (`@touchque/web` does it). The browser's `Referer` tells Touchque which page that is; one that is not on your site marks the request `phishing_origin`. Always the same 1x1 GIF with `Cache-Control: no-store`, whether or not the token is known. No CORS or cross-origin resource policy applies.","security":[],"responses":{"200":{"description":"1x1 transparent GIF.","content":{"image/gif":{"schema":{"type":"string","contentEncoding":"binary"}}}}}}},"/c/{token}":{"parameters":[{"name":"token","in":"path","required":true,"description":"The token in `canary.url`, without `.gif`.","schema":{"type":"string"}}],"post":{"tags":["Mirror Alarm"],"operationId":"postMirrorBeacon","summary":"Mirror Alarm beacon (end user's browser)","description":"Sent with `navigator.sendBeacon` by the page that shows the sign-in (`@touchque/web` does it): `{ \"origin\": location.origin, \"top\": window.top === window, \"referrer\": document.referrer }` as `text/plain` JSON, so no CORS preflight is needed. Read together with the browser's own `Origin` header. Always 204.","requestBody":{"required":false,"content":{"text/plain":{"schema":{"type":"object","properties":{"origin":{"type":"string","description":"`location.origin` of the page."},"top":{"type":"boolean","description":"false when the page runs in a frame."},"referrer":{"type":"string","description":"`document.referrer` (only its host is kept)."}}}}}},"security":[],"responses":{"204":{"description":"Received (also for an unknown or expired token)."}}}},"/openapi.json":{"get":{"tags":["Meta"],"operationId":"getOpenApiSpec","summary":"This OpenAPI document","description":"Served with `Cache-Control: public, max-age=300`.","security":[],"responses":{"200":{"description":"OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/agents":{"post":{"tags":["Agents"],"operationId":"createAgent","summary":"Register an AI agent","description":"An agent is a key, not a name: generate a key pair where the agent runs (the SDKs have a helper) and register only the public key. The agent then authenticates itself (`POST /agents/token`); it never holds your `tq_` key. Needs the `agents:write` scope. At most 200 agents per workspace.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgent"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"`invalid_request`, `invalid_key`, `unknown_type`, `invalid_owner`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`agent_limit`; `name_too_similar` (with `similarTo`): the name reads like another agent's of the workspace (look-alike letters, 0/O, 1/l, case, spaces), and the phone shows the agent's name first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"tags":["Agents"],"operationId":"listAgents","summary":"The workspace's agents and its kill switch","parameters":[{"name":"status","in":"query","required":false,"description":"Only agents in this status.","schema":{"type":"string","enum":["active","suspended","revoked"]}}],"responses":{"200":{"description":"Agents.","content":{"application/json":{"schema":{"type":"object","properties":{"agents":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}},"killSwitch":{"$ref":"#/components/schemas/AgentKillSwitch"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/kill-switch":{"put":{"tags":["Agents"],"operationId":"setAgentKillSwitch","summary":"Stop (or restart) every agent of the workspace at once","description":"While engaged, every agent call and token request is refused (`workspace_paused`), including tokens issued before.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["engaged"],"properties":{"engaged":{"type":"boolean"},"reason":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"The kill switch.","content":{"application/json":{"schema":{"type":"object","properties":{"killSwitch":{"$ref":"#/components/schemas/AgentKillSwitch"}}}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/workspace-policy":{"get":{"tags":["Agents"],"operationId":"getAgentWorkspacePolicy","summary":"The floor every agent's policy sits on","description":"The workspace's own rules for all its agents (an agent's policy can only be stricter), with the defaults.","responses":{"200":{"description":"The workspace policy.","content":{"application/json":{"schema":{"type":"object","properties":{"policy":{"type":"object"},"defaults":{"type":"object"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Agents"],"operationId":"setAgentWorkspacePolicy","summary":"Set the floor for every agent's policy","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"policy":{"type":"object"}}}}}},"responses":{"200":{"description":"The saved policy.","content":{"application/json":{"schema":{"type":"object","properties":{"policy":{"type":"object"}}}}}},"400":{"description":"The policy's own validation code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/approval-audience":{"get":{"tags":["Agents"],"operationId":"getAgentApprovalAudience","summary":"The audience of this workspace's approval tokens","description":"The `aud` (`touchque:workspace:<id>`) every approval token issued for this workspace carries. The SDKs' offline check (`verifyApprovalToken`) fetches it once and refuses a token without it or with another workspace's, since `GET /agents/jwks.json` is one key set for every workspace.","responses":{"200":{"description":"The audience.","content":{"application/json":{"schema":{"type":"object","required":["audience"],"properties":{"audience":{"type":"string","example":"touchque:workspace:6b8e3069-0000-4000-8000-000000000000"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/events":{"get":{"tags":["Agents"],"operationId":"listAgentEvents","summary":"The workspace's agent events, for a SIEM","description":"Oldest first. Page with `after` (a cursor this API gave); start with `since` (a date) or from the beginning.","parameters":[{"name":"limit","in":"query","required":false,"description":"How many events (1 to the page maximum).","schema":{"type":"integer"}},{"name":"after","in":"query","required":false,"description":"The cursor of the last page.","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Start at this time (ISO 8601) when there is no cursor.","schema":{"type":"string"}}],"responses":{"200":{"description":"Events and the next cursor.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_cursor`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}":{"get":{"tags":["Agents"],"operationId":"getAgent","summary":"An agent","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"responses":{"200":{"description":"The agent.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Agents"],"operationId":"updateAgent","summary":"Change, hand over or renew an agent","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgent"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"`invalid_request`, `unknown_type`, `invalid_owner`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`agent_revoked`, `name_too_similar`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Agents"],"operationId":"revokeAgent","summary":"Revoke an agent for good","description":"Its tokens stop at once. The record stays (status `revoked`) for the audit trail.","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/keys":{"post":{"tags":["Agents"],"operationId":"addAgentKey","summary":"Add a key (rotation)","description":"Add the new key, move the agent to it, then remove the old one. At most 3 keys.","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["publicKey"],"properties":{"publicKey":{"$ref":"#/components/schemas/PublicJwk"}}}}}},"responses":{"201":{"description":"Added.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"`invalid_key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`key_exists`, `key_limit`, `agent_revoked`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/keys/{kid}":{"delete":{"tags":["Agents"],"operationId":"removeAgentKey","summary":"Remove a key","description":"Tokens the agent got with this key stop at once.","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}},{"name":"kid","in":"path","required":true,"description":"The key's kid.","schema":{"type":"string"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`, `key_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`last_key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/suspend":{"post":{"tags":["Agents"],"operationId":"suspendAgent","summary":"Suspend an agent","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Suspended.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`agent_revoked`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/resume":{"post":{"tags":["Agents"],"operationId":"resumeAgent","summary":"Resume a suspended agent","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"responses":{"200":{"description":"Active again.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`agent_revoked`, `review_overdue` (renew first).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/policy":{"put":{"tags":["Agents"],"operationId":"setAgentPolicy","summary":"Set the agent's budgets, amounts, recipients and background rule","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["policy"],"properties":{"policy":{"$ref":"#/components/schemas/AgentPolicy"}}}}}},"responses":{"200":{"description":"The agent with its policy.","content":{"application/json":{"schema":{"type":"object","required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/Agent"}}}}}},"400":{"description":"`invalid_policy`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`agent_revoked`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/blocks":{"get":{"tags":["Agents"],"operationId":"listAgentBlocks","summary":"The people who stopped this agent asking them","description":"A person stops an agent by answering \"this wasn't me\" or \"someone told me to\" on the phone, or by rejecting it three times in 30 minutes. Its requests to them answer 403 `blocked_by_person` until you lift it.","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}}],"responses":{"200":{"description":"Blocks.","content":{"application/json":{"schema":{"type":"object","properties":{"blocks":{"type":"array","items":{"type":"object","properties":{"externalUsername":{"type":"string"},"reason":{"type":"string","enum":["not_me","coached","rejections","phone"],"description":"`phone`: the person blocked the agent on their phone (Block this agent, or in the app's AI agents settings)."},"approvalId":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/blocks/{externalUsername}":{"delete":{"tags":["Agents"],"operationId":"liftAgentBlock","summary":"Let the agent ask this person again","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}},{"name":"externalUsername","in":"path","required":true,"description":"The person.","schema":{"type":"string"}}],"responses":{"200":{"description":"Lifted.","content":{"application/json":{"schema":{"type":"object","properties":{"lifted":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`agent_not_found`, `block_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/token":{"post":{"tags":["Agents"],"operationId":"agentToken","summary":"An agent gets its own token (client credentials, private_key_jwt, DPoP)","description":"Called by the agent, not by your server. Form-encoded (or JSON):\n\n- `grant_type=client_credentials`\n- `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`\n- `client_assertion`: a JWT (RFC 7523) signed with one of the agent's registered keys (`kid` in the header), `iss` = `sub` = the agent id, `aud` = this URL, `exp` at most 5 minutes after `iat`, a unique `jti` (each assertion is accepted once).\n- `scope` (optional): a subset of `agent:read approvals:request approvals:read vault:read`.\n\nA `DPoP` proof header (RFC 9449) is required: the token is bound to its key (`token_type: DPoP`) and every later call must prove that key. Tokens live 10 minutes; there is no refresh token, the agent asks again. A suspended, revoked or overdue agent, or a paused workspace, answers `unauthorized_client` with `reason`.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type","client_assertion_type","client_assertion"],"properties":{"grant_type":{"type":"string","const":"client_credentials"},"client_assertion_type":{"type":"string"},"client_assertion":{"type":"string"},"scope":{"type":"string"}}}}}},"security":[],"responses":{"200":{"description":"A DPoP-bound token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentToken"}}}},"400":{"description":"`invalid_dpop_proof`, `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"`invalid_client`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"`slow_down`: 60 requests a minute per address, 30 per agent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/agents/approvals":{"post":{"tags":["Agents"],"operationId":"requestAgentApproval","summary":"The agent asks a person to approve on their phone (agent token)","description":"Goes through the same pipeline as POST /login/request (freeze, push limit, risk engine, Actions, Vishing Shield, case code). The phone shows rows Touchque drew from `authorizationDetails` (the agent first, its reason marked unverified) and needs Face ID or the hardware key. At most 60 requests per agent and 5 per agent and person in 10 minutes. The risk engine also checks the agent against its own history (a burst of requests, an unusual amount, a new network, an unusual hour, a recipient new to this person): a step-up, which asks the person for the number. Every refusal carries a `caseCode` (\"Why?\").","security":[{"AgentDPoP":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentApprovalRequest"}}}},"responses":{"201":{"description":"Sent to the phone (or, within a mandate, approved at once: status approved and the mandate).","content":{"application/json":{"schema":{"type":"object","properties":{"approvalId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","approved"]},"secondApprover":{"type":"string"},"mandate":{"$ref":"#/components/schemas/Mandate"},"presence":{"type":"string"},"challengeCode":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"reasons":{"type":"array","items":{"type":"object"}},"caseCode":{"type":"string"}}}}}},"400":{"description":"`invalid_request`, `invalid_authorization_details`, `unknown_action`; `suspicious_content` (with `markers`: `instruction`, `data_url`): the agent's text carries an instruction to the model or a link built to carry data out, so whatever the agent read told it to ask; nothing reaches the phone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"description":"`invalid_token`, `invalid_dpop_proof`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"403":{"description":"`type_not_allowed`, `background_not_allowed`, `passkey_only_type`, `passkey_required`, `no_second_approver`, `four_eyes_required`, `mandate_mismatch`, `mandate_exceeded` (per request), `blocked_by_person`, `amount_over_limit`, `daily_amount_over_limit`, `recipient_not_allowed`, `new_recipient`, `agent_blocked`, `insufficient_scope`, and the refusals of POST /login/request (`blocked`, …).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"description":"`device_not_linked`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`interactive_required`: the checks asked for number matching and the agent asked in the background, where nobody reads the number; ask again with presence interactive. Within a mandate: `mandate_pending`, `mandate_expired`, `mandate_revoked`, `mandate_exhausted`, `mandate_exceeded` (no room left).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"423":{"description":"`frozen`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`rate_limited` (the agent's policy), `loop_detected` (the same request to the same person three times in 10 minutes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/agents/approvals/{approvalId}":{"get":{"tags":["Agents"],"operationId":"getAgentApproval","summary":"Where the agent's request is; the approval token once approved (agent token)","security":[{"AgentDPoP":[]}],"parameters":[{"name":"approvalId","in":"path","required":true,"description":"From POST /agents/approvals.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentApprovalStatus"}}}},"401":{"description":"`invalid_token`, `invalid_dpop_proof`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"403":{"description":"`agent_blocked`, `insufficient_scope`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"description":"`approval_not_found` (also another agent's request).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/agents/approvals/redeem":{"post":{"tags":["Agents"],"operationId":"redeemAgentApproval","summary":"Use an agent's approval exactly once, before doing the thing","description":"Your server calls this with the token the agent hands it, right before executing. Touchque checks its signature, workspace and expiry and that the approval is still usable, marks it used and answers what the person approved. Compare `authorizationDetails` with what you are about to execute (the SDKs do it with `expect`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}}}}},"responses":{"200":{"description":"The approval, now used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeemedApproval"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"description":"`invalid_token` (not signed by Touchque, another workspace, unknown), `token_expired`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`already_redeemed`, or the approval's state (`provisional`, `revoked`, `stale`…).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/mandates":{"post":{"tags":["Agents"],"operationId":"requestAgentMandate","summary":"The agent asks for a mandate: one approval for a bounded series of requests (agent token)","description":"The phone shows what the mandate allows (type, how many, total, per request, recipients, until when) with number matching. Requests that would need four eyes cannot have a mandate.","security":[{"AgentDPoP":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MandateRequest"}}}},"responses":{"201":{"description":"Sent to the phone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mandate"}}}},"400":{"description":"`invalid_authorization_details`, `invalid_request`, `unknown_action`, `suspicious_content`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"403":{"description":"`type_not_allowed`, `background_not_allowed`, `four_eyes_required`, `blocked_by_person`, `amount_over_limit`, `recipient_not_allowed`, `passkey_only_type`, and the refusals of POST /login/request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/agents/mandates/{mandateId}":{"get":{"tags":["Agents"],"operationId":"getAgentMandate","summary":"A mandate's state and what is left (agent token)","security":[{"AgentDPoP":[]}],"parameters":[{"name":"mandateId","in":"path","required":true,"description":"The mandate.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The mandate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mandate"}}}},"404":{"description":"`mandate_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/agents/mandates/{mandateId}/revoke":{"post":{"tags":["Agents"],"operationId":"revokeOwnAgentMandate","summary":"The agent gives up its mandate (agent token)","security":[{"AgentDPoP":[]}],"parameters":[{"name":"mandateId","in":"path","required":true,"description":"The mandate.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mandate"}}}},"404":{"description":"`mandate_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/agents/vault/connections":{"get":{"tags":["Agents"],"operationId":"listAgentVaultConnections","summary":"Token vault: the connections to other services","responses":{"200":{"description":"Connections, with how many people have tokens kept.","content":{"application/json":{"schema":{"type":"object","properties":{"connections":{"type":"array","items":{"$ref":"#/components/schemas/AgentVaultConnection"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/vault/connections/{name}":{"put":{"tags":["Agents"],"operationId":"putAgentVaultConnection","summary":"Token vault: add or replace a connection","description":"Your own OAuth app at the service (Touchque refreshes people's tokens with it) and the agents that may get tokens from it. `clientSecret` is kept when left out of a replacement.","parameters":[{"name":"name","in":"path","required":true,"description":"Lowercase letters, digits and dashes.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["provider","clientId"],"properties":{"provider":{"type":"string","enum":["google","microsoft","github","facebook","linkedin","x","oidc"]},"clientId":{"type":"string"},"clientSecret":{"type":"string"},"tenant":{"type":"string","description":"Microsoft: common, organizations, consumers or a directory id."},"issuer":{"type":"string","description":"OpenID Connect: the issuer URL."},"agentIds":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Replaced.","content":{"application/json":{"schema":{"type":"object","properties":{"connection":{"$ref":"#/components/schemas/AgentVaultConnection"}}}}}},"201":{"description":"Added.","content":{"application/json":{"schema":{"type":"object","properties":{"connection":{"$ref":"#/components/schemas/AgentVaultConnection"}}}}}},"400":{"description":"`invalid_request`, `unknown_agent`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Agents"],"operationId":"removeAgentVaultConnection","summary":"Token vault: remove a connection and every person's tokens kept for it","parameters":[{"name":"name","in":"path","required":true,"description":"The connection.","schema":{"type":"string"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"boolean"},"tokensDeleted":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`connection_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/vault/accounts":{"get":{"tags":["Agents"],"operationId":"listAgentVaultAccounts","summary":"Token vault: a person's connected accounts (never a token)","parameters":[{"name":"externalUsername","in":"query","required":false,"description":"The person.","schema":{"type":"string"}}],"responses":{"200":{"description":"Accounts.","content":{"application/json":{"schema":{"type":"object","properties":{"accounts":{"type":"array","items":{"$ref":"#/components/schemas/AgentVaultAccount"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/vault/accounts/{connection}/{externalUsername}":{"put":{"tags":["Agents"],"operationId":"depositAgentVaultTokens","summary":"Token vault: keep a person's tokens after your own OAuth flow with them","parameters":[{"name":"connection","in":"path","required":true,"description":"The connection.","schema":{"type":"string"}},{"name":"externalUsername","in":"path","required":true,"description":"The person.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["accessToken"],"properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"type":"integer"},"scope":{"type":"string"}}}}}},"responses":{"200":{"description":"Kept, sealed.","content":{"application/json":{"schema":{"type":"object","properties":{"account":{"$ref":"#/components/schemas/AgentVaultAccount"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`connection_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Agents"],"operationId":"removeAgentVaultTokens","summary":"Token vault: delete a person's tokens","parameters":[{"name":"connection","in":"path","required":true,"description":"The connection.","schema":{"type":"string"}},{"name":"externalUsername","in":"path","required":true,"description":"The person.","schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`account_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/vault/token":{"post":{"tags":["Agents"],"operationId":"agentVaultToken","summary":"A person's own access token at another service (agent token with vault:read)","description":"For a connection that names this agent: the kept access token, refreshed by Touchque when it is about to expire. The refresh token never leaves. `scope` may only narrow what the person granted.","security":[{"AgentDPoP":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["externalUsername","connection"],"properties":{"externalUsername":{"type":"string"},"connection":{"type":"string"},"scope":{"type":"string"}}}}}},"responses":{"200":{"description":"A Bearer token for the service.","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"},"scope":{"type":"string"},"connection":{"type":"string"}}}}}},"403":{"description":"`connection_not_allowed`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"description":"`connection_not_found`, `not_connected`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`no_refresh_token`, `refresh_failed`: deposit new tokens.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/agents/{agentId}/mandates":{"get":{"tags":["Agents"],"operationId":"listAgentMandates","summary":"An agent's mandates","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}},{"name":"externalUsername","in":"query","required":false,"description":"Only this person's.","schema":{"type":"string"}}],"responses":{"200":{"description":"Mandates.","content":{"application/json":{"schema":{"type":"object","properties":{"mandates":{"type":"array","items":{"$ref":"#/components/schemas/Mandate"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/{agentId}/mandates/{mandateId}/revoke":{"post":{"tags":["Agents"],"operationId":"revokeAgentMandate","summary":"End an agent's mandate at once","parameters":[{"name":"agentId","in":"path","required":true,"description":"The agent's id.","schema":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"}},{"name":"mandateId","in":"path","required":true,"description":"The mandate.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Mandate"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`mandate_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/agents/jwks.json":{"get":{"tags":["Agents"],"operationId":"agentApprovalKeys","summary":"The keys approval tokens are signed with","description":"For servers that verify approval tokens themselves. Redeeming still makes a token single-use.","security":[],"responses":{"200":{"description":"JWKS.","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object"}}}}}}}}}},"/agents/me":{"get":{"tags":["Agents"],"operationId":"agentMe","summary":"The agent as it sees itself (agent token)","security":[{"AgentDPoP":[]}],"responses":{"200":{"description":"The agent.","content":{"application/json":{"schema":{"type":"object","properties":{"agent":{"type":"object"},"workspace":{"type":"object","properties":{"companyName":{"type":"string"}}},"scope":{"type":"string"}}}}}},"401":{"description":"`invalid_token`, `invalid_dpop_proof` (with a `WWW-Authenticate: DPoP` header).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"403":{"description":"`agent_blocked` (with `reason`), `insufficient_scope`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/mobile/enrollments":{"post":{"tags":["Mobile SDK"],"operationId":"createMobileEnrollment","summary":"Enrol a user's device inside your app (Mobile SDK)","description":"Issues a one-time enrollment token for one of your signed-in users. Your app passes it to the Touchque SDK (`Touchque.enroll`), which creates its hardware key, proves the app when it can, and becomes the user's approver: `/login/request` then reaches your app instead of the Touchque app. You authenticate the user before asking — the token makes whoever spends it their approver. A user who already approves from a device needs `replace: true` (the old device stops when the new one enrols); a frozen user (a request reported as not theirs) too. Earlier unused tokens of the user stop working. Then a `DEVICE_LINKED` webhook arrives with `via: mobile_sdk`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string","maxLength":255},"replace":{"type":"boolean","description":"Move the user from the device they approve with today."},"ttlSeconds":{"type":"integer","minimum":60,"maximum":86400,"description":"Default: the workspace's `enrollTtlSeconds` (600)."}}}}}},"responses":{"201":{"description":"The token. Not cached (`Cache-Control: no-store`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MobileEnrollment"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`already_enrolled` (with `deviceId`, `channel`: `mobile_sdk` | `touchque_app`, `platform`) or `account_frozen`: send `replace: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/mobile/devices":{"get":{"tags":["Mobile SDK"],"operationId":"listMobileDevices","summary":"List the devices inside your app","parameters":[{"name":"externalUsername","in":"query","required":false,"description":"Only this user's devices.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Default `active`.","schema":{"type":"string","enum":["active","revoked","all"]}},{"name":"limit","in":"query","required":false,"description":"1–50, default 20.","schema":{"type":"integer","minimum":1,"maximum":50}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/MobileDevice"}},"nextCursor":{"type":["string","null"]}}}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/mobile/devices/{deviceId}":{"get":{"tags":["Mobile SDK"],"operationId":"getMobileDevice","summary":"A device inside your app","parameters":[{"name":"deviceId","in":"path","required":true,"description":"The mobile SDK device's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The device.","content":{"application/json":{"schema":{"type":"object","properties":{"device":{"$ref":"#/components/schemas/MobileDevice"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`device_not_found`: no mobile SDK device with this id in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Mobile SDK"],"operationId":"removeMobileDevice","summary":"Stop a device inside your app from approving","description":"Revokes the device. If it is the user's approver, the user has no approver until you enrol again; requests waiting on it expire, and a `DEVICE_UNLINKED` webhook arrives with `via: mobile_sdk`. The SDK sees it on its next check and signs itself out.","parameters":[{"name":"deviceId","in":"path","required":true,"description":"The mobile SDK device's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"boolean"},"wasLinked":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`device_not_found`: no mobile SDK device with this id in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/mobile/devices/{deviceId}/test-push":{"post":{"tags":["Mobile SDK"],"operationId":"testMobilePush","summary":"Send a test push to a device inside your app","description":"A harmless push the way a request would reach the device: sent by Touchque with your APNs key / FCM service account for its platform (see `push` in the settings), otherwise a `MOBILE_PUSH` webhook with `kind: test`. The SDK only connects and finds nothing waiting. Tells you which way it went and whether APNs / FCM accepted it.","parameters":[{"name":"deviceId","in":"path","required":true,"description":"The mobile SDK device's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Sent (or tried).","content":{"application/json":{"schema":{"type":"object","properties":{"via":{"type":"string","enum":["touchque","webhook"]},"ok":{"type":"boolean","description":"APNs / FCM accepted it, or the webhook was handed to your URL."},"reason":{"type":"string","description":"Why not, e.g. BadDeviceToken, UNREGISTERED, no_webhook_url."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`device_not_found`: no mobile SDK device with this id in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`device_revoked` or `no_push_token` (the app has not called Touchque.setPushToken).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/mobile/settings":{"get":{"tags":["Mobile SDK"],"operationId":"getMobileSdkSettings","summary":"Your Mobile SDK settings","responses":{"200":{"description":"The settings (defaults when never set).","content":{"application/json":{"schema":{"type":"object","properties":{"settings":{"$ref":"#/components/schemas/MobileSdkSettings"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Mobile SDK"],"operationId":"updateMobileSdkSettings","summary":"Set which apps may enrol, whether they must prove themselves, and your push keys","description":"Only the fields sent change. App Attest and Android Key Attestation need Touchque's trust anchors, which production has; set `attestation: required` once your released apps send evidence. `apns` / `fcm`: your own APNs key (.p8) / Firebase service account; Touchque then sends that platform's push itself (alert + the `touchque` wake-up data) instead of the `MOBILE_PUSH` webhook. The keys are sealed (AES-256-GCM) and never returned; `null` removes one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"iosAppIds":{"type":"array","items":{"type":"string"}},"androidPackages":{"type":"array","items":{"type":"string"}},"androidCerts":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Package name → SHA-256 fingerprints (\"AB:CD:…\" or plain hex). Certificates of apps not in androidPackages are dropped."},"attestation":{"type":"string","enum":["off","optional","required"]},"enrollTtlSeconds":{"type":"integer"},"apns":{"type":["object","null"],"properties":{"keyId":{"type":"string","pattern":"^[A-Z0-9]{10}$"},"teamId":{"type":"string","pattern":"^[A-Z0-9]{10}$"},"topic":{"type":"string","description":"Your app's bundle id."},"key":{"type":"string","writeOnly":true,"description":"The .p8 file's text (P-256). Leave it out to keep the stored key."},"production":{"type":"boolean","description":"APNs production (App Store / TestFlight builds); default false (development)."}}},"fcm":{"type":["object","null"],"properties":{"serviceAccount":{"type":["object","string"],"writeOnly":true,"description":"The Firebase service account JSON (project_id, client_email, private_key)."}}}}}}}},"responses":{"200":{"description":"The settings now.","content":{"application/json":{"schema":{"type":"object","properties":{"settings":{"$ref":"#/components/schemas/MobileSdkSettings"}}}}}},"400":{"description":"`invalid_settings`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assist":{"post":{"tags":["Help Desk"],"operationId":"openAssist","summary":"Start helping a person","description":"Call it before your help desk acts on someone's account: a short-lived session bound to the agent and the person. By default their phone gets a calm notice, nothing to approve: \"<agent> from IT is working on your account now.\" While it is open, the person's \"someone says they're from IT\" button shows this agent; with none open it tells them to hang up, and you get `ASSIST_CLAIM_UNVERIFIED`.\n\nBefore a sensitive action, ask for a check (`POST /assist/{id}/verify`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistOpenRequest"}}}},"responses":{"201":{"description":"Open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistSession"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`user_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"tags":["Help Desk"],"operationId":"listAssists","summary":"List assist sessions","description":"A person's sessions, or every open one, newest first (50 at most).","parameters":[{"name":"externalUsername","in":"query","required":false,"description":"The person.","schema":{"type":"string","maxLength":255}},{"name":"open","in":"query","required":false,"description":"`true`: open ones only.","schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"Sessions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistSessionList"}}}},"400":{"description":"`invalid_request` — pass `externalUsername` or `open=true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assist/{id}":{"get":{"tags":["Help Desk"],"operationId":"getAssist","summary":"Assist session and its checks","description":"Poll it while a check is `pending`.","parameters":[{"name":"id","in":"path","required":true,"description":"The assist session id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`assist_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assist/{id}/verify":{"post":{"tags":["Help Desk"],"operationId":"verifyAssist","summary":"Check the person before a sensitive action","description":"Sends the person's phone a critical, number-matched request (type `HELPDESK_VERIFY`): \"IT (<agent>) is about to <action> because someone on a call asked. Is that you on the call?\" Show `challengeCode` to the agent; they read it to the person, who types it and approves with Face ID. Default deny. Wrong digits, \"this isn't me\" or \"someone told me to\" block it and raise an alert (`ASSIST_CHECK_FAILED`); \"this isn't me\" also freezes the account. The Vishing Shield does not block it: a call with IT is expected here.\n\nNo working phone (`409 no_phone`, `route: never_locked_out`): the person moves to a new phone with Never Locked Out (`/recovery`). The help desk cannot do it for them.","parameters":[{"name":"id","in":"path","required":true,"description":"The assist session id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistVerifyRequest"}}}},"responses":{"201":{"description":"Sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistVerifyResponse"}}}},"400":{"description":"`invalid_request`, `invalid_details`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`assist_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`assist_not_open`, `no_phone` (with `route: never_locked_out`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`rate_limited` — ten checks per session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assist/{id}/spend":{"post":{"tags":["Help Desk"],"operationId":"spendAssistCheck","summary":"Use a passed check for its action","description":"Call it right before you take the action: a `verified` check for the same `action`, at most ten minutes old, used once. Anything else is `428 check_required`.","parameters":[{"name":"id","in":"path","required":true,"description":"The assist session id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistSpendRequest"}}}},"responses":{"200":{"description":"Used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`assist_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`assist_not_open`, `busy`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"428":{"description":"`check_required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assist/{id}/unfreeze":{"post":{"tags":["Help Desk"],"operationId":"unfreezeAssist","summary":"Unlock the person's approvals","description":"Lifts a freeze (after \"this isn't me\" or repeated rejections), using a passed check for `unlock`.","parameters":[{"name":"id","in":"path","required":true,"description":"The assist session id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requestId"],"properties":{"requestId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistUnfreezeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`assist_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`assist_not_open`, `busy`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"428":{"description":"`check_required`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/assist/{id}/end":{"post":{"tags":["Help Desk"],"operationId":"endAssist","summary":"Stop helping","description":"Ends the session; checks still waiting on the phone expire.","parameters":[{"name":"id","in":"path","required":true,"description":"The assist session id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistEndRequest"}}}},"responses":{"200":{"description":"Ended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`assist_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/identify":{"post":{"tags":["Device Risk"],"operationId":"identifyDeviceRiskVisitor","summary":"Identify the visitor on a page (the end user's browser)","description":"Sent by `@touchque/web` `deviceRisk.identify({ siteKey, purpose })` from your sign-in, sign-up, password-reset or approval page, as `text/plain` JSON so no CORS preflight is needed. Only from pages on your organization's verified domains (Organization → Domains) or your listed origins (Device risk settings). The page gets `{ visitorId, requestId, confidence }` only: your server reads the signals and the verdict by `requestId`. Rate limited per address (60 a minute) and per site key (the settings' `ratePerMinute`, 1200 by default). Through your own first-party proxy, send `X-Touchque-Proxy-Secret` (the settings' `proxySecret`) and `X-Touchque-Client-IP` (the visitor's address) so the visitor's address is counted, not the proxy's.","parameters":[{"name":"X-Touchque-Proxy-Secret","in":"header","required":false,"schema":{"type":"string"},"description":"Your first-party proxy only (server-side, never in the page)."},{"name":"X-Touchque-Client-IP","in":"header","required":false,"schema":{"type":"string"},"description":"The visitor's address; read only with a valid `X-Touchque-Proxy-Secret`."}],"requestBody":{"required":true,"content":{"text/plain":{"schema":{"type":"object","required":["siteKey","report"],"properties":{"siteKey":{"type":"string"},"report":{"$ref":"#/components/schemas/DeviceRiskReport"},"view":{"type":"boolean","description":"Sent by the script tag, which identifies when the page loads: Touchque counts the identification when your server checks it, not per page view."}}}}}},"security":[],"responses":{"200":{"description":"The identification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRiskIdentification"}}}},"400":{"description":"`invalid_request`, `report_malformed`, `report_stale`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"403":{"description":"`unknown_site_key`, `device_risk_off`, `origin_not_allowed`, `proxy_secret_invalid`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`report_replayed`: this report was already sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`rate_limited`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/device-risk/events/{requestId}":{"get":{"tags":["Device Risk"],"operationId":"getDeviceRiskEvent","summary":"Read an identification (trusted)","description":"What Touchque saw for a `requestId` the page sent you: the visitor, how sure, what the page showed and the risk engine's verdict. Never trust the page's own copy.","parameters":[{"name":"requestId","in":"path","required":true,"description":"From deviceRisk.identify().","schema":{"type":"string","pattern":"^dre_[A-Za-z0-9_-]{22}$"}}],"responses":{"200":{"description":"The identification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRiskEvent"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/check":{"post":{"tags":["Device Risk"],"operationId":"checkDeviceRisk","summary":"Check an identification for an account (sign-up, reset, sign-in, approval)","description":"For pages login.request and risk.check do not cover (sign-up, password reset), or to know before the push: binds the identification to the account (one account per identification, 15 minutes) and asks the risk engine, which counts per visitor and decides. Every signal only asks for more and never blocks. `requestId` (the `touchque_request_id` form field the script tag fills, or deviceRisk.identify) or `deviceRisk` (deviceRisk.collect, relayed by your server). Send the field as it came: a missing, empty, unknown or expired one is still a verdict (`report`), never an error. Its own budget per workspace: the settings' `ratePerMinute` (1200 a minute by default), 429 with `Retry-After` above it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":["string","null"],"description":"As the page sent it (`dre_…`, and after a dot the 18 characters the script tag adds on submit: how the pointer and the fingers moved, as counts the browser computed, weighed by the risk engine as `device_pointer_synthetic` and never kept); missing or empty: a verdict with `report: missing`. The answer's `requestId` is the identification alone."},"deviceRisk":{"$ref":"#/components/schemas/DeviceRiskReport"},"event":{"type":"string","enum":["sign_in","sign_up","password_reset","approval"],"description":"Default `sign_in`."},"externalUsername":{"type":"string","maxLength":255,"description":"The account the page is for, when there is one (accounts per visitor)."},"clientIp":{"type":"string","description":"The end user's address; default: the address the browser identified from."}}}}}},"responses":{"200":{"description":"The visitor and the engine's verdict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRiskCheckResult"}}}},"400":{"description":"`invalid_request`: an unknown `event` or a bad `externalUsername`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`device_risk_off`: turn Device risk on in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/settings":{"get":{"tags":["Device Risk"],"operationId":"getDeviceRiskSettings","summary":"Device risk settings","responses":{"200":{"description":"The settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRiskSettings"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Device Risk"],"operationId":"updateDeviceRiskSettings","summary":"Turn Device risk on or off, set the retention, the page origins and the alerts","description":"In production a new origin must use https (http only for localhost).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"retentionDays":{"type":"integer","minimum":30,"maximum":90},"origins":{"type":"array","maxItems":20,"items":{"type":"string"}},"alerts":{"$ref":"#/components/schemas/DeviceRiskAlerts"}}}}}},"responses":{"200":{"description":"The settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRiskSettings"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/site-key/rotate":{"post":{"tags":["Device Risk"],"operationId":"rotateDeviceRiskSiteKey","summary":"Replace the site key","description":"Pages with the old key stop identifying.","responses":{"200":{"description":"The settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRiskSettings"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/overview":{"get":{"tags":["Device Risk"],"operationId":"getDeviceRiskOverview","summary":"Visitors, accounts per visitor, signals and coverage","responses":{"200":{"description":"Counts for the last 30 days (signals: 7 days); trend30d per UTC day.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/events":{"get":{"tags":["Device Risk"],"operationId":"searchDeviceRiskEvents","summary":"Search identifications","description":"The workspace's identifications, newest first, one page at a time: a date range (7 days by default, at most 90, within the retention) and any of the filters, each on an index. Pass `nextCursor` back as `cursor` until it is null; do not page by changing `from`/`to`. A search that would read too much answers 422 `search_too_broad`: narrow the range or add a filter. A workspace runs two searches at a time and 120 a minute. For everything at once use the export, the `device_risk.*` webhooks or the organization's log stream.","parameters":[{"name":"from","in":"query","required":false,"description":"Start: YYYY-MM-DD (UTC) or an ISO time. Default: 7 days before `to`.","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"End: YYYY-MM-DD (UTC, the whole day) or an ISO time. Default: now.","schema":{"type":"string"}},{"name":"signal","in":"query","required":false,"description":"Only identifications that raised this signal code.","schema":{"type":"string"}},{"name":"stepUp","in":"query","required":false,"description":"`true`: only the ones that asked for more; `false`: only the others.","schema":{"type":"boolean"}},{"name":"kind","in":"query","required":false,"description":"The page.","schema":{"type":"string","enum":["sign_in","sign_up","password_reset","approval"]}},{"name":"origin","in":"query","required":false,"description":"The page's origin, like https://shop.example.com.","schema":{"type":"string"}},{"name":"ip","in":"query","required":false,"description":"An address or a range in CIDR (`203.0.113.0/24`, `2001:db8:1::/48`): a range is /16 or narrower for IPv4 and /48 or narrower for IPv6. A filter only: no answer carries an address. Not with `requestId`.","schema":{"type":"string"}},{"name":"account","in":"query","required":false,"description":"The account (externalUsername).","schema":{"type":"string"}},{"name":"visitorId","in":"query","required":false,"description":"One visitor (`tqv_…`).","schema":{"type":"string"}},{"name":"requestId","in":"query","required":false,"description":"One identification (`dre_…`).","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"The `nextCursor` of the previous page (opaque).","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"1 to 100, default 25.","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"A page.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/DeviceRiskIdentification"}},"nextCursor":{"type":["string","null"]},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"signalTexts":{"type":"object"}}}}}},"400":{"description":"A filter that cannot be read: `invalid_range`, `range_too_long`, `out_of_retention`, `invalid_signal`, `invalid_ip`, `cidr_too_broad` (a range wider than /16 for IPv4 or /48 for IPv6), `invalid_filter` (`ip` with `requestId`), `invalid_cursor`, `invalid_limit`…","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"`search_too_broad`: narrow the date range or add a filter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`search_busy`: two searches of this workspace are already running; or `integration_quota_exceeded`: more than 120 searches a minute. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/trend":{"get":{"tags":["Device Risk"],"operationId":"getDeviceRiskTrend","summary":"Identifications, step-ups and signals per day","description":"One point per UTC day, today last, from the daily rows the nightly job writes (kept 400 days, past the identifications' retention) and live where a day has none yet.","parameters":[{"name":"days","in":"query","required":false,"description":"30, 90 or 365 (default 30).","schema":{"type":"integer","enum":[30,90,365]}}],"responses":{"200":{"description":"{ days, points }.","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"integer"},"points":{"type":"array","items":{"type":"object","properties":{"day":{"type":"string","format":"date"},"identifications":{"type":"integer"},"stepUps":{"type":"integer"},"signals":{"type":"integer"},"enforced":{"type":"integer"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/recent":{"get":{"tags":["Device Risk"],"operationId":"listRecentDeviceRiskIdentifications","summary":"The latest identifications (a live feed)","description":"Newest first: each identification with its visitorId, page, account, confidence and the risk engine's verdict (signals, enforced or not, case code). Coarse labels only, never a raw trait.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"{ events, signalTexts }.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/visitors":{"get":{"tags":["Device Risk"],"operationId":"listDeviceRiskVisitors","summary":"List visitors","parameters":[{"name":"filter","in":"query","required":false,"description":"`flagged`: a signal in the last 7 days; `marked`: trusted or step up.","schema":{"type":"string","enum":["all","flagged","marked"]}},{"name":"signal","in":"query","required":false,"description":"Only visitors flagged with this signal code in the last 7 days (`device_tampered`, `device_many_accounts`…).","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"A visitorId (`tqv_…`), a requestId (`dre_…`: its visitor), an IP address, a range in CIDR (/16 or narrower for IPv4, /48 or narrower for IPv6) or an account: the visitors of the newest 5 000 matching identifications within the retention, at most 500; past either `truncated: true`, narrow the search.","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"The `nextCursor` of the previous page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"1 to 100, default 25.","schema":{"type":"integer","minimum":1,"maximum":100}}],"description":"One page of an indexed query within the retention, newest first: a workspace with millions of visitors is searched and filtered, never listed whole. All the data goes out through the export, the `device_risk.*` webhooks and the organization's log stream.","responses":{"200":{"description":"A page of visitors.","content":{"application/json":{"schema":{"type":"object","properties":{"visitors":{"type":"array","items":{"$ref":"#/components/schemas/DeviceRiskVisitor"}},"nextCursor":{"type":["string","null"]},"truncated":{"type":"boolean","description":"The search matched more visitors than one answer looks at."},"signalTexts":{"type":"object"}}}}}},"400":{"description":"`cidr_too_broad`: a range wider than /16 for IPv4 or /48 for IPv6.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"`search_too_broad`: the search reads too many identifications; narrow it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/visitors/{visitorId}":{"get":{"tags":["Device Risk"],"operationId":"getDeviceRiskVisitor","summary":"One visitor, its accounts and its latest identifications","parameters":[{"name":"visitorId","in":"path","required":true,"description":"The visitor's id (`tqv_…`).","schema":{"type":"string","pattern":"^tqv_[A-Za-z0-9_-]{20}$"}}],"responses":{"200":{"description":"The visitor.","content":{"application/json":{"schema":{"type":"object","properties":{"visitor":{"$ref":"#/components/schemas/DeviceRiskVisitor"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no visitor with this id in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Device Risk"],"operationId":"forgetDeviceRiskVisitor","summary":"Forget a visitor (it is new next time)","parameters":[{"name":"visitorId","in":"path","required":true,"description":"The visitor's id (`tqv_…`).","schema":{"type":"string","pattern":"^tqv_[A-Za-z0-9_-]{20}$"}}],"responses":{"200":{"description":"Forgotten, here and in the risk engine's counters.","content":{"application/json":{"schema":{"type":"object","properties":{"forgotten":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no visitor with this id in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/visitors/{visitorId}/status":{"put":{"tags":["Device Risk"],"operationId":"setDeviceRiskVisitorStatus","summary":"Mark a visitor: trusted, step up always, or none","description":"The risk engine reads the mark with every identification of the visitor. `trusted` keeps only the signals about the page and the report (another site, a reused identification); it never skips any other check. `step_up` asks on every sign-in from the visitor, enforced at once.","parameters":[{"name":"visitorId","in":"path","required":true,"description":"The visitor's id (`tqv_…`).","schema":{"type":"string","pattern":"^tqv_[A-Za-z0-9_-]{20}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["none","trusted","step_up"]}}}}}},"responses":{"200":{"description":"The visitor.","content":{"application/json":{"schema":{"type":"object","properties":{"visitor":{"$ref":"#/components/schemas/DeviceRiskVisitor"}}}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no visitor with this id in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/accounts/{externalUsername}":{"get":{"tags":["Device Risk"],"operationId":"getDeviceRiskAccountVisitors","summary":"The visitorIds an account used","parameters":[{"name":"externalUsername","in":"path","required":true,"description":"The account.","schema":{"type":"string"}}],"responses":{"200":{"description":"Its visitors, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"externalUsername":{"type":"string"},"visitors":{"type":"array","items":{"$ref":"#/components/schemas/DeviceRiskVisitor"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Device Risk"],"operationId":"forgetDeviceRiskAccount","summary":"Delete one person's identifications","parameters":[{"name":"externalUsername","in":"path","required":true,"description":"The account.","schema":{"type":"string"}}],"responses":{"200":{"description":"What was deleted (visitors left with none go too; the account leaves the engine's counters).","content":{"application/json":{"schema":{"type":"object","properties":{"reports":{"type":"integer"},"visitors":{"type":"integer"}}}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/delete-all":{"post":{"tags":["Device Risk"],"operationId":"deleteAllDeviceRiskVisitors","summary":"Delete every visitor","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["confirm"],"properties":{"confirm":{"type":"string","const":"delete all"}}}}}},"responses":{"200":{"description":"How many were deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"integer"}}}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/device-risk/export":{"get":{"tags":["Device Risk"],"operationId":"exportDeviceRisk","summary":"Export the visitors and identifications of a date range (JSON)","parameters":[{"name":"from","in":"query","required":false,"description":"First day (YYYY-MM-DD, UTC): visitors seen since, with that range's identifications.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Last day (YYYY-MM-DD, UTC).","schema":{"type":"string","format":"date"}},{"name":"format","in":"query","required":false,"description":"`csv`: the identifications of the range, one row each (no address), streamed; at most 200 000 rows, `X-Touchque-Truncated: true` when cut. A file whose last line starts with `# export stopped` is incomplete: the export failed after the first rows. Default JSON.","schema":{"type":"string","enum":["json","csv"]}}],"description":"Streamed, one export of a workspace at a time (either format). JSON: one document written visitor by visitor; its `truncated` comes last. At most 50 000 visitors, 500 identifications per visitor, 200 000 identifications and 48 MB in all: past any of them `truncated: true`, export a shorter range. `X-Touchque-Truncated: true` is sent when the cut is known before the first byte (more than 50 000 visitors). A failure after the first byte still ends the document, with `truncated: true` and `stopped: true`: it is incomplete.","responses":{"200":{"description":"No keyed hashes and no addresses.","content":{"application/json":{"schema":{"type":"object","properties":{"exportedAt":{"type":"string","format":"date-time"},"from":{"type":["string","null"]},"to":{"type":["string","null"]},"visitors":{"type":"array","items":{"type":"object"}},"truncated":{"type":"boolean"},"stopped":{"type":"boolean","description":"Present (true) only when the export failed midway: the document is incomplete."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"`search_too_broad` (CSV): the range reads too many identifications; export a shorter one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"description":"`export_in_progress`: an export of this workspace is already running.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/approvals":{"post":{"tags":["Safe Guard"],"operationId":"createSafeGuardApproval","summary":"Ask an approver's phone to approve a Safe transaction","description":"Goes through the same pipeline as POST /login/request (freeze, push limit, risk engine, Actions, Vishing Shield, case code), always with biometrics. For a Safe registered in Safe Guard, its Touchque limits are checked first; its approvals keep working while Safe Guard is being turned off.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["externalUsername","type","safeTx"],"properties":{"externalUsername":{"type":"string"},"type":{"type":"string","description":"An action type you defined."},"safeTx":{"$ref":"#/components/schemas/SafeTx"},"referenceId":{"type":["string","null"]},"expiresIn":{"type":"integer","minimum":30,"maximum":300,"default":300},"clientIp":{"type":["string","null"]},"userAgent":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"The request: requestId, challengeCode and how the server classed the transaction (class, flags, hashes, rows).","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`over_limit` (with limit, max, amount), a feature refusal, or the pipeline's own refusals (with a case code).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`crypto_key_missing`: the phone has no crypto approval key yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/approvals/{id}":{"get":{"tags":["Safe Guard"],"operationId":"getSafeGuardApproval","summary":"A Safe approval request: its status and, once approved, the approval for the guard","parameters":[{"name":"id","in":"path","required":true,"description":"The requestId.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{ requestId, externalUsername, status, provisional, safeTx }.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["PENDING","CONFIRMED","REJECTED","EXPIRED"]},"safeTx":{"$ref":"#/components/schemas/SafeGuardApprovalResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/overview":{"get":{"tags":["Safe Guard"],"operationId":"getSafeGuardOverview","summary":"The workspace's Safes with their chain state, and 30 days of requests","responses":{"200":{"description":"{ safes, activity: { days, refused }, networks: [{ chainId, name, test, available, guard, reason }] }.","content":{"application/json":{"schema":{"type":"object","properties":{"safes":{"type":"array","items":{"$ref":"#/components/schemas/SafeGuardSafe"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/approvers":{"get":{"tags":["Safe Guard"],"operationId":"listSafeGuardApproverCandidates","summary":"The workspace's people and whether their phone has a crypto key","responses":{"200":{"description":"{ approvers: [{ externalUsername, platform, cryptoKey, registeredAt }] }.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/history":{"get":{"tags":["Safe Guard"],"operationId":"listSafeGuardApprovals","summary":"Approval requests, newest first, and those refused before any phone was asked","parameters":[{"name":"safeId","in":"query","required":false,"description":"Only this Safe.","schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","required":false,"description":"PENDING, CONFIRMED, REJECTED, EXPIRED or REFUSED.","schema":{"type":"string","enum":["PENDING","CONFIRMED","REJECTED","EXPIRED","REFUSED"]}},{"name":"cursor","in":"query","required":false,"description":"nextCursor of the previous page.","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"description":"At most 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"{ approvals, refused, nextCursor }; each approval with its case code.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes":{"post":{"tags":["Safe Guard"],"operationId":"addSafeGuardSafe","summary":"Add a Safe to Safe Guard","description":"Nothing changes on chain. The Safe must be readable on its network. The guard is always Touchque's own contract on that network (GET /safe-guard/overview `networks`); until the contract is audited, only test networks are available in production.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chainId","safe","name"],"properties":{"chainId":{"type":"integer"},"safe":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"guard":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","description":"Optional; if given, it must be Touchque's guard on this network (`guard_mismatch` otherwise)."},"name":{"type":"string","maxLength":80}}}}}},"responses":{"201":{"description":"{ safe }.","content":{"application/json":{"schema":{"type":"object","properties":{"safe":{"$ref":"#/components/schemas/SafeGuardSafe"}}}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`exists` or `too_many`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"description":"`not_on_chain`, or `guard_not_available` (no Touchque guard on this network yet, or not before the audit).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}":{"get":{"tags":["Safe Guard"],"operationId":"getSafeGuardSafe","summary":"One Safe: chain state, approvers matched to people, changes waiting on chain","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}},{"name":"fresh","in":"query","required":false,"description":"1: read the chain now, not the 30-second cache.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"{ safe, chain, changes, exit, pending, policy }: changes are the governance changes the phones were asked about, each with its stage (collecting, ready_to_schedule, waiting, ready_to_run), its approvals and the guard call to send next (scheduleTx, vetoTx); exit is the emergency exit the owners requested ({ at, pending, cancels, cancelTx }) or null; alerts are what the chain watcher saw on this Safe, newest first ({ id, kind, severity, details, phonesAsked, createdAt }: exit_requested, exit_cancelled, change_scheduled, change_vetoed, guard_removed, module_enabled, owners_changed, unknown_transaction).","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Safe Guard"],"operationId":"updateSafeGuardSafe","summary":"Rename a Safe or set its Touchque limits","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"policy":{"$ref":"#/components/schemas/SafeGuardPolicy"}}}}}},"responses":{"200":{"description":"{ safe }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Safe Guard"],"operationId":"removeSafeGuardSafe","summary":"Take a Safe out of Safe Guard","description":"Only once its guard is off the Safe: while it is on, its approvals need this Safe here.","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Removed."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`guard_installed`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/change":{"post":{"tags":["Safe Guard"],"operationId":"prepareSafeGuardChange","summary":"The Safe transaction of a setup step or a change, and how the guard classes it","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["setup","install","remove_guard","add_recipient","remove_recipient","lock","thresholds","delays","add_approver","remove_approver","test"]},"args":{"type":"object"},"locale":{"type":"string","enum":["tr","en"]}}}}}},"responses":{"200":{"description":"{ tx, class, flags, rows, safeTxHash, governanceHash, domainHash, messageHash, needsPhones }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"description":"`not_on_chain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/veto":{"post":{"tags":["Safe Guard"],"operationId":"vetoSafeGuardChange","summary":"Ask an approver's phone to stop a change waiting on chain","description":"The phone decodes the waiting change again and signs a veto (purpose 3). Once it has, GET /safe-guard/safes/{id} gives the `vetoTx` (guard.veto) that anyone may send with any wallet.","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["governanceHash","externalUsername"],"properties":{"governanceHash":{"type":"string","pattern":"^0x[0-9a-f]{64}$"},"externalUsername":{"type":"string"}}}}}},"responses":{"200":{"description":"As POST /safe-guard/approvals.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`not_waiting`, `crypto_key_missing` or `not_an_approver`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"description":"`not_on_chain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/cancel-exit":{"post":{"tags":["Safe Guard"],"operationId":"cancelSafeGuardExit","summary":"Ask an approver's phone to cancel the emergency exit the owners requested","description":"The phone reads the requested exit from the chain and signs its cancel (purpose 4, subject the exit time). Once it has, GET /safe-guard/safes/{id} gives the `exit.cancelTx` (guard.cancelExit) that anyone may send with any wallet.","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string"}}}}}},"responses":{"200":{"description":"As POST /safe-guard/approvals.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`no_pending_exit`, `crypto_key_missing` or `not_an_approver`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"description":"`not_on_chain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/create":{"post":{"tags":["Safe Guard"],"operationId":"prepareSafeGuardSafeCreation","summary":"The factory call that creates a new Safe, decoded","description":"The canonical Safe v1.4.1 factory call with these owners and threshold, the rows it decodes to, the keccak256 of its data and the new Safe's address before it exists. The owner's own wallet sends it; then add the Safe with POST /safe-guard/safes. Touchque sends nothing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["chainId","owners","threshold"],"properties":{"chainId":{"type":"integer"},"owners":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"minItems":1,"maxItems":20},"threshold":{"type":"integer","minimum":1},"locale":{"type":"string","enum":["tr","en"]}}}}}},"responses":{"200":{"description":"{ tx: { to, value, data }, predicted, rows, dataHash, saltNonce, contracts }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"`guard_not_available`, `safe_contracts_missing` or `not_on_chain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/proposals":{"post":{"tags":["Safe Guard"],"operationId":"proposeSafeGuardInstall","summary":"The guard's install as one Safe transaction the owners sign off chain","description":"The guard's setup (its batch target the canonical MultiSendCallOnly), the address book, the lock and setGuard in one delegatecall to MultiSendCallOnly, at the Safe's current nonce. The answer has the Safe's EIP-712 typed data for the owners' wallets (eth_signTypedData_v4) and the hashes a hardware wallet shows.","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","args"],"properties":{"kind":{"type":"string","enum":["install"]},"args":{"type":"object","description":"{ approvers: [crypto keys], threshold, govThreshold, govDelay, exitDelay, book?: [{ address, label }], lock? }"}}}}}},"responses":{"201":{"description":"{ proposal }: { id, kind, tx, safeTxHash, domainHash, messageHash, rows, args, calls, typedData, owners, need, signatures, done, stale, execTx, shown }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`guard_installed` or `has_modules`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"description":"`not_on_chain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"tags":["Safe Guard"],"operationId":"listSafeGuardProposals","summary":"The Safe's latest proposals","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{ proposals }.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/proposals/{pid}":{"get":{"tags":["Safe Guard"],"operationId":"getSafeGuardProposal","summary":"One proposal, with the owners who signed and, once enough have, the execTransaction call","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}},{"name":"pid","in":"path","required":true,"description":"The proposal's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{ proposal }.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/proposals/{pid}/signatures":{"post":{"tags":["Safe Guard"],"operationId":"signSafeGuardProposal","summary":"Add an owner's signature of the proposal","description":"The owner's eth_signTypedData_v4 signature of `typedData`. Kept only when it recovers to a current owner and the Safe's nonce has not moved; the same owner signing again replaces theirs.","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}},{"name":"pid","in":"path","required":true,"description":"The proposal's id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]{130}$"}}}}}},"responses":{"200":{"description":"{ proposal }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`bad_signature`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`not_an_owner` or `stale`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"description":"`not_on_chain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/proposals/{pid}/show":{"post":{"tags":["Safe Guard"],"operationId":"showSafeGuardProposal","summary":"Show the proposal on a person's phone, decoded there","description":"The phone decodes the same Safe transaction on its own and shows it with the hashes a hardware wallet shows, for an owner to compare before signing. It approves nothing: the phone signs purpose 16 (preview), which the guard never accepts. The proposal's `shown` says what each phone answered (same, different, waiting, expired).","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}},{"name":"pid","in":"path","required":true,"description":"The proposal's id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string"}}}}}},"responses":{"200":{"description":"As POST /safe-guard/approvals.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`crypto_key_missing`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/safe-guard/safes/{id}/send":{"post":{"tags":["Safe Guard"],"operationId":"sendSafeGuardChange","summary":"Ask an approver's phone to approve a setup step or a change","parameters":[{"name":"id","in":"path","required":true,"description":"The Safe's id in Safe Guard.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","externalUsername"],"properties":{"kind":{"type":"string","enum":["setup","install","remove_guard","add_recipient","remove_recipient","lock","thresholds","delays","add_approver","remove_approver","test"]},"args":{"type":"object"},"externalUsername":{"type":"string"},"locale":{"type":"string","enum":["tr","en"]}}}}}},"responses":{"200":{"description":"As POST /safe-guard/approvals.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` or `invalid_safe_tx`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`: no such Safe in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"409":{"description":"`guard_not_installed`, `crypto_key_missing` or `not_an_approver`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/tenant":{"get":{"tags":["Touchque Login"],"summary":"Get the tenant","operationId":"identity_get_tenant","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityTenant"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_enabled`: turn Touchque Login on first (`PUT /identity/tenant`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Touchque Login"],"summary":"Turn Touchque Login on, or change its settings","operationId":"identity_put_tenant","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityTenant"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityTenant"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/tenant/signing-keys/rotate":{"post":{"tags":["Touchque Login"],"summary":"Rotate the token signing key","operationId":"identity_post_tenant_signing_keys_rotate","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/clients":{"get":{"tags":["Touchque Login"],"summary":"List apps","operationId":"identity_get_clients","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add an app (the secret is in the answer, once)","operationId":"identity_post_clients","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityClient"}}}},"responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityClient"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/clients/{clientId}":{"get":{"tags":["Touchque Login"],"summary":"Get an app","operationId":"identity_get_clients_clientId","parameters":[{"name":"clientId","in":"path","required":true,"description":"The clientId.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityClient"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Touchque Login"],"summary":"Change an app","operationId":"identity_patch_clients_clientId","parameters":[{"name":"clientId","in":"path","required":true,"description":"The clientId.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityClient"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityClient"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Remove an app (its refresh tokens end)","operationId":"identity_delete_clients_clientId","parameters":[{"name":"clientId","in":"path","required":true,"description":"The clientId.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/clients/{clientId}/rotate-secret":{"post":{"tags":["Touchque Login"],"summary":"Rotate an app's secret","operationId":"identity_post_clients_clientId_rotate_secret","parameters":[{"name":"clientId","in":"path","required":true,"description":"The clientId.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityClient"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/apis":{"get":{"tags":["Touchque Login"],"summary":"List APIs","operationId":"identity_get_apis","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add an API","operationId":"identity_post_apis","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityApi"}}}},"responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityApi"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/apis/{id}":{"get":{"tags":["Touchque Login"],"summary":"Get an API","operationId":"identity_get_apis_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityApi"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Touchque Login"],"summary":"Change an API","operationId":"identity_patch_apis_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityApi"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityApi"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Remove an API","operationId":"identity_delete_apis_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/connections":{"get":{"tags":["Touchque Login"],"summary":"List connections","operationId":"identity_get_connections","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add a social or enterprise connection","operationId":"identity_post_connections","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityConnection"}}}},"responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityConnection"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/connections/{id}":{"get":{"tags":["Touchque Login"],"summary":"Get a connection","operationId":"identity_get_connections_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityConnection"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Touchque Login"],"summary":"Change a connection","operationId":"identity_patch_connections_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityConnection"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityConnection"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Remove a connection (its linked accounts go)","operationId":"identity_delete_connections_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users":{"get":{"tags":["Touchque Login"],"summary":"Search customers","operationId":"identity_get_users","parameters":[{"name":"q","in":"query","required":false,"description":"Filter or option: q.","schema":{"type":"string"}},{"name":"blocked","in":"query","required":false,"description":"Filter or option: blocked.","schema":{"type":"string"}},{"name":"emailVerified","in":"query","required":false,"description":"Filter or option: emailVerified.","schema":{"type":"string"}},{"name":"connection","in":"query","required":false,"description":"Filter or option: connection.","schema":{"type":"string"}},{"name":"organization","in":"query","required":false,"description":"Filter or option: organization.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Filter or option: limit.","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Filter or option: cursor.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add a customer","operationId":"identity_post_users","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityUserCreate"}}}},"responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityUser"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}":{"get":{"tags":["Touchque Login"],"summary":"Get a customer","operationId":"identity_get_users_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityUser"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Touchque Login"],"summary":"Change a customer (metadata is merged; null removes a key)","operationId":"identity_patch_users_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityUserCreate"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityUser"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Erase a customer (KVKK/GDPR)","operationId":"identity_delete_users_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/block":{"post":{"tags":["Touchque Login"],"summary":"Block (signs out everywhere)","operationId":"identity_post_users_id_block","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/unblock":{"post":{"tags":["Touchque Login"],"summary":"Unblock","operationId":"identity_post_users_id_unblock","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/export":{"get":{"tags":["Touchque Login"],"summary":"Everything about the customer (KVKK/GDPR access request)","operationId":"identity_get_users_id_export","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/sessions":{"get":{"tags":["Touchque Login"],"summary":"Signed-in sessions","operationId":"identity_get_users_id_sessions","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Sign out everywhere","operationId":"identity_delete_users_id_sessions","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/sessions/{sessionId}":{"delete":{"tags":["Touchque Login"],"summary":"End one session","operationId":"identity_delete_users_id_sessions_sessionId","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"sessionId","in":"path","required":true,"description":"The sessionId.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/mfa":{"delete":{"tags":["Touchque Login"],"summary":"Reset second factors (passkeys, the Touchque app, email codes)","operationId":"identity_delete_users_id_mfa","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/identities/{identityId}":{"delete":{"tags":["Touchque Login"],"summary":"Unlink a provider account","operationId":"identity_delete_users_id_identities_identityId","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"identityId","in":"path","required":true,"description":"The identityId.","schema":{"type":"string"}},{"name":"force","in":"query","required":false,"description":"Filter or option: force.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/connected-accounts":{"get":{"tags":["Touchque Login"],"summary":"Provider accounts whose tokens the token vault keeps for the customer","operationId":"identity_get_users_id_connected_accounts","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/connected-accounts/{accountId}":{"delete":{"tags":["Touchque Login"],"summary":"Stop apps using a provider account for the customer (deletes its stored tokens)","operationId":"identity_delete_users_id_connected_accounts_accountId","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"accountId","in":"path","required":true,"description":"The accountId.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/verification-email":{"post":{"tags":["Touchque Login"],"summary":"Send a verification email","operationId":"identity_post_users_id_verification_email","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"202":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/password-reset":{"post":{"tags":["Touchque Login"],"summary":"A password link, emailed or returned (`sendEmail: false`)","operationId":"identity_post_users_id_password_reset","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"202":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/roles":{"get":{"tags":["Touchque Login"],"summary":"The customer's roles","operationId":"identity_get_users_id_roles","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Give roles (tenant-wide or in an organization)","operationId":"identity_post_users_id_roles","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Take roles (body { roleIds, organizationId })","operationId":"identity_delete_users_id_roles","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/roles/{roleId}":{"delete":{"tags":["Touchque Login"],"summary":"Take one role","operationId":"identity_delete_users_id_roles_roleId","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"roleId","in":"path","required":true,"description":"The roleId.","schema":{"type":"string"}},{"name":"organizationId","in":"query","required":false,"description":"Filter or option: organizationId.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users/{id}/logs":{"get":{"tags":["Touchque Login"],"summary":"The customer's activity","operationId":"identity_get_users_id_logs","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Filter or option: limit.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/jobs":{"get":{"tags":["Touchque Login"],"summary":"Recent imports and exports","operationId":"identity_get_jobs","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users-imports":{"post":{"tags":["Touchque Login"],"summary":"Import customers (JSON or NDJSON, with old password hashes)","operationId":"identity_post_users_imports","responses":{"202":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityJob"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users-imports/{id}":{"get":{"tags":["Touchque Login"],"summary":"An import's progress","operationId":"identity_get_users_imports_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityJob"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users-exports":{"post":{"tags":["Touchque Login"],"summary":"Export customers (CSV or JSON lines; no hashes)","operationId":"identity_post_users_exports","responses":{"202":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityJob"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users-exports/{id}":{"get":{"tags":["Touchque Login"],"summary":"An export's progress","operationId":"identity_get_users_exports_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityJob"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/users-exports/{id}/file":{"get":{"tags":["Touchque Login"],"summary":"The export file (kept 24 hours)","operationId":"identity_get_users_exports_id_file","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/roles":{"get":{"tags":["Touchque Login"],"summary":"List roles","operationId":"identity_get_roles","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add a role","operationId":"identity_post_roles","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityRole"}}}},"responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityRole"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/roles/{id}":{"get":{"tags":["Touchque Login"],"summary":"Get a role","operationId":"identity_get_roles_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityRole"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Touchque Login"],"summary":"Change a role and its permissions","operationId":"identity_patch_roles_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityRole"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityRole"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Remove a role","operationId":"identity_delete_roles_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/roles/{id}/users":{"get":{"tags":["Touchque Login"],"summary":"Who has the role","operationId":"identity_get_roles_id_users","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations":{"get":{"tags":["Touchque Login"],"summary":"List organizations","operationId":"identity_get_organizations","parameters":[{"name":"q","in":"query","required":false,"description":"Filter or option: q.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add an organization","operationId":"identity_post_organizations","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOrganization"}}}},"responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOrganization"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations/{id}":{"get":{"tags":["Touchque Login"],"summary":"Get an organization","operationId":"identity_get_organizations_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOrganization"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Touchque Login"],"summary":"Change an organization","operationId":"identity_patch_organizations_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOrganization"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOrganization"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Remove an organization","operationId":"identity_delete_organizations_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations/{id}/members":{"get":{"tags":["Touchque Login"],"summary":"Members and their roles there","operationId":"identity_get_organizations_id_members","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add members","operationId":"identity_post_organizations_id_members","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations/{id}/members/{userId}":{"delete":{"tags":["Touchque Login"],"summary":"Remove a member","operationId":"identity_delete_organizations_id_members_userId","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"description":"The userId.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations/{id}/members/{userId}/roles":{"get":{"tags":["Touchque Login"],"summary":"A member's roles there","operationId":"identity_get_organizations_id_members_userId_roles","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"description":"The userId.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations/{id}/connections":{"put":{"tags":["Touchque Login"],"summary":"Set the organization's connections","operationId":"identity_put_organizations_id_connections","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations/{id}/invitations":{"get":{"tags":["Touchque Login"],"summary":"Invitations","operationId":"identity_get_organizations_id_invitations","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Invite by email (the link opens the app's sign-in route)","operationId":"identity_post_organizations_id_invitations","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/organizations/{id}/invitations/{invitationId}":{"delete":{"tags":["Touchque Login"],"summary":"Revoke an invitation","operationId":"identity_delete_organizations_id_invitations_invitationId","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}},{"name":"invitationId","in":"path","required":true,"description":"The invitationId.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/custom-domains":{"get":{"tags":["Touchque Login"],"summary":"List custom domains, and `verifiedDomains`: your organization's verified domains (null when they cannot be read)","operationId":"identity_get_custom_domains","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Touchque Login"],"summary":"Add a custom domain (DNS records in the answer). On a verified domain of your organization it is verified at once (`proof: organization_domain`) and needs only the CNAME","operationId":"identity_post_custom_domains","responses":{"201":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/custom-domains/{id}/verify":{"post":{"tags":["Touchque Login"],"summary":"Check the DNS TXT record (or, on a verified domain of your organization, verify it without one)","operationId":"identity_post_custom_domains_id_verify","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/custom-domains/{id}/check-certificate":{"post":{"tags":["Touchque Login"],"summary":"Check the domain's certificate at the sign-in edge now (`certificate`: status pending | active | error, notAfter, issuer, error)","operationId":"identity_post_custom_domains_id_check_certificate","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/custom-domains/{id}":{"patch":{"tags":["Touchque Login"],"summary":"Make a verified domain the issuer","operationId":"identity_patch_custom_domains_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Remove a custom domain","operationId":"identity_delete_custom_domains_id","parameters":[{"name":"id","in":"path","required":true,"description":"The object's id.","schema":{"type":"string"}}],"responses":{"204":{"description":"Done."},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/attack-protection":{"get":{"tags":["Touchque Login"],"summary":"Attack protection: brute force, suspicious addresses, bot challenge, breached passwords, new-device notices, the risk engine, rate limits","operationId":"identity_get_attack_protection","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Touchque Login"],"summary":"Change attack protection (partial; the bot secret is write-only)","operationId":"identity_put_attack_protection","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityAttackProtection"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/attack-protection/events":{"get":{"tags":["Touchque Login"],"summary":"What attack protection caught lately","operationId":"identity_get_attack_protection_events","parameters":[{"name":"limit","in":"query","required":false,"description":"Filter or option: limit.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/delivery":{"get":{"tags":["Touchque Login"],"summary":"Email and SMS providers (secrets never returned; secretsSet lists which are stored), and `senderDomains` { organization, login }: the domains an email may be sent from","operationId":"identity_get_delivery","responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/delivery/{channel}":{"put":{"tags":["Touchque Login"],"summary":"Set the email (smtp, ses, postmark, sendgrid) or SMS (twilio, netgsm) provider","operationId":"identity_put_delivery_channel","parameters":[{"name":"channel","in":"path","required":true,"description":"The channel.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityDelivery"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Touchque Login"],"summary":"Remove the provider (Touchque's own transport is used again)","operationId":"identity_delete_delivery_channel","parameters":[{"name":"channel","in":"path","required":true,"description":"The channel.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/delivery/{channel}/test":{"post":{"tags":["Touchque Login"],"summary":"Send one test message through the provider in use","operationId":"identity_post_delivery_channel_test","parameters":[{"name":"channel","in":"path","required":true,"description":"The channel.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/identity/outbox":{"get":{"tags":["Touchque Login"],"summary":"The delivery log: what was sent, to whom, status and attempts. Message text only in the local dev inbox","operationId":"identity_get_outbox","parameters":[{"name":"to","in":"query","required":false,"description":"Filter or option: to.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Filter or option: limit.","schema":{"type":"string"}}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` with `field` and `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}}},"webhooks":{"loginResult":{"post":{"tags":["Webhooks"],"operationId":"onLoginResult","summary":"Login request approved (SUCCESS) or rejected (REJECTED)","description":"Sent when a login request is approved — on the phone, with a passkey (`/webauthn/login/verify`, `/webauthn/authenticate/primary/verify`) or with a recovery code — or rejected (on the phone, or by Touchque at the tap). Not sent for requests blocked at creation, for expiry, for offline-sign approvals, or while a request the Vishing Shield moved to a passkey is still waiting for it. A SUCCESS with `provisional: true` can be followed by an `approvalEvent`.\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nIf every in-process attempt fails, the callback is queued and redelivered by a background job (checked every 5 minutes) with a fresh `jti`, `timestamp` and signature but the same `Idempotency-Key`. A failed redelivery is queued again from the first interval, so an endpoint that keeps failing keeps receiving redeliveries.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload (see above).","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed; same value as the signed `timestamp` field).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti` (not signed).","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"The `requestId` — stable across retries and redeliveries.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"`SUCCESS` or `REJECTED`.","schema":{"type":"string","enum":["SUCCESS","REJECTED"]}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key (or the literal `[integration-api-key]` when none is stored). Do not treat it as proof of origin — verify the signature.","schema":{"type":"string"}},{"name":"x-sdk-name","in":"header","required":true,"description":"Sender name (the backend package name).","schema":{"type":"string"}},{"name":"x-sdk-version","in":"header","required":true,"description":"Sender version.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCallbackPayload"}}}},"responses":{"2XX":{"description":"Acknowledged. The body is ignored."},"default":{"description":"Any other status (or a timeout) is retried."}}}},"deviceEvent":{"post":{"tags":["Webhooks"],"operationId":"onDeviceEvent","summary":"Device linked or unlinked","description":"`DEVICE_LINKED` when the user links the Touchque app by scanning the enrollment QR, or moves the account to a new phone with a transfer code (then with `via: transfer`); `DEVICE_UNLINKED` when the user disables 2FA from the phone.\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nEvent webhooks are not queued: after the in-process attempts they are dropped. `Idempotency-Key` is the event's `jti`.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload.","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"The event type.","schema":{"type":"string","enum":["DEVICE_LINKED","DEVICE_UNLINKED"]}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key. Not proof of origin — verify the signature.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventWebhookPayload"}}}},"responses":{"2XX":{"description":"Acknowledged."},"default":{"description":"Any other status (or a timeout) is retried in-process, then dropped."}}}},"approvalEvent":{"post":{"tags":["Webhooks"],"operationId":"onApprovalEvent","summary":"Approval re-confirmed or taken back (Vishing Shield)","description":"Follow-ups to a SUCCESS login result. `APPROVAL_RECONFIRMED` when, after the call, the user confirms on the phone that an approval given during it (`provisional: true`) was theirs; `APPROVAL_REVOKED` when the user takes an approval back (\"someone on the phone told me to approve\", `reason: COACHED`, or an undo, `reason: UNDO`): end the session or stop the action built on it, and expect the user's 2FA to be frozen. Sent to the same `callbackUrl` as the other events; a handler that switches on `event` should ignore types it does not know.\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nEvent webhooks are not queued: after the in-process attempts they are dropped, so also read `provisional`, `reconfirmedAt` and `revoked` from `GET /login/status/{id}` for a session you still treat as provisional. `Idempotency-Key` is the event's `jti`.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload.","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"The event type.","schema":{"type":"string","enum":["APPROVAL_RECONFIRMED","APPROVAL_REVOKED"]}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key. Not proof of origin — verify the signature.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalEventWebhookPayload"}}}},"responses":{"2XX":{"description":"Acknowledged."},"default":{"description":"Any other status (or a timeout) is retried in-process, then dropped."}}}},"recoveryEvent":{"post":{"tags":["Webhooks"],"operationId":"onRecoveryEvent","summary":"A recovery moved on (Never Locked Out)","description":"Every step of a recovery started with `POST /recovery/start`: the new phone bound, the vouchers' quorum reached (cooling-off), the account moved (`RECOVERY_COMPLETED`, followed by `DEVICE_LINKED` with `via: recovery`), or it ended without a move (vetoed, expired, failed). Approvals on a voucher's or the old phone's recovery request never send a `loginResult`.\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nEvent webhooks are not queued: read `GET /recovery/{id}` for the truth. `Idempotency-Key` is the event's `jti`.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload.","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"The event type.","schema":{"type":"string","enum":["RECOVERY_STARTED","RECOVERY_DEVICE_BOUND","RECOVERY_COOLING","RECOVERY_COMPLETED","RECOVERY_VETOED","RECOVERY_EXPIRED","RECOVERY_FAILED"]}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key. Not proof of origin — verify the signature.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryEventWebhookPayload"}}}},"responses":{"2XX":{"description":"Acknowledged."},"default":{"description":"Any other status (or a timeout) is retried in-process, then dropped."}}}},"passkeyEvent":{"post":{"tags":["Webhooks"],"operationId":"onPasskeyEvent","summary":"Passkey registered or deleted","description":"`PASSKEY_REGISTERED` when `POST /webauthn/register/verify` stores a new passkey for the user; `PASSKEY_DELETED` when `DELETE /webauthn/credentials/{id}` removes one. Sent to the same `callbackUrl` as the device events, so a handler that switches on `event` should ignore types it does not know. Use it to tell the user, for example by e-mail: a passkey added through a stolen session is a lasting second factor.\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nEvent webhooks are not queued: after the in-process attempts they are dropped. `Idempotency-Key` is the event's `jti`. Delivery never holds or fails the registration or the delete.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload.","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"The event type.","schema":{"type":"string","enum":["PASSKEY_REGISTERED","PASSKEY_DELETED"]}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key. Not proof of origin — verify the signature.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasskeyEventWebhookPayload"}}}},"responses":{"2XX":{"description":"Acknowledged."},"default":{"description":"Any other status (or a timeout) is retried in-process, then dropped."}}}},"mobilePush":{"post":{"tags":["Webhooks"],"operationId":"onMobilePush","summary":"Push a request to a device inside your app","description":"Sent for every request (sign-in, protected action, AI agent approval, recovery notice) to a device enrolled with the Mobile SDK, instead of a Touchque push. Forward `data` with your own APNs/FCM; the request itself travels end to end encrypted to the SDK. Without a `callbackUrl` no push is possible and the SDK only receives requests while your app is open.\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nEvent webhooks are not queued: after the in-process attempts they are dropped. `Idempotency-Key` is the event's `jti`.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload.","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"The event type.","schema":{"type":"string","const":"MOBILE_PUSH"}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key. Not proof of origin — verify the signature.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MobilePushWebhookPayload"}}}},"responses":{"2XX":{"description":"Acknowledged."},"default":{"description":"Any other status (or a timeout) is retried in-process, then dropped."}}}},"assistEvent":{"post":{"tags":["Webhooks"],"operationId":"onAssistEvent","summary":"Help desk: a session opened or ended, a check answered, or an \"IT\" call nobody from IT made","description":"`ASSIST_OPENED`, `ASSIST_ENDED`, `ASSIST_CHECK_PASSED`, `ASSIST_CHECK_FAILED` (`outcome`: `wrong_number`, `not_me`, `coached`, `declined`), `ASSIST_UNFROZEN`, `ASSIST_CLAIM_CONFIRMED` (the person checked that the agent is real) and `ASSIST_CLAIM_UNVERIFIED` (nobody from IT was working on the account: someone is pretending to be IT). Checks and unverified claims carry a case code. A check's approval never sends a `loginResult`.\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nEvent webhooks are not queued: read `GET /assist/{id}` for the truth. `Idempotency-Key` is the event's `jti`.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload.","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"The event type.","schema":{"type":"string","enum":["ASSIST_OPENED","ASSIST_ENDED","ASSIST_CHECK_PASSED","ASSIST_CHECK_FAILED","ASSIST_UNFROZEN","ASSIST_CLAIM_CONFIRMED","ASSIST_CLAIM_UNVERIFIED"]}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key. Not proof of origin — verify the signature.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistEventWebhookPayload"}}}},"responses":{"2XX":{"description":"Acknowledged."},"default":{"description":"Any other status (or a timeout) is retried in-process, then dropped."}}}},"deviceRiskEvent":{"post":{"tags":["Webhooks"],"operationId":"onDeviceRiskEvent","summary":"Device risk: a signal that asks for more, or an alert","description":"`DEVICE_RISK_SIGNAL` when an identification or a `/device-risk/check` raised a signal that asks for more, enforced or in shadow (page loads of the same visitor with the same signals: once in 10 minutes; every check). `DEVICE_RISK_ALERT` when an alert rule of the workspace went off (Device risk → Settings → Alerts; at most once an hour per rule). Never a trait: identifiers, signals and the risk engine's verdict.\n\nFor a workspace of an SSO organization with a log stream (Splunk, Datadog, Sentinel, webhook, S3), the same events also go to that stream (category `device_risk`).\n\n**Verifying.** `x-signature` (also repeated as `signature` in the body) is the lowercase hex HMAC-SHA256 of the canonical payload, keyed with the workspace's **webhook signing secret** (the integration API secret shown in the Dashboard — not a `tq_` credential secret):\n\n```\npayload   = the JSON body without its \"signature\" field\ncanonical = JSON.stringify(payload with ONLY its top-level keys sorted)\nsignature = hex(HMAC-SHA256(signingSecret, canonical))\n```\n\nNested objects keep their key order and numbers their exact text (see `sdks/fixtures/webhook-vectors.json`). `x-timestamp` and `x-nonce` are NOT part of the signed string; check freshness with the signed `timestamp` field and deduplicate on `Idempotency-Key`. Use a constant-time comparison.\n\n**Delivery.** POST with `Content-Type: application/json` to the workspace `callbackUrl`, only when its host matches `allowedCallbackDomain` (subdomains allowed) and, in production, it is HTTPS and not a loopback/metadata address — otherwise nothing is sent. Any 2xx acknowledges. A 5 s timeout or non-2xx is retried in-process up to 3 more times (exponential backoff from 0.4 s, ±25% jitter).\n\nEvent webhooks are not queued: after the in-process attempts they are dropped. `Idempotency-Key` is the event's `jti`.","security":[],"parameters":[{"name":"x-signature","in":"header","required":true,"description":"Hex HMAC-SHA256 of the canonical payload.","schema":{"type":"string","pattern":"^[0-9a-f]{64}$"}},{"name":"x-timestamp","in":"header","required":true,"description":"Send time, ISO 8601 (not signed).","schema":{"type":"string","format":"date-time"}},{"name":"x-nonce","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"description":"Same value as `jti`.","schema":{"type":"string","format":"uuid"}},{"name":"x-callback-event","in":"header","required":true,"description":"The event type.","schema":{"type":"string","enum":["DEVICE_RISK_SIGNAL","DEVICE_RISK_ALERT"]}},{"name":"x-api-key","in":"header","required":true,"description":"The workspace's integration API key. Not proof of origin — verify the signature.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRiskEventWebhookPayload"}}}},"responses":{"2XX":{"description":"Acknowledged."},"default":{"description":"Any other status (or a timeout) is retried in-process, then dropped."}}}}},"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Partner API key. Current keys are per-user credentials prefixed `tq_` (created in the Dashboard); they resolve to the workspace of the organization the key was created in and may carry an IP allowlist (403 `IP not allowed`). A key stops working once its creator is no longer a member of that organization (403 `credential_org_access_revoked`), and at its expiry when it was created with one (403 `credential_expired`); keys created before keys had an organization resolve to the creator's oldest organization.\n\nKeys without the `tq_` prefix are legacy integration keys: still accepted, but every response carries `Deprecation: true` and `Sunset: 2026-09-01` (a date that has already passed — the key keeps working). Legacy keys are also checked against the server-wide `API_IP_ALLOWLIST` (403 `IP_BLOCKED`).\n\nMust be sent together with `x-timestamp`, `x-nonce` and `x-signature` (see `Signature`)."},"Timestamp":{"type":"apiKey","in":"header","name":"x-timestamp","description":"Request time as **Unix epoch milliseconds** (decimal string, e.g. `1788000000000`); an ISO 8601 date-time is also accepted. Must be at most 5 minutes old and at most 1 minute in the future (`API_TIMESTAMP_TOLERANCE_MS` / `API_TIMESTAMP_FUTURE_SLACK_MS`), otherwise 401 `TIMESTAMP_EXPIRED`. Unix *seconds* are read as milliseconds (1970) and rejected. Legacy (non-`tq_`) keys may send it as `x-request-timestamp` instead."},"Nonce":{"type":"apiKey","in":"header","name":"x-nonce","description":"A fresh random value per request (the SDKs send 32 hex characters; use at least 16). Each nonce is accepted once within 10 minutes (Redis, in-memory fallback); a repeat answers 401 `NONCE_INVALID`. The error message mentions a 16-character minimum, but the server only enforces uniqueness."},"Signature":{"type":"apiKey","in":"header","name":"x-signature","description":"Lowercase hex **HMAC-SHA256** of the canonical request string, keyed with the API secret (as a UTF-8 string):\n\n```\ncanonical   = METHOD + \":\" + PATH + \":\" + TIMESTAMP + \":\" + NONCE + \":\" + BODY_SHA256\nx-signature = hex(HMAC-SHA256(apiSecret, canonical))\n```\n\n- `METHOD` — the HTTP method, upper case (`GET`, `POST`, `DELETE`).\n- `PATH` — the request target exactly as sent: path **plus `?` and the query string** when there is one (Express `req.originalUrl`), no scheme or host, percent-encoding kept as on the wire (e.g. `/webauthn/credentials?externalUsername=a%2Bb%40company.com`). The SDKs sort query parameters by key before sending; the server only requires the signed string to match the URL actually requested byte for byte.\n- `TIMESTAMP` — the exact `x-timestamp` header value.\n- `NONCE` — the exact `x-nonce` header value.\n- `BODY_SHA256` — lowercase hex SHA-256 of the raw request body; for no body, the SHA-256 of the empty string (`e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`). Only `application/json` and `application/x-www-form-urlencoded` bodies are captured for hashing, so always send JSON (any other body type is hashed as empty, i.e. left unsigned).\n\nExample (from `sdks/fixtures/signature-vectors.json`): secret `whsec_fixture_secret_0123456789abcdef`, `POST /login/request`, body `{\"externalUsername\":\"user@company.com\",\"type\":\"LOGIN\"}`, timestamp `1788000000000`, nonce `0123456789abcdef0123456789abcdef` → `189f70d50b408e47612c1fe0873b5be97e69f9c2e37894abb1a48d36a2d40bbb`.\n\n**Legacy path-only signatures.** Before 2026-08 the SDKs signed the path without its query string. For a request that has a query string, a signature over the path alone is still accepted until `LEGACY_PATH_SIGNATURE_SUNSET` (default `2026-12-31T23:59:59Z`; `LEGACY_PATH_SIGNATURE=off` disables it immediately) and the response carries `Deprecation: true` and `Sunset` headers; after that it fails with 401 `SIGNATURE_LEGACY_REJECTED`.\n\n**Enforcement.** Signatures are always required in production (`NODE_ENV=production`). Elsewhere a request without `x-signature` is accepted unless `API_REQUIRE_SIGNATURE=true`; whenever `x-signature` is present, `x-timestamp` and `x-nonce` are mandatory. A wrong signature answers 403 `SIGNATURE_INVALID`."},"TelemetryToken":{"type":"http","scheme":"bearer","description":"Short-lived, single-request bearer token returned as `telemetryToken` by `POST /login/request` when the workspace enabled behavioral biometrics (`TenantPolicy.behavioralBiometricsEnabled`). Scoped to that one `requestId`, valid for 45 seconds, never persisted. Used only by the partner's browser (the `@touchque/web` behavioral widget), which can never hold the HMAC secret."},"AgentDPoP":{"type":"http","scheme":"DPoP","description":"An agent's own token from `POST /agents/token`, as `Authorization: DPoP <token>`, with a `DPoP` header: a proof JWT (RFC 9449, `typ: dpop+jwt`, ES256 or EdDSA, the public key in `jwk`) naming `htm`, `htu` (scheme, host and path of this call), a fresh `iat` (±60 s), a unique `jti` and `ath` (base64url SHA-256 of the token), signed by the key the token was bound to. Each proof is accepted once."}},"schemas":{"Error":{"type":"object","description":"Error body. Three shapes are in use, all carrying `error`:\n\n- **Coded** (`{ code, error, ... }`, newer endpoints such as `/login/request`, `/login/{id}/consume`, `/login/{id}/report-fraud`, `POST /action-types`): `code` is the stable machine-readable value, `error` a human-readable message. See `CodedError`.\n- **Middleware** (`{ error, message }`): `error` is an upper-case code (`SIGNATURE_INVALID`, `RATE_LIMITED`, ...) or a snake_case code (`phishing_resistant_required`, `offline_sign_disabled`), `message` explains it.\n- **Legacy** (`{ error }`): `error` is a human-readable sentence. The global error handler also answers this way (plus `details` for 400/409 database validation errors).\n\nClients should branch on `code` when present, otherwise on `error`.","required":["error"],"properties":{"error":{"type":"string","description":"Error code or human-readable message (see above)."},"code":{"type":"string","description":"Machine-readable error code (coded errors only)."},"message":{"type":"string","description":"Human-readable explanation."},"details":{"type":"string","description":"Extra detail (global error handler only)."},"caseCode":{"type":"string","pattern":"^[A-HJ-NP-Z2-9]{3}-[A-HJ-NP-Z2-9]{3}$","description":"\"Why?\" case code of the refusal (`/login/request` and `/risk/check` refusals) (6 characters without I, O, 0 or 1, e.g. `K7P-2QX`). Show it to the user (\"Tell your support team: K7P-2QX\"); your team opens the full decision (each check, the rule that matched, what would have let it through) by this code in the dashboard (Logs → Look up a case)."}},"additionalProperties":true},"CodedError":{"description":"Error with a stable machine-readable `code`.","allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","error"]},"RateLimitError":{"description":"Too many requests. `retryAfter` is in seconds.","allOf":[{"$ref":"#/components/schemas/Error"}],"properties":{"retryAfter":{"type":"integer","minimum":1}}},"OfflineResult":{"type":"object","description":"Result body of the offline verification endpoints. Failures carry `approved: false` and a `reason`.","required":["approved"],"properties":{"approved":{"type":"boolean"},"reason":{"type":"string","enum":["invalid_code","locked","used","expired","unknown_challenge","request_rejected","too_many_failures","device_not_enrolled","no_linked_device","frozen","step_up_required","blocked","unavailable","passkey_required"],"description":"`passkey_required`: the linked push now waits for a passkey (Vishing Shield: the user was on a call when they approved it) or its sign-in page is a phishing proxy; no code may finish it."},"blockedBy":{"type":"string","enum":["geo_policy","access_policy"],"description":"With `reason: blocked` (`POST /offline/totp/verify`): the workspace rule that refused the code. Absent when the risk engine refused it."},"attemptsLeft":{"type":"integer","minimum":0,"description":"Remaining code attempts for this challenge (`POST /offline/verify` only)."},"frozenUntil":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"}}},"LoginStatus":{"type":"string","enum":["PENDING","CONFIRMED","REJECTED","EXPIRED"]},"ConfirmedVia":{"type":["string","null"],"enum":["DEVICE","WEBAUTHN","WEBAUTHN_PRIMARY","RECOVERY_CODE",null],"description":"How a CONFIRMED request was approved: `DEVICE` (push on the phone), `WEBAUTHN` (passkey approval of a pending request), `WEBAUTHN_PRIMARY` (passwordless passkey sign-in), `RECOVERY_CODE`. `null` while not confirmed."},"Assurance":{"type":"object","description":"What a partner can rely on for an approved request. Only a passkey is phishing-resistant (origin-bound).","required":["phishingResistant","method"],"properties":{"phishingResistant":{"type":"boolean"},"method":{"type":["string","null"],"description":"`passkey`, `push`, `recovery_code`, `offline_code` or `offline_totp` (other `confirmedVia` values are lower-cased)."}}},"ApprovalProof":{"type":"object","description":"Device-signed proof of exactly what was approved. Present only when the approving phone has a registered hardware (Secure Enclave) key; verify it with the SDK's `verifyApprovalProof` against the public key you expect.\n\n`message` is the UTF-8 text that was signed — five lines joined by `\\n`:\n\n```\ntouchque-approve:v2\n<requestId>\n<type>\n<challengeCode or empty>\n<detailsDigest or empty>\n```\n\n`detailsDigest` is the lowercase hex SHA-256 of the details as `label + \"\\u001F\" + value`, items joined by `\"\\u001E\"` (empty when there are no details).","required":["version","algorithm","message","signature","publicKey"],"properties":{"version":{"type":"string","const":"v2"},"algorithm":{"type":"string","const":"ECDSA-P256-SHA256"},"message":{"type":"string","description":"The signed approval message (see above)."},"signature":{"type":"string","contentEncoding":"base64","description":"DER-encoded ECDSA P-256 / SHA-256 signature, base64."},"publicKey":{"type":"string","contentEncoding":"base64","description":"Uncompressed P-256 public key (65 bytes, X9.63), base64."}}},"LoginDetailsInput":{"description":"Transaction context shown on the approval screen (e.g. amount, recipient). Either a map of label → value (order kept) or an array of `{ label, value }`. At most 8 entries; labels 1–40 and values 1–120 characters after trimming; numbers and booleans are converted to strings; control characters and Unicode bidi overrides are rejected (400 `invalid_details`). Invalid input is rejected, never truncated. An empty map/array counts as no details.","oneOf":[{"type":"null"},{"type":"object","maxProperties":8,"additionalProperties":{"type":["string","number","boolean"]}},{"type":"array","maxItems":8,"items":{"type":"object","required":["label","value"],"properties":{"label":{"type":["string","number","boolean"]},"value":{"type":["string","number","boolean"]}}}}]},"LoginDetail":{"type":"object","required":["label","value"],"properties":{"label":{"type":"string","minLength":1,"maxLength":40},"value":{"type":"string","minLength":1,"maxLength":120}}},"LoginDetails":{"type":["array","null"],"description":"Normalised transaction details (always the array form), or `null` when none were sent.","maxItems":8,"items":{"$ref":"#/components/schemas/LoginDetail"}},"CreateLoginRequest":{"type":"object","required":["externalUsername","type"],"properties":{"externalUsername":{"type":"string","description":"Your identifier for the user (the one used at enrollment)."},"type":{"type":"string","description":"Action type: the `type` slug of an action type defined via `POST /action-types` or the Dashboard, or its `id` (UUID). Undefined types are refused (400 `unknown_action`)."},"referenceId":{"type":["string","null"],"description":"Your own reference (order id, transaction id). Echoed in status, consume and webhooks."},"clientIp":{"type":["string","null"],"description":"The END USER's IP address (not your server's). Used for location display, geo policy and risk. Non-IP and loopback values are ignored. When absent, risk scoring falls back to the caller's IP and the location stays unresolved."},"userAgent":{"type":["string","null"],"description":"The end user's browser user agent."},"details":{"$ref":"#/components/schemas/LoginDetailsInput"},"requireBiometric":{"type":"boolean","default":false,"description":"Require Face ID / Touch ID on the phone (verified server-side)."},"requireNumberMatch":{"type":"boolean","default":false,"description":"Force number matching (step-up) for this request."},"challengeDigits":{"type":"integer","enum":[2,3],"description":"Digits of the matching number when one is shown: 3 for 100–999. The longer of this and the workspace policy (`TenantPolicy.challengeDigits`) wins: a request can lengthen the number, never shorten it."},"context":{"$ref":"#/components/schemas/ActionContext"},"securityChanges":{"type":"object","description":"When the user's sign-in security last changed in your own system, each an ISO 8601 time or epoch milliseconds. Touchque adds what it saw itself (the phone linked, moved or recovered, passkeys added or removed; for Touchque Login customers also password, email and second-factor changes; the newest wins) and gives access rules and Actions the minutes since each: `minutesSince.passwordChange`, `.mfaChange`, `.deviceChange`, `.emailChange`, `.phoneChange`, `.anyChange` (a kind with no known change is left out, so a rule on it does not apply). Example rule: refuse your `PAYOUT_CHANGE` within 24 hours of a 2FA change.","additionalProperties":false,"properties":{"password":{"oneOf":[{"type":"string","format":"date-time"},{"type":"integer"}]},"mfa":{"oneOf":[{"type":"string","format":"date-time"},{"type":"integer"}]},"device":{"oneOf":[{"type":"string","format":"date-time"},{"type":"integer"}]},"email":{"oneOf":[{"type":"string","format":"date-time"},{"type":"integer"}]},"phone":{"oneOf":[{"type":"string","format":"date-time"},{"type":"integer"}]}}},"destination":{"type":"object","required":["kind","value"],"description":"Where the action sends money or value. Touchque shows it in full on the phone, first and in groups of four (`TR33 0006 1005 …`), and checks it against the destinations of this kind the user approved before (and `known`): one that looks like one of them but is not the same (same start and end with a different middle, confusable or mixed-alphabet letters, one or two characters off) gets a warning row above it. Access rules read `destination.lookalike`, `destination.known`, `destination.kind`; Actions read `event.request.destination`. Do not repeat it in `details`. 400 `invalid_request` for an IBAN whose check digits fail.","properties":{"kind":{"type":"string","enum":["iban","account","wallet","address","payee"]},"value":{"type":"string","maxLength":120},"known":{"type":"array","maxItems":50,"items":{"type":"string"},"description":"Destinations of this kind the user already uses in your system (their saved payees, addresses)."}}},"locale":{"type":"string","enum":["tr","en"],"description":"Language of the rows Touchque adds (the destination and its warning). Default tr."},"expiresIn":{"type":"integer","minimum":30,"maximum":300,"description":"How long the push waits for the person, in seconds (default 30). Longer suits a request the person has to read first, such as an AI agent asking on their behalf (CIBA). Ignored for `reply` (5 minutes) and `qrPairing` (90 seconds)."},"qrPairing":{"type":"string","pattern":"^[A-Za-z0-9_-]{22}$","description":"A browser pairing by QR code (`POST /qr-pairings`) that the user scanned and went on with in the Touchque app (`state: confirmed`). Only the phone that scanned it is asked, for the account it scanned with, once: always with number matching, for 90 seconds, the browser key's fingerprint added to `details` (so the phone's hardware-key signature covers it) and the pairing carried to the phone, which checks it is the code it scanned. 409 `pairing_not_confirmed` / `pairing_other_user` / `pairing_other_device`, 410 `pairing_expired`, 404 `pairing_not_found`. Cannot go with `reply`."},"reply":{"type":"object","required":["kind","key"],"description":"Ask the approver to type something back on the phone: today `kind: code`, a sign-in code another service sent them (a text message, WhatsApp, e-mail). Give a one-time P-256 public key: the phone encrypts what was typed to it (ECDH P-256, HKDF-SHA256 with the request id as salt and `touchque-code-reply-v1` as info, AES-256-GCM with `touchque-code-reply:<requestId>` as additional data; `v1.<its one-time key>.<nonce|ciphertext|tag>`, base64url) and Touchque hands the ciphertext back in `GET /login/status` `reply`: it never sees the code. Such a request lives 5 minutes and is never folded into another pending one.","properties":{"kind":{"type":"string","enum":["code"]},"key":{"type":"string","description":"Your one-time P-256 public key: its uncompressed point (65 bytes), base64url."},"channel":{"type":["string","null"],"enum":["sms","whatsapp","email","app","backup","other",null],"description":"Where the code went, shown on the phone."},"target":{"type":["string","null"],"maxLength":40,"description":"The masked number or address it went to (`+**********26`), shown on the phone."}}},"clientSignals":{"type":"string","maxLength":4096,"description":"Is a person at the page or software driving the browser (Playwright, a headless Chrome, an AI browser agent)? The end user's page check from `@touchque/web` `startClientSignals().encode()`, passed through untouched (booleans, counts and timings only). With signs of automation the request is never silent and asks with number matching; the workspace can block it instead (`TenantPolicy.automatedClients`: 403 `blocked` with reason `automated_client`)."},"deviceRiskRequestId":{"type":"string","pattern":"^dre_[A-Za-z0-9_-]{22}(\\.[A-Za-z0-9_-]{18})?$","description":"Device risk (a paid add-on, off by default): the page's identification from `@touchque/web` `deviceRisk.identify()`, or the script tag's `touchque_request_id` field as it came (after a dot, how the pointer moved: counts weighed as `device_pointer_synthetic`, never kept). Used only when the workspace turned Device risk on, for this account only (another account gets `device_report_replayed`). The risk engine counts the visitor and decides: its signals only ever ask for more (reported only until Touchque enforces them), a visitor your admin marked \"step up always\" asks at once, and a sign-in with a device signal is never silent."},"deviceRisk":{"allOf":[{"$ref":"#/components/schemas/DeviceRiskReport"}],"description":"Instead of deviceRiskRequestId: the page's report from `@touchque/web` `deviceRisk.collect()`, relayed untouched."},"browserProof":{"$ref":"#/components/schemas/BrowserProof","description":"Silent sign-ins: the end user's browser key proof, passed through untouched. A valid one is kept for 10 minutes; when this request is approved by a real second factor (a push that is not provisional, a passkey, an offline QR) that browser becomes known, so a later `POST /risk/check` can answer `prompt: false`. Recovery codes and typed time-based codes never make a browser known. An invalid proof is ignored (the request goes on as usual)."}}},"LoginRequestCreated":{"type":"object","required":["requestId","expiresAt","message"],"properties":{"message":{"type":"string","description":"`Request sent`, or `A previous request is already pending on the device.` when an identical pending request was reused."},"requestId":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/LoginStatus","description":"Only present when an existing pending request was reused."},"challengeCode":{"type":"string","pattern":"^[0-9]+$","description":"Number matching (step-up): show this number to the user, who picks it on the phone. Present when number matching is required (critical action, `requireNumberMatch`, risk engine, access policy, tenant policy, geo policy); a trusted device does not waive it. 2 digits by default (`TenantPolicy.challengeDigits`). Absent for passkey-only requests."},"expiresAt":{"type":"string","format":"date-time","description":"The request expires 30 seconds after creation (5 minutes with `reply`)."},"requiresPasskey":{"type":"boolean","const":true,"description":"Present (true) when the workspace policy demands a phishing-resistant factor for this action: no push is sent; approve it with `POST /webauthn/login/options` + `/webauthn/login/verify`."},"caseCode":{"type":"string","pattern":"^[A-HJ-NP-Z2-9]{3}-[A-HJ-NP-Z2-9]{3}$","description":"\"Why?\" case code of this decision (6 characters without I, O, 0 or 1, e.g. `K7P-2QX`). Show it to the user (\"Tell your support team: K7P-2QX\"); your team opens the full decision (each check, the rule that matched, what would have let it through) by this code in the dashboard (Logs → Look up a case)."},"reasons":{"type":"array","items":{"$ref":"#/components/schemas/RiskReason"},"description":"Why number matching or a passkey is needed (from a fixed list, never a score); absent for a plain push."},"destination":{"type":"object","description":"Only with a `destination`: whether the user approved it before, and the destinations it looks like (also shown on the phone).","properties":{"known":{"type":"boolean"},"lookalike":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"reason":{"type":"string","enum":["same_start_and_end","confusable","near_miss","mixed_script"]},"similarTo":{"type":"string","description":"The known destination, masked (`TR33…1326`)."}}}}}},"telemetryToken":{"type":"string","description":"Bearer token for `POST /login/{requestId}/telemetry` (behavioral biometrics widget). Only on newly created requests of workspaces with behavioral biometrics enabled."},"canary":{"type":"object","required":["url"],"description":"Mirror Alarm: have the page that shows this sign-in load `url` (a hidden image) and send a beacon to it without `.gif` (`@touchque/web` does both). If that page is not on your site, i.e. a real-time phishing proxy, the approval is refused (`reason: phishing_origin`) and the phone warns the user. Your Content-Security-Policy must allow its host in `img-src` and `connect-src`. Valid until 5 minutes after `expiresAt`.","properties":{"url":{"type":"string","format":"uri"}}}}},"LoginStatusResponse":{"type":"object","required":["status","externalUsername","type","referenceId","details","expiresAt","consumed","confirmedVia","assurance","approvalProof"],"properties":{"status":{"$ref":"#/components/schemas/LoginStatus","description":"A PENDING request past `expiresAt` is reported as EXPIRED. CONFIRMED is final and does not expire."},"externalUsername":{"type":"string","description":"Bound user — check it matches the user you are signing in."},"type":{"type":"string","description":"Action type slug the request was created with."},"referenceId":{"type":["string","null"]},"details":{"$ref":"#/components/schemas/LoginDetails"},"expiresAt":{"type":"string","format":"date-time"},"consumed":{"type":"boolean","description":"Whether `POST /login/{id}/consume` already used this approval."},"confirmedVia":{"$ref":"#/components/schemas/ConfirmedVia"},"rejectReason":{"type":"string","enum":["user_reported_coached","user_reported_not_me"],"description":"REJECTED only, when the user said more than no: someone on the phone told them to approve or to scan the code (Vishing Shield; their 2FA is frozen), or it was not them."},"pairing":{"type":"object","required":["id","keyThumbprint"],"description":"Requests created with `qrPairing`: the pairing and the browser key it binds.","properties":{"id":{"type":"string"},"keyThumbprint":{"type":"string"}}},"rejectedBy":{"type":"string","enum":["user","risk_engine","server"],"description":"REJECTED only: who ended the request — the user on the phone, the risk engine (blocked before the push, or refused after the approval), or the server (the approval carried an invalid device signature, or a device that reports itself compromised tried to approve a critical action)."},"requiresPasskey":{"type":"boolean","const":true,"description":"Present (true) for passkey-only requests, and for a PENDING request the Vishing Shield moved to a passkey at the tap (the user was on a call; `riskFactors` carry `vishing_suspected`): finish it in the browser with `POST /webauthn/login/options` + `/webauthn/login/verify`; `expiresAt` was extended by 180 seconds for the ceremony. The SDKs report it as `passkey_required`."},"assurance":{"oneOf":[{"$ref":"#/components/schemas/Assurance"},{"type":"null"}],"description":"Only for CONFIRMED requests."},"approvalProof":{"oneOf":[{"$ref":"#/components/schemas/ApprovalProof"},{"type":"null"}],"description":"Only for CONFIRMED requests approved by a hardware-bound phone."},"reply":{"type":"object","required":["ct"],"description":"CONFIRMED requests created with `reply`, when the approver typed it back: the ciphertext, decrypted with your one-time key. An approval without it means the approver approved without typing (an older app).","properties":{"ct":{"type":"string"},"at":{"type":["string","null"],"format":"date-time"}}},"provisional":{"type":"boolean","description":"Vishing Shield: the phone approved while the user was on a call. Still CONFIRMED; the user's critical actions are refused (403 `blocked` / `provisional`) until the phone re-confirms it after the call (`reconfirmedAt`, `APPROVAL_RECONFIRMED`) or the user takes it back (`revoked`, `APPROVAL_REVOKED`). Treat a session built on it as limited."},"reconfirmedAt":{"type":["string","null"],"format":"date-time","description":"When the phone re-confirmed a provisional approval after the call."},"revoked":{"type":"boolean","description":"The user took this approval back on the phone: end any session or action built on it. A revoked approval that was not consumed yet can no longer be (409 `revoked`)."},"revokeReason":{"type":["string","null"],"enum":["UNDO","COACHED",null],"description":"`COACHED`: someone on the phone told the user to approve. `UNDO`: the user undid it."}}},"ConsumeResponse":{"type":"object","required":["consumed","requestId","externalUsername","type","referenceId","details","confirmedVia","assurance","approvalProof"],"properties":{"consumed":{"type":"boolean","const":true},"requestId":{"type":"string","format":"uuid"},"externalUsername":{"type":"string"},"type":{"type":"string"},"referenceId":{"type":["string","null"]},"details":{"$ref":"#/components/schemas/LoginDetails"},"confirmedVia":{"$ref":"#/components/schemas/ConfirmedVia"},"assurance":{"$ref":"#/components/schemas/Assurance"},"approvalProof":{"oneOf":[{"$ref":"#/components/schemas/ApprovalProof"},{"type":"null"}]},"provisional":{"type":"boolean","description":"Vishing Shield: the phone approved while the user was on a call. Still CONFIRMED; the user's critical actions are refused (403 `blocked` / `provisional`) until the phone re-confirms it after the call (`reconfirmedAt`, `APPROVAL_RECONFIRMED`) or the user takes it back (`revoked`, `APPROVAL_REVOKED`). Treat a session built on it as limited."},"reconfirmedAt":{"type":["string","null"],"format":"date-time","description":"When the phone re-confirmed a provisional approval after the call."},"revoked":{"type":"boolean","description":"The user took this approval back on the phone: end any session or action built on it. A revoked approval that was not consumed yet can no longer be (409 `revoked`)."},"revokeReason":{"type":["string","null"],"enum":["UNDO","COACHED",null],"description":"`COACHED`: someone on the phone told the user to approve. `UNDO`: the user undid it."}}},"ConsumeConflict":{"description":"The request cannot be used.","allOf":[{"$ref":"#/components/schemas/CodedError"}],"properties":{"code":{"type":"string","enum":["already_used","rejected","expired","not_approved","revoked"]}}},"ReportFraudRequest":{"type":"object","properties":{"reason":{"type":"string","enum":["chargeback","account_takeover","social_engineering","other"],"default":"other"},"note":{"type":"string","maxLength":500}}},"ReportFraudResponse":{"type":"object","required":["reported","alreadyReported","requestId","trainingLabel"],"properties":{"reported":{"type":"boolean","const":true},"alreadyReported":{"type":"boolean","description":"True when this request had already been reported (idempotent)."},"requestId":{"type":"string","format":"uuid"},"trainingLabel":{"type":"boolean","description":"Whether the report was sent to the risk engine as a training label. Only a request a person acted on in the last 180 days is: approved, rejected by the user, or answered with an offline code. A request that was still pending, expired unanswered, or was ended by the risk engine or the server is recorded but is no training label; when it is under 7 days old, the user's pushes in your workspace still need number matching for the next 14 days."}}},"RecoveryRequest":{"type":"object","required":["requestId","code"],"properties":{"requestId":{"type":"string","format":"uuid","description":"A PENDING login request of this workspace."},"code":{"type":"string","description":"One of the user's recovery codes (`XXXXX-XXXXX`, Crockford base32; case, spaces and hyphens are ignored, O/I/L are folded to 0/1). Legacy 8-hex-character codes are still accepted."}}},"SuccessMessage":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"}}},"TelemetryRequest":{"type":"object","description":"Behavioral aggregates collected by the `@touchque/web` widget. All fields optional.","properties":{"mouseDistance":{"type":"number"},"mouseJitter":{"type":"number"},"clicks":{"type":"number","description":"Clicks with a mouse or a pen (a tap and Enter on a button are not counted)."},"touches":{"type":"number","description":"Touches: with these, no mouse movement is not a sign."},"keystrokes":{"type":"number"},"keystrokeSpeedAvg":{"type":"number"},"deviceSignals":{"type":"object","description":"Browser recognition signals. Stored only when the workspace enabled `TenantPolicy.deviceFingerprintingEnabled`; otherwise dropped.","properties":{"fingerprint":{"type":"string","pattern":"^tq2_fp_[0-9a-f]{32}$"},"deviceId":{"type":"string","pattern":"^tq2_[0-9a-f]{32}$"}}},"deviceFingerprint":{"type":"string","description":"v1 widget fingerprint (used only when `deviceSignals` is absent or invalid and fingerprinting is enabled)."}}},"TelemetryAccepted":{"type":"object","required":["accepted"],"properties":{"accepted":{"type":"boolean","description":"False when the request is unknown, no longer PENDING, or processing failed. Telemetry is advisory and never fails the page."}}},"BrowserProof":{"description":"Proof from the end user's browser that it holds its device-bound key: a non-extractable ECDSA P-256 key made with WebCrypto and kept in IndexedDB (`@touchque/web` `browserKey.proof()`). Pass it through untouched: the JSON object, its JSON text, or the base64url(JSON) form of the `X-Touchque-Browser` header.\n\n`{ v: 1, jwk: { kty: \"EC\", crv: \"P-256\", x, y }, aud: <page origin>, iat: <unix seconds>, nonce: <base64url, 16 random bytes>, sig: <base64url P1363 r||s> }`, signed over the UTF-8 text `touchque-browser:v1\\n{aud}\\n{iat}\\n{nonce}` with ECDSA SHA-256.\n\nAccepted when `iat` is within 5 minutes, the signature is valid, the nonce is new (each proof works once per endpoint) and, when the workspace's organization has verified domains (Organization → Domains) or the workspace has passkey origins, `aud` is an https origin on one of those sites (`http://localhost` and `http://127.0.0.1` outside production). Anything else reads as \"no known browser\", never as an error.","oneOf":[{"type":"object","required":["v","jwk","aud","iat","nonce","sig"],"properties":{"v":{"type":"integer","const":1},"jwk":{"type":"object","required":["kty","crv","x","y"],"properties":{"kty":{"type":"string","const":"EC"},"crv":{"type":"string","const":"P-256"},"x":{"type":"string"},"y":{"type":"string"}}},"aud":{"type":"string","description":"The page's origin (`location.origin`)."},"iat":{"type":"integer","description":"Unix seconds."},"nonce":{"type":"string","minLength":16,"maxLength":86},"sig":{"type":"string","description":"base64url of the 64-byte r||s signature."}}},{"type":"string","maxLength":2048,"description":"The JSON text, or base64url(JSON) as in the `X-Touchque-Browser` header."}]},"RiskCheckRequest":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string","maxLength":255,"description":"Your identifier for the user."},"type":{"type":"string","default":"LOGIN","description":"Action type slug or id, as on `POST /login/request`. A critical action is never silent. Undefined types answer 400 `unknown_action`, disabled ones 403 `action_disabled`."},"clientIp":{"type":["string","null"],"description":"The END USER's IP address. Without it the network cannot be compared with earlier sign-ins and the answer is `prompt: true`."},"userAgent":{"type":["string","null"],"description":"The end user's browser user agent."},"clientSignals":{"type":"string","maxLength":4096,"description":"Is a person at the page or software driving the browser (Playwright, a headless Chrome, an AI browser agent)? The end user's page check from `@touchque/web` `startClientSignals().encode()`, passed through untouched (booleans, counts and timings only). With signs of automation the request is never silent and asks with number matching; the workspace can block it instead (`TenantPolicy.automatedClients`: 403 `blocked` with reason `automated_client`)."},"deviceRiskRequestId":{"type":"string","pattern":"^dre_[A-Za-z0-9_-]{22}(\\.[A-Za-z0-9_-]{18})?$","description":"Device risk (a paid add-on, off by default): the page's identification from `@touchque/web` `deviceRisk.identify()`, or the script tag's `touchque_request_id` field as it came (after a dot, how the pointer moved: counts weighed as `device_pointer_synthetic`, never kept). Used only when the workspace turned Device risk on, for this account only (another account gets `device_report_replayed`). The risk engine counts the visitor and decides: its signals only ever ask for more (reported only until Touchque enforces them), a visitor your admin marked \"step up always\" asks at once, and a sign-in with a device signal is never silent."},"deviceRisk":{"allOf":[{"$ref":"#/components/schemas/DeviceRiskReport"}],"description":"Instead of deviceRiskRequestId: the page's report from `@touchque/web` `deviceRisk.collect()`, relayed untouched."},"browserProof":{"$ref":"#/components/schemas/BrowserProof"},"maxAgeSeconds":{"type":"integer","minimum":0,"description":"Your own limit on the age of the browser's last real approval; `0` asks every time. The engine's own limit (7 days by default) applies when it is shorter."},"context":{"$ref":"#/components/schemas/ActionContext"}}},"RiskReason":{"type":"object","required":["code","text"],"properties":{"code":{"type":"string","description":"Stable code; translate it in your UI. Silent: `known_browser`, `recent_approval`, `same_phone`, `familiar_network`, `no_risk_signal`.\nAsked by the risk engine: `no_browser_key`, `new_browser`, `approval_too_old`, `policy_every_sign_in`, `other_phone`, `new_country`, `new_network`, `network_unknown`, `hosting_network`, `critical_action`, `sensitive_action`, `risk_signal`, `push_fatigue`, `fraud_report`, `new_factor`, `vishing_shield`, `checks_incomplete`, `not_a_sign_in`.\nAsked by the workspace or the server: `silent_mode_shadow` (silent sign-ins are measured, not used yet), `phone_not_linked`, `frozen`, `phone_keys_lost`, `passkey_required_by_policy`, `policy_always_challenge`, `step_up_pending`, `access_policy` (an access or country rule), `risk_signal` (also for local facts such as repeated rejections), `engine_unavailable`.\nFrom your own Actions: `action` (with `action`, the Action's slug; `text` is the reason the Action gave).\nNew codes may be added."},"text":{"type":"string","description":"Plain English explanation."},"action":{"type":"string","description":"For `code: action`: the slug of the Action that asked."}}},"ActionContext":{"type":["object","null"],"additionalProperties":true,"maxProperties":50,"description":"Anything your Actions should know about this request and that Touchque cannot (roles, plan, amount, tenant). At most 4 KB of JSON. Your Actions read it as `event.context`; it is never shown on the phone and not stored on the request (only in the recorded Actions event, kept 30 days)."},"ActionTrigger":{"type":"string","enum":["login.request","risk.check","approval.decided","shield.decision","fraud.reported","device.linked","device.keys_lost","user.unlinked","passkey.added","passkey.removed","recovery.updated"],"description":"Where in the pipeline the Action runs. `login.request` and `risk.check` run inside the request and can change its decision; the others run after the fact and can only notify, call allowed hosts and freeze the user."},"ActionCapability":{"type":"string","enum":["skip_number_match","override_block","allow_silent"],"description":"A loosening power. A version that asks for one is `pending_approval` until an admin approves it on their phone in the dashboard; until then the earlier version keeps running."},"ActionTemplateRef":{"type":"object","required":["id"],"properties":{"id":{"type":"string","enum":["alert","block_or_step_up","require_passkey_for_role","always_ask_when"]},"params":{"type":"object","additionalProperties":true,"description":"See `GET /actions/templates`."}}},"DeployAction":{"type":"object","description":"Send `code` or `template`. Deploying the same code and settings again changes nothing (same version).","properties":{"trigger":{"$ref":"#/components/schemas/ActionTrigger","description":"Required when the Action is new; cannot change later."},"name":{"type":"string","maxLength":255},"code":{"type":"string","maxLength":65536,"description":"JavaScript exporting `onExecute`: `exports.onExecute = async (event, api) => { ... }`. Runs in a sandbox (QuickJS in WebAssembly): 50 ms CPU, 16 MB, no filesystem, network only through `api.fetch` to `allowedHosts`."},"template":{"$ref":"#/components/schemas/ActionTemplateRef"},"secrets":{"type":"array","maxItems":10,"items":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,63}$"},"description":"Names of secrets this version may read (`event.secrets.NAME`) or use in notifications."},"allowedHosts":{"type":"array","maxItems":10,"items":{"type":"string"},"description":"The only hosts `api.fetch` and webhook notifications may reach (`*.example.com` allowed)."},"capabilities":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/ActionCapability"}},"onFailure":{"type":"string","enum":["continue","step_up","deny"],"default":"continue","description":"Pre triggers only: what an error or timeout of this Action means. `continue` never blocks a sign-in."},"order":{"type":"integer","minimum":0,"maximum":10000,"default":100},"enabled":{"type":"boolean","default":true},"activate":{"type":"boolean","default":true,"description":"`false` saves the version without making it live."}}},"Action":{"type":"object","required":["slug","name","trigger","enabled","order","onFailure","activeVersion"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"trigger":{"$ref":"#/components/schemas/ActionTrigger"},"enabled":{"type":"boolean"},"order":{"type":"integer"},"onFailure":{"type":"string","enum":["continue","step_up","deny"]},"activeVersion":{"type":["integer","null"]},"pendingApproval":{"type":"integer","description":"Versions waiting for an admin's phone approval (list only)."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ActionVersion":{"type":"object","required":["number","status","active"],"properties":{"number":{"type":"integer"},"status":{"type":"string","enum":["ready","pending_approval","rejected"]},"active":{"type":"boolean"},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}},"approvedCapabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}},"allowedHosts":{"type":"array","items":{"type":"string"}},"secrets":{"type":"array","items":{"type":"string"}},"template":{"oneOf":[{"$ref":"#/components/schemas/ActionTemplateRef"},{"type":"null"}]},"codeHash":{"type":"string"},"code":{"type":"string","description":"`GET /actions/{slug}/versions/{number}` only."},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":["string","null"]},"approvedAt":{"type":["string","null"],"format":"date-time"}}},"ActionDeployResult":{"type":"object","required":["action","version","deployed","approval","warnings"],"properties":{"action":{"$ref":"#/components/schemas/Action"},"version":{"$ref":"#/components/schemas/ActionVersion"},"deployed":{"type":"boolean","description":"This version is the one running now."},"approval":{"type":["object","null"],"properties":{"required":{"type":"boolean"},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}},"message":{"type":"string"}}},"warnings":{"type":"array","items":{"type":"string"},"description":"E.g. a decision call the trigger does not offer, a capability not asked for, a secret not set yet."}}},"ActionDetail":{"type":"object","properties":{"action":{"$ref":"#/components/schemas/Action"},"code":{"type":["string","null"],"description":"The active version's code (or the latest one)."},"versions":{"type":"array","items":{"$ref":"#/components/schemas/ActionVersion"}}}},"UpdateAction":{"type":"object","properties":{"name":{"type":"string","maxLength":255},"enabled":{"type":"boolean"},"order":{"type":"integer","minimum":0,"maximum":10000},"onFailure":{"type":"string","enum":["continue","step_up","deny"]}}},"ActionCall":{"type":"object","required":["kind","reason"],"properties":{"kind":{"type":"string","enum":["deny","stepUp","requirePasskey","prompt","skipNumberMatch","overrideBlock","allowSilent"]},"reason":{"type":"string"}}},"ActionLogLine":{"type":"object","properties":{"level":{"type":"string"},"message":{"type":"string"},"at":{"type":"integer","description":"ms since the run started"}}},"ActionTestRequest":{"type":"object","description":"A draft (`trigger` + `code` or `template`, as for a deploy) or a deployed Action (`slug`), run once on a recorded event (`eventId`), your own `event` (merged over the trigger's sample) or the sample. Notifications are not sent; `api.fetch` works (set `network: false` to turn it off).","properties":{"trigger":{"$ref":"#/components/schemas/ActionTrigger"},"code":{"type":"string"},"template":{"$ref":"#/components/schemas/ActionTemplateRef"},"slug":{"type":"string"},"secrets":{"type":"array","items":{"type":"string"}},"allowedHosts":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}},"eventId":{"type":"string"},"event":{"type":"object","additionalProperties":true},"network":{"type":"boolean","default":true}}},"ActionTestResult":{"type":"object","properties":{"outcome":{"type":"string","enum":["ok","error","timeout"]},"error":{"type":["string","null"]},"durationMs":{"type":"integer"},"cpuMs":{"type":"number"},"calls":{"type":"array","items":{"$ref":"#/components/schemas/ActionCall"}},"notifications":{"type":"array","items":{"type":"object","additionalProperties":true}},"logs":{"type":"array","items":{"$ref":"#/components/schemas/ActionLogLine"}},"reasons":{"type":"array","items":{"$ref":"#/components/schemas/RiskReason"}},"wouldBe":{"type":["object","null"],"additionalProperties":true,"description":"For a recorded pre-trigger event: the decision with this Action alone (`verdict`: deny, passkey, number_match, push; or prompt, silent)."},"eventId":{"type":["string","null"]},"event":{"type":"object","additionalProperties":true},"assumedCapabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}},"note":{"type":"string"}}},"ActionDryRunRequest":{"type":"object","description":"A draft (or `slug` for the deployed version) replayed against the last `limit` recorded events of its trigger. `replaces`: the slug of the deployed Action the draft would replace, so its current version is left out of the comparison.","properties":{"trigger":{"$ref":"#/components/schemas/ActionTrigger"},"code":{"type":"string"},"template":{"$ref":"#/components/schemas/ActionTemplateRef"},"slug":{"type":"string"},"replaces":{"type":"string"},"secrets":{"type":"array","items":{"type":"string"}},"allowedHosts":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}},"onFailure":{"type":"string","enum":["continue","step_up","deny"]},"limit":{"type":"integer","minimum":1,"maximum":1000,"default":500},"since":{"type":"string","format":"date-time"}}},"ActionDryRunResult":{"type":"object","properties":{"trigger":{"$ref":"#/components/schemas/ActionTrigger"},"evaluated":{"type":"integer"},"from":{"type":["string","null"],"format":"date-time"},"to":{"type":["string","null"],"format":"date-time"},"matched":{"type":"integer","description":"Events where the draft made a decision call or a notification."},"changed":{"type":["integer","null"],"description":"Pre triggers: decisions that would have come out differently."},"transitions":{"type":["object","null"],"additionalProperties":{"type":"integer"},"description":"e.g. `{ \"push→number_match\": 12, \"push→deny\": 1 }`."},"notifications":{"type":"object","additionalProperties":{"type":"integer"},"description":"Would-be notifications by type (not sent)."},"errors":{"type":"integer"},"timeouts":{"type":"integer"},"errorSamples":{"type":"array","items":{"type":"object","additionalProperties":true}},"p50Ms":{"type":"integer"},"p95Ms":{"type":"integer"},"samples":{"type":"array","items":{"type":"object","additionalProperties":true}},"assumedCapabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}}}},"ActionEventList":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"trigger":{"$ref":"#/components/schemas/ActionTrigger"},"time":{"type":"string","format":"date-time"},"user":{"type":["string","null"]},"requestId":{"type":["string","null"]},"outcome":{"type":["object","null"],"additionalProperties":true},"event":{"type":"object","additionalProperties":true}}}}}},"ActionLogList":{"type":"object","properties":{"logs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"time":{"type":"string","format":"date-time"},"version":{"type":["integer","null"]},"eventId":{"type":["string","null"]},"requestId":{"type":["string","null"]},"outcome":{"type":"string","enum":["ok","error","timeout"]},"decision":{"type":["string","null"]},"calls":{"type":"array","items":{"$ref":"#/components/schemas/ActionCall"}},"notifications":{"type":"array","items":{"type":"object","additionalProperties":true}},"logs":{"type":"array","items":{"$ref":"#/components/schemas/ActionLogLine"}},"error":{"type":["string","null"]},"durationMs":{"type":"integer"}}}}}},"ActionMetrics":{"type":"object","properties":{"window":{"type":"string","enum":["1h","24h","7d","30d"]},"runs":{"type":"integer"},"errors":{"type":"integer"},"timeouts":{"type":"integer"},"errorRate":{"type":"number"},"p50Ms":{"type":"integer"},"p95Ms":{"type":"integer"},"decisions":{"type":"object","additionalProperties":{"type":"integer"}},"notifications":{"type":"object","properties":{"sent":{"type":"integer"},"failed":{"type":"integer"},"skipped":{"type":"integer"}}}}},"ActionSecret":{"type":"object","properties":{"name":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ActionCatalog":{"type":"object","properties":{"triggers":{"type":"array","items":{"type":"object","additionalProperties":true}},"capabilities":{"type":"array","items":{"type":"object","additionalProperties":true}},"limits":{"type":"object","additionalProperties":true}}},"ActionTemplateList":{"type":"object","properties":{"templates":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"RenderedTemplate":{"type":"object","properties":{"code":{"type":"string"},"secrets":{"type":"array","items":{"type":"string"}},"allowedHosts":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/ActionCapability"}}}},"RiskCheckResult":{"type":"object","required":["prompt","reasons","assurance","silentMode"],"properties":{"prompt":{"type":"boolean","description":"`false`: skip the phone approval for this sign-in (the browser proved its key, passed a real second factor recently on the same phone, and nothing about the sign-in is new). Anything else: ask as usual (`POST /login/request` with the same `browserProof`). Treat a missing or non-boolean value as `true`."},"reasons":{"type":"array","items":{"$ref":"#/components/schemas/RiskReason"},"description":"Why it is silent, or why it asks (most important first)."},"assurance":{"type":"string","enum":["high","normal","low"],"description":"`high`: nothing new about this sign-in; `normal`: nothing wrong, but something is new or unknown; `low`: a risk signal."},"silentMode":{"type":"string","enum":["enforce","shadow"],"description":"`shadow`: silent sign-ins are only measured (`prompt` is always `true`), also when the risk engine could not be asked."},"caseCode":{"type":"string","pattern":"^[A-HJ-NP-Z2-9]{3}-[A-HJ-NP-Z2-9]{3}$","description":"\"Why?\" case code of this decision (6 characters without I, O, 0 or 1, e.g. `K7P-2QX`). Show it to the user (\"Tell your support team: K7P-2QX\"); your team opens the full decision (each check, the rule that matched, what would have let it through) by this code in the dashboard (Logs → Look up a case)."}}},"BrowserKeyInfo":{"type":"object","required":["thumbprint","label","createdAt","lastMfaAt","lastMfaMethod","lastSeenAt","lastSilentAt"],"properties":{"thumbprint":{"type":"string","description":"RFC 7638 thumbprint of the browser's public key (base64url SHA-256, 43 characters)."},"label":{"type":["string","null"],"description":"e.g. `Chrome · macOS`, from the user agent of the request that made it known."},"createdAt":{"type":"string","format":"date-time"},"lastMfaAt":{"type":["string","null"],"format":"date-time","description":"Its last real second factor."},"lastMfaMethod":{"type":["string","null"],"enum":["push","passkey","offline_qr",null]},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"lastSilentAt":{"type":["string","null"],"format":"date-time","description":"Its last silent sign-in."}}},"RecoveryVouchersRequest":{"type":"object","required":["externalUsername","vouchers"],"properties":{"externalUsername":{"type":"string","maxLength":255},"vouchers":{"type":"array","maxItems":20,"items":{"type":"string","maxLength":255},"description":"Other users of this workspace; at most 5 are kept."}},"additionalProperties":false},"RecoveryVoucherEligibility":{"type":"object","required":["externalUsername","eligible"],"properties":{"externalUsername":{"type":"string"},"eligible":{"type":"boolean"},"reason":{"type":"string","enum":["no_phone","no_hardware_key","keys_lost","frozen","on_probation","same_phone"]}},"additionalProperties":false},"QrPairingCreate":{"type":"object","required":["nonceHash","keyThumbprint"],"properties":{"nonceHash":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$","description":"base64url SHA-256 of the one-time nonce you put in the QR code (at least 20 characters; 32 random bytes recommended). Touchque never sees the nonce until the phone scans it."},"keyThumbprint":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$","description":"RFC 7638 thumbprint of the browser's own non-extractable key: the pairing binds to exactly that browser."},"clientIp":{"type":["string","null"],"description":"The browser's address: shown on the phone with its place, and compared with the phone's."},"userAgent":{"type":["string","null"]},"browser":{"type":["string","null"],"maxLength":60,"description":"What the phone names the browser, e.g. `Chrome 155`."},"os":{"type":["string","null"],"maxLength":60,"description":"Its operating system, e.g. `macOS`."},"ttlSeconds":{"type":"integer","minimum":30,"maximum":300,"default":120,"description":"How long the code can be scanned."}}},"QrPairingCreated":{"type":"object","required":["pairingId","expiresAt"],"properties":{"pairingId":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"}}},"QrPairing":{"type":"object","required":["pairingId","state","keyThumbprint","expiresAt","requestId"],"properties":{"pairingId":{"type":"string"},"state":{"type":"string","enum":["waiting","scanned","confirmed","requested","refused","reported","expired"],"description":"`scanned`: a phone read the code and shows what is being paired; `confirmed`: the person went on, ask the phone now (`POST /login/request` with `qrPairing`); `requested`: asked (`requestId`); `refused`: you refused it, or the person declined it on the phone (`outcome.reason: declined`); `reported`: the person said someone told them to scan it (their 2FA is frozen: treat it as an attack)."},"keyThumbprint":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"externalUsername":{"type":"string","description":"Once scanned: the account the phone holds with your workspace."},"scannedAt":{"type":"string","format":"date-time"},"confirmedAt":{"type":["string","null"],"format":"date-time"},"phone":{"type":"object","properties":{"country":{"type":["string","null"]},"location":{"type":["string","null"]}}},"browser":{"type":"object","properties":{"country":{"type":["string","null"]},"location":{"type":["string","null"]}}},"proximity":{"type":"object","description":"How near the phone that scanned was to the browser: each `true`, `false` or `null` (cannot be told, e.g. a private address).","properties":{"sameNetwork":{"type":["boolean","null"]},"sameCountry":{"type":["boolean","null"]},"warnings":{"type":"array","items":{"type":"string","enum":["different_country","different_network"]}}}},"requestId":{"type":["string","null"]},"outcome":{"type":"object","properties":{"reason":{"type":"string"},"message":{"type":"string"},"caseCode":{"type":["string","null"]},"deviceCode":{"type":["string","null"]},"at":{"type":"string","format":"date-time"}}},"reportedAt":{"type":"string","format":"date-time"}}},"QrPairingOutcome":{"type":"object","required":["reason","message"],"properties":{"reason":{"type":"string","enum":["policy","device_not_approved","too_many_browsers","rate_limited","no_account","other"]},"message":{"type":"string","maxLength":300,"description":"What the phone shows."},"caseCode":{"type":["string","null"],"description":"Your own case code for the refusal, shown on the phone."},"deviceCode":{"type":["string","null"],"description":"The computer's code an administrator approves (`K7Q-2PD`)."}}},"RecoveryVouchers":{"type":"object","required":["externalUsername","vouchers"],"properties":{"externalUsername":{"type":"string"},"vouchers":{"type":"array","items":{"$ref":"#/components/schemas/RecoveryVoucherEligibility"}},"updatedAt":{"type":["string","null"],"format":"date-time"},"probationUntil":{"type":["string","null"],"format":"date-time","description":"The user's own account is on probation after a vouch recovery until then."}},"additionalProperties":false},"RecoveryStartRequest":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string","maxLength":255},"clientIp":{"type":"string","description":"The user's IP address (what the risk engine and the vouchers see)."},"userAgent":{"type":"string","maxLength":1000},"displayName":{"type":"string","maxLength":255,"description":"How the vouchers know the user (\"Ada Lovelace\")."},"fallbackVouchers":{"type":"array","maxItems":20,"items":{"type":"string","maxLength":255},"description":"Fill up a short named list, e.g. with the organization's admins. Never instead of the named vouchers."},"required":{"type":"integer","minimum":2,"maximum":5,"description":"More vouchers than the floor of 2."},"coolOffMinutes":{"type":"integer","minimum":1,"maximum":10080,"description":"A longer cooling-off than the floor (15 minutes; default 60)."},"details":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/LoginDetail"},"description":"What the vouchers' phones show instead of the default English lines."},"noticeDetails":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/LoginDetail"},"description":"What the old phone shows (approve to move now, reject to stop) instead of the default English lines."}},"additionalProperties":false},"RecoveryVoucherState":{"type":"object","required":["externalUsername","state"],"properties":{"externalUsername":{"type":"string"},"source":{"type":"string","enum":["nominated","fallback"]},"state":{"type":"string","enum":["waiting","pending","approved","expired","unreachable"]},"code":{"type":["string","null"],"description":"The number this voucher must type, while their request is open."}},"additionalProperties":false},"Recovery":{"type":"object","required":["recoveryId","externalUsername","method","status"],"properties":{"recoveryId":{"type":"string","format":"uuid"},"externalUsername":{"type":"string"},"method":{"type":"string","enum":["vouch","old_phone","transfer"]},"status":{"type":"string","enum":["AWAITING_DEVICE","VOUCHING","COOLING","COMPLETED","VETOED","EXPIRED","FAILED"]},"required":{"type":"integer"},"approvals":{"type":"integer"},"vouchers":{"type":"array","items":{"$ref":"#/components/schemas/RecoveryVoucherState"}},"oldPhone":{"type":"object","properties":{"notified":{"type":"boolean"},"code":{"type":["string","null"]}},"additionalProperties":false},"coolOffMinutes":{"type":"integer"},"coolOffUntil":{"type":["string","null"],"format":"date-time"},"elevated":{"type":"boolean"},"reasons":{"type":"array","items":{"type":"string","enum":["risk_engine_unavailable","risk_hard_signals","risk_challenge","recent_fraud_report","recent_recovery","vouchers_recently_changed"]}},"location":{"type":["string","null"]},"newPhoneBound":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time"},"completedAt":{"type":["string","null"],"format":"date-time"},"endedReason":{"type":["string","null"],"description":"e.g. `cooling_off_ended`, `old_phone_approved`, `voucher_rejected`, `old_phone_rejected`, `voucher_revoked`, `partner_veto`, `not_enough_approvals`, `not_bound`, `replaced`, `new_phone_key_changed`."},"endedBy":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"receipts":{"type":"array","items":{"$ref":"#/components/schemas/RecoveryReceipt"}}},"additionalProperties":true},"RecoveryReceipt":{"type":"object","description":"One step of the recovery, chained: `hash` is SHA-256 (hex) of the canonical JSON (sorted keys) of every other field, and `prev` is the hash of the step before. A voucher's `vouched` step carries the signature of its hardware key over the approval.","required":["seq","at","step","actor","data","prev","hash"],"properties":{"seq":{"type":"integer","minimum":1},"at":{"type":"string","format":"date-time"},"step":{"type":"string","enum":["started","device_bound","vouched","quorum_reached","completed","vetoed","expired","failed","transfer_started"]},"actor":{"type":"object","additionalProperties":true},"data":{"type":"object","additionalProperties":true},"prev":{"type":["string","null"]},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"additionalProperties":false},"RecoveryStarted":{"allOf":[{"$ref":"#/components/schemas/Recovery"}],"type":"object","required":["bindCode","bindUri"],"properties":{"bindCode":{"type":"string","description":"One-time; shown once."},"bindUri":{"type":"string","description":"Show it as a QR for the Touchque app on the new phone: `touchque://recover?id=…&code=…`."},"riskContext":{"type":"object","additionalProperties":true}}},"RecoveryList":{"type":"object","required":["recoveries"],"properties":{"recoveries":{"type":"array","items":{"$ref":"#/components/schemas/Recovery"}}},"additionalProperties":false},"RecoveryVetoRequest":{"type":"object","properties":{"by":{"type":"string","maxLength":255,"description":"Who stopped it (shown in the receipts), e.g. the admin's e-mail."},"reason":{"type":"string","pattern":"^[a-z_]{1,40}$","description":"Default `partner_veto`."}},"additionalProperties":false},"RecoveryEventWebhookPayload":{"type":"object","description":"Body of a recovery event. `signature` is the HMAC of every other field.","required":["event","externalUsername","integrationId","timestamp","jti","recoveryId","status","signature"],"properties":{"event":{"type":"string","enum":["RECOVERY_STARTED","RECOVERY_DEVICE_BOUND","RECOVERY_COOLING","RECOVERY_COMPLETED","RECOVERY_VETOED","RECOVERY_EXPIRED","RECOVERY_FAILED"]},"externalUsername":{"type":"string"},"integrationId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"jti":{"type":"string","format":"uuid"},"recoveryId":{"type":"string","format":"uuid"},"status":{"type":"string"},"method":{"type":"string","description":"`RECOVERY_COMPLETED`: `vouch` or `old_phone`."},"probationUntil":{"type":["string","null"],"format":"date-time","description":"`RECOVERY_COMPLETED` by vouch: critical actions are refused until then."},"reason":{"type":"string","description":"Ended events: why."},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"additionalProperties":true},"BrowserKeyList":{"type":"object","required":["browsers"],"properties":{"browsers":{"type":"array","items":{"$ref":"#/components/schemas/BrowserKeyInfo"}}}},"ForgetBrowsersRequest":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string","maxLength":255},"thumbprint":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$","description":"One browser; omitted: all of the user's browsers."}}},"ForgetBrowsersResult":{"type":"object","required":["forgotten"],"properties":{"forgotten":{"type":"integer","minimum":0,"description":"How many browsers were forgotten."}}},"ExternalUsernameRequest":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string","description":"Your identifier for the user."}}},"EnrollmentSecret":{"type":"object","required":["secret","qrCodeDataUrl","externalUsername","expiresAt","recoveryCodes"],"properties":{"secret":{"type":"string","pattern":"^[0-9A-F]{40}$","description":"One-time device-pairing token (40 upper-case hex characters) that the Touchque app links via QR scan. Not an RFC 6238 TOTP secret. Returned only once."},"qrCodeDataUrl":{"type":["string","null"],"description":"`data:image/png;base64,...` QR code encoding `secret`; `null` if rendering failed."},"externalUsername":{"type":"string"},"expiresAt":{"type":"string","format":"date-time","description":"The secret must be linked within 5 minutes."},"recoveryCodes":{"type":"array","items":{"type":"string"},"minItems":10,"maxItems":10,"description":"Ten one-time recovery codes (`XXXXX-XXXXX`). Show them to the user once; they are stored hashed."}}},"GeneratedSecret":{"allOf":[{"$ref":"#/components/schemas/EnrollmentSecret"}],"required":["ttlMs"],"properties":{"ttlMs":{"type":"integer","const":300000}}},"ResetSecret":{"allOf":[{"$ref":"#/components/schemas/EnrollmentSecret"}],"required":["success","message"],"properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"}}},"SecretConflict":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["secretId","hint"],"properties":{"secretId":{"type":"string","format":"uuid"},"hint":{"type":"string"},"expiresAt":{"type":["string","null"],"format":"date-time"},"ttlRemainingMs":{"type":["integer","null"]}}},"OfflineChallengeRequest":{"type":"object","required":["externalUsername","type"],"properties":{"externalUsername":{"type":"string"},"type":{"type":"string","description":"Action type slug or id (as in `POST /login/request`)."},"details":{"$ref":"#/components/schemas/LoginDetailsInput","description":"Required for critical action types."},"clientIp":{"type":["string","null"]},"userAgent":{"type":["string","null"]},"ttlSeconds":{"type":"integer","minimum":30,"maximum":300,"default":120},"requestId":{"type":"string","format":"uuid","description":"The push login request this QR is a fallback for. Its number-matching code is reused, and a request the phone already REJECTED is refused (409)."},"requireNumberMatch":{"type":"boolean","description":"Add a 2-digit matching number (when no linked request supplies one)."},"includeQrImage":{"type":"boolean","default":true,"description":"Set false to skip rendering `qrDataUrl`."}}},"OfflineChallenge":{"type":"object","required":["challengeId","qr","qrDataUrl","expiresAt","expiresInSeconds","totpAvailable"],"properties":{"challengeId":{"type":"string","format":"uuid"},"qr":{"type":"string","description":"Text to render as a QR code (prefix `TQ2`), scanned by the user's phone."},"qrDataUrl":{"type":["string","null"],"description":"Ready-to-render PNG data URL of `qr`, or null (`includeQrImage: false` or rendering failed)."},"expiresAt":{"type":"string","format":"date-time"},"expiresInSeconds":{"type":"integer"},"totpAvailable":{"type":"boolean","description":"Whether the typed time-based code fallback (`POST /offline/totp/verify`) is enabled."},"challengeCode":{"type":"string","description":"Number matching: print this under the QR; the phone offers it among two decoys."}}},"OfflineVerifyRequest":{"type":"object","required":["challengeId","code"],"properties":{"challengeId":{"type":"string","format":"uuid"},"code":{"type":"string","description":"7 characters from `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` shown by the phone; case-insensitive, spaces and hyphens ignored."}}},"OfflineVerified":{"type":"object","required":["approved","challengeId","externalUsername","type"],"properties":{"approved":{"type":"boolean","const":true},"challengeId":{"type":"string","format":"uuid"},"externalUsername":{"type":"string"},"type":{"type":"string"}}},"OfflineTotpVerifyRequest":{"type":"object","required":["externalUsername","code"],"properties":{"externalUsername":{"type":"string"},"code":{"type":"string","description":"The rolling 7-character code from the app (same alphabet as `/offline/verify`)."},"type":{"type":"string","description":"Action type. Optional — but the refusal of critical action types only happens when it is sent, and when it is omitted, pending push requests of ALL types are superseded on success."},"requestId":{"type":"string","format":"uuid","description":"Linked push login request; a REJECTED one is refused (410)."},"clientIp":{"type":"string","description":"End-user IP (logging only)."}}},"OfflineTotpVerified":{"type":"object","required":["approved","externalUsername"],"properties":{"approved":{"type":"boolean","const":true},"externalUsername":{"type":"string"}}},"OfflineTotpVerifiersRequest":{"type":"object","required":["externalUsernames"],"properties":{"externalUsernames":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"string","maxLength":255},"description":"Users × steps at most 24,000 a call (50 users for 4 hours, 1,000 for 12 minutes)."},"fromStep":{"type":"integer","description":"First 30-second step (default: the current one; from the current step - 1)."},"steps":{"type":"integer","minimum":1,"maximum":480,"description":"How many steps (default 480: 4 hours). The window ends at most 480 steps after the current one."}}},"OfflineTotpVerifiers":{"type":"object","required":["stepSeconds","fromStep","steps","notAfterStep","kdf","users"],"properties":{"stepSeconds":{"type":"integer","const":30},"fromStep":{"type":"integer"},"steps":{"type":"integer"},"notAfterStep":{"type":"integer"},"kdf":{"type":"object","required":["name","iterations","length"],"properties":{"name":{"type":"string","const":"pbkdf2-sha256"},"iterations":{"type":"integer"},"length":{"type":"integer","description":"Bytes per digest (4)."}}},"users":{"type":"array","description":"Only users who can use time-based codes now (a linked phone set up for offline codes, approvals not frozen).","items":{"type":"object","required":["externalUsername","fromStep","salt","digests"],"properties":{"externalUsername":{"type":"string"},"fromStep":{"type":"integer","description":"This user's first step (later than the request's when earlier steps were used)."},"salt":{"type":"string","description":"Base64, 16 bytes."},"digests":{"type":"string","description":"Base64 of kdf.length bytes per digest, two per step from fromStep: PBKDF2-SHA256(code, salt || \":\" || step, iterations, length) of the anchored and the unanchored code. A typed code (upper case, no dashes or spaces) matches when its digest is one of the step's two, at the current step or one step either side. Accept each step once."}}}}}},"ActionType":{"type":"object","required":["id","integrationId","type","name","critical","active"],"properties":{"id":{"type":"string","format":"uuid"},"integrationId":{"type":"string"},"type":{"type":"string","description":"Slug used as `type` in `POST /login/request`."},"name":{"type":"string","description":"Display name on the approval screen."},"description":{"type":["string","null"]},"icon":{"type":["string","null"]},"critical":{"type":"boolean","description":"Critical actions always use number matching, refuse recovery codes (unless `TenantPolicy.allowRecoveryForCritical`) and are refused by the offline TOTP fallback."},"active":{"type":"boolean","description":"Disabled types (Dashboard) answer 403 `action_disabled`."},"secondThought":{"type":["object","null"],"description":"Second Thought: at most two short yes/no questions the phone asks before Approve, from a fixed list (`someone_guiding`, `safe_account`, `remote_access`, `asked_for_code`, `support_call`, `met_online`, `promised_return`, `know_recipient`, `sim_in_hand`), in `locale` (tr or en). A risky answer, no answer (an app that does not know the questions yet) or answers whose signature does not verify make the approval provisional, as an approval during a call is: `provisional: true` on the result, the user's critical actions wait, and the phone asks \"Was this you?\" later (`APPROVAL_RECONFIRMED` / `APPROVAL_REVOKED`). Hold the action until it is reconfirmed. `null` turns it off.","properties":{"questions":{"type":"array","maxItems":2,"items":{"type":"string","enum":["someone_guiding","safe_account","remote_access","asked_for_code","support_call","met_online","promised_return","know_recipient","sim_in_hand"]}},"locale":{"type":"string","enum":["tr","en"]}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpsertActionType":{"type":"object","required":["type","name"],"properties":{"type":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$"},"name":{"type":"string"},"description":{"type":"string","description":"Only updated when sent."},"critical":{"type":"boolean","description":"Only updated when sent; defaults to false on create."},"secondThought":{"type":["object","null"],"description":"Only updated when sent; null turns them off. Second Thought questions the phone asks before Approve (see ActionType.secondThought).","properties":{"questions":{"type":"array","maxItems":2,"items":{"type":"string","enum":["someone_guiding","safe_account","remote_access","asked_for_code","support_call","met_online","promised_return","know_recipient","sim_in_hand"]}},"locale":{"type":"string","enum":["tr","en"]}}}}},"User":{"type":"object","description":"A user's enrollment (one per externalUsername).","required":["externalUsername","deviceId","used","frozen","createdAt","expireAt"],"properties":{"externalUsername":{"type":"string"},"deviceId":{"type":["string","null"],"description":"Linked phone, or null when not linked yet."},"used":{"type":"boolean","description":"True once the enrollment secret was linked to a phone."},"frozen":{"type":"boolean","description":"Manual freeze flag (automatic freezes after rejections are time-limited and not reflected here)."},"createdAt":{"type":"string","format":"date-time"},"expireAt":{"type":["string","null"],"format":"date-time","description":"Link deadline of the enrollment secret."},"device":{"oneOf":[{"$ref":"#/components/schemas/DevicePosture"},{"type":"null"}],"description":"The linked phone's posture, or null before a phone is linked."}}},"DevicePosture":{"type":"object","description":"What the Touchque app last reported about the phone it runs on, for device trust. Self-reported by the app; `signed` says the phone's hardware approval key signed it with an approval. `state`: `compromised` (jailbroken/rooted or tampered), `unknown` (no report, or older than 14 days), `at_risk` (no screen lock, approval keys destroyed, no hardware key, debugger or developer mode), else `healthy`.","required":["platform","osVersion","appVersion","screenLock","jailbroken","compromised","hardwareKey","attestation","signed","reportedAt","lastSeenAt","state","issues"],"properties":{"platform":{"type":["string","null"],"enum":["ios","android",null]},"model":{"type":["string","null"],"description":"Hardware model, e.g. `iPhone15,3` or `Pixel 8`."},"osVersion":{"type":["string","null"],"description":"e.g. `18.1` or `15`."},"securityPatch":{"type":["string","null"],"description":"Android security patch level (`YYYY-MM-DD`); null on iOS."},"appVersion":{"type":["string","null"]},"screenLock":{"type":["boolean","null"],"description":"A passcode, PIN, pattern or password is set."},"biometrics":{"type":["string","null"],"enum":["face","touch","fingerprint","iris","none",null]},"jailbroken":{"type":"boolean","description":"Jailbroken or rooted, by the app's checks."},"compromised":{"type":"boolean","description":"Jailbroken/rooted or tampered; sticky until the phone reports clean for a cool-down."},"hardwareKey":{"type":"boolean","description":"Approvals are signed by a Secure Enclave / Keystore key that still works."},"attestation":{"type":["string","null"],"enum":["apple-appattest","android-key",null],"description":"How the hardware key was proven genuine; null when it was not attested."},"signed":{"type":"boolean","description":"The latest report was signed by the hardware key."},"hardware":{"oneOf":[{"type":"object","description":"What the phone's secure hardware attested at the latest approval (Android Key Attestation of a fresh key made with the posture's challenge): not self-reported.","properties":{"verifiedBoot":{"type":["string","null"],"enum":["verified","self_signed","unverified","failed",null]},"bootloaderLocked":{"type":["boolean","null"]},"osVersion":{"type":["string","null"]},"osPatchLevel":{"type":["string","null"],"description":"`YYYY-MM`."},"securityLevel":{"type":["string","null"],"enum":["tee","strongbox",null]},"attestedAt":{"type":["string","null"],"format":"date-time"}}},{"type":"null"}]},"reportedAt":{"type":["string","null"],"format":"date-time","description":"When the latest report came."},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"linkedAt":{"type":["string","null"],"format":"date-time"},"state":{"type":"string","enum":["healthy","at_risk","compromised","unknown"]},"issues":{"type":"array","items":{"type":"string","enum":["jailbroken","tampered","boot_not_verified","integrity_failed","no_report","stale_report","no_screen_lock","keys_lost","no_hardware_key","debugger","developer_mode","bootloader_unlocked"]}}}},"PublicKeyCredentialCreationOptionsJSON":{"type":"object","description":"WebAuthn registration options as produced by `@simplewebauthn/server` `generateRegistrationOptions` (pass to `navigator.credentials.create` via `@simplewebauthn/browser` / `@touchque/web`). User verification is always required.","required":["challenge","rp","user","pubKeyCredParams"],"properties":{"challenge":{"type":"string","description":"base64url"},"rp":{"type":"object","properties":{"name":{"type":"string"},"id":{"type":"string"}}},"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"displayName":{"type":"string"}}},"pubKeyCredParams":{"type":"array","items":{"type":"object"}},"timeout":{"type":"integer"},"excludeCredentials":{"type":"array","items":{"type":"object"}},"authenticatorSelection":{"type":"object"},"attestation":{"type":"string","enum":["none","direct"],"description":"`direct` only when `TenantPolicy.requireVerifiedAuthenticator` is on."}},"additionalProperties":true},"PublicKeyCredentialRequestOptionsJSON":{"type":"object","description":"WebAuthn authentication options as produced by `@simplewebauthn/server` `generateAuthenticationOptions` (pass to `navigator.credentials.get`).","required":["challenge"],"properties":{"challenge":{"type":"string","description":"base64url"},"rpId":{"type":"string"},"allowCredentials":{"type":"array","items":{"type":"object"}},"userVerification":{"type":"string","enum":["required","preferred"]},"timeout":{"type":"integer"}},"additionalProperties":true},"WebAuthnCredentialResponseJSON":{"type":"object","description":"The browser's `PublicKeyCredential` serialised to JSON (`RegistrationResponseJSON` / `AuthenticationResponseJSON` from `@simplewebauthn/browser`), relayed unchanged by your backend.","required":["id","rawId","response","type"],"properties":{"id":{"type":"string"},"rawId":{"type":"string"},"type":{"type":"string","const":"public-key"},"response":{"type":"object","additionalProperties":true},"clientExtensionResults":{"type":"object","additionalProperties":true},"authenticatorAttachment":{"type":"string"}},"additionalProperties":true},"WebAuthnRegisterOptionsRequest":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string"},"discoverable":{"type":"boolean","default":false,"description":"Register a resident (discoverable) credential with a stable WebAuthn user handle — needed for passwordless sign-in."},"approvalRequestId":{"type":"string","format":"uuid","description":"Required when the user has a linked phone: the id of a `PASSKEY_ENROLL` request (`POST /login/request`, type `PASSKEY_ENROLL`, reserved and always critical) that the user approved on the phone or with an existing passkey, at most 10 minutes ago. Each approval unlocks one registration."}}},"WebAuthnRegisterVerifyRequest":{"type":"object","required":["externalUsername","response"],"properties":{"externalUsername":{"type":"string"},"response":{"$ref":"#/components/schemas/WebAuthnCredentialResponseJSON"},"label":{"type":"string","description":"Display name for the credential. Everything outside printable ASCII and the characters `<` `>` is removed, then it is trimmed and cut to 64 characters; defaults to a name derived from the authenticator."}}},"WebAuthnRegistered":{"type":"object","required":["verified","credentialId"],"properties":{"verified":{"type":"boolean","const":true},"credentialId":{"type":"string","description":"base64url WebAuthn credential id."}}},"WebAuthnLoginOptionsRequest":{"type":"object","required":["requestId"],"properties":{"requestId":{"type":"string","format":"uuid","description":"A PENDING login request of this workspace."}}},"WebAuthnLoginVerifyRequest":{"type":"object","required":["requestId","response"],"properties":{"requestId":{"type":"string","format":"uuid"},"response":{"$ref":"#/components/schemas/WebAuthnCredentialResponseJSON"}}},"WebAuthnLoginApproved":{"type":"object","required":["success","message","externalUsername","requestId"],"properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"externalUsername":{"type":"string","description":"The user that was approved — open the session for this user, never for a name the browser sent."},"requestId":{"type":"string","format":"uuid"}}},"WebAuthnPrimaryOptions":{"type":"object","required":["attemptId","options"],"properties":{"attemptId":{"type":"string","format":"uuid","description":"Pass back to `/webauthn/authenticate/primary/verify` (valid 5 minutes, single use)."},"options":{"$ref":"#/components/schemas/PublicKeyCredentialRequestOptionsJSON"}}},"WebAuthnPrimaryAvailability":{"type":"object","required":["enabled","userVerification"],"properties":{"enabled":{"type":"boolean","description":"Passkey sign-in (`TenantPolicy.passwordlessLoginEnabled`) is on: show the passkey button and passkey autofill. Off: show neither."},"userVerification":{"type":["string","null"],"enum":["required","preferred",null],"description":"What the options will ask of the authenticator; null when off."}}},"WebAuthnPrimaryOptionsRequest":{"type":"object","properties":{"externalUsername":{"type":"string","description":"The account to sign in. Leave it out for passkey autofill: any passkey of the site."}}},"WebAuthnPrimaryVerifyRequest":{"type":"object","required":["attemptId","response"],"properties":{"attemptId":{"type":"string","format":"uuid"},"response":{"$ref":"#/components/schemas/WebAuthnCredentialResponseJSON"}}},"WebAuthnPrimaryResult":{"description":"Either a completed passwordless sign-in, or a step-up demand: the passkey verified but the risk engine (a CHALLENGE or a number-match request, also when it is unreachable), an access rule, an unreadable access policy or a country that cannot be verified under a country rule requires the regular push / number-matching flow.","oneOf":[{"type":"object","title":"Signed in","required":["success","requestId","externalUsername","riskScore"],"properties":{"success":{"type":"boolean","const":true},"requestId":{"type":"string","format":"uuid","description":"A CONFIRMED login request (`confirmedVia: WEBAUTHN_PRIMARY`) recorded for audit and webhooks."},"externalUsername":{"type":"string"},"riskScore":{"type":"number"},"userVerified":{"type":"boolean","description":"The authenticator verified the user (biometric or PIN) in this assertion. `false` is possession only. Treat a missing field as `false`."},"credentialDeviceType":{"type":"string","enum":["singleDevice","multiDevice"],"description":"`singleDevice`: the key cannot leave the authenticator. `multiDevice`: a synced passkey (iCloud Keychain, Google Password Manager). From this assertion, not from registration. Treat a missing field as `multiDevice`."},"credentialBackedUp":{"type":"boolean","description":"The passkey is currently backed up (synced) by its provider. Only `singleDevice` and not backed up is hardware-bound. Treat a missing field as `true`."}}},{"type":"object","title":"Step-up required","required":["success","requiresStepUp","externalUsername","riskScore"],"properties":{"success":{"type":"boolean","const":false},"requiresStepUp":{"type":"boolean","const":true},"externalUsername":{"type":"string"},"riskScore":{"type":"number"},"userVerified":{"type":"boolean","description":"The authenticator verified the user (biometric or PIN) in this assertion. `false` is possession only. Treat a missing field as `false`."},"credentialDeviceType":{"type":"string","enum":["singleDevice","multiDevice"],"description":"`singleDevice`: the key cannot leave the authenticator. `multiDevice`: a synced passkey (iCloud Keychain, Google Password Manager). From this assertion, not from registration. Treat a missing field as `multiDevice`."},"credentialBackedUp":{"type":"boolean","description":"The passkey is currently backed up (synced) by its provider. Only `singleDevice` and not backed up is hardware-bound. Treat a missing field as `true`."}}}]},"WebAuthnCredentialSummary":{"type":"object","required":["id","credentialId","backedUp","createdAt"],"properties":{"id":{"type":"string","format":"uuid","description":"Record id — use it with `DELETE /webauthn/credentials/{id}`."},"credentialId":{"type":"string","description":"base64url WebAuthn credential id."},"deviceType":{"type":["string","null"],"enum":["singleDevice","multiDevice",null]},"backedUp":{"type":"boolean"},"label":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":["string","null"],"format":"date-time"}}},"TrustedDevice":{"type":"object","required":["id","integrationId","externalUsername","deviceId","trustedAt","expiresAt"],"properties":{"id":{"type":"string","format":"uuid"},"integrationId":{"type":"string"},"externalUsername":{"type":"string"},"deviceId":{"type":"string"},"trustedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"}}},"RevokeTrustRequest":{"type":"object","required":["externalUsername"],"properties":{"externalUsername":{"type":"string"},"deviceId":{"type":"string","description":"Revoke only this device; omit to revoke all of the user's trusted devices."}}},"FlagDeviceRequest":{"type":"object","required":["deviceId","externalUsername","reason"],"properties":{"deviceId":{"type":"string","description":"A device that confirmed at least one login for this user in your workspace."},"externalUsername":{"type":"string"},"reason":{"type":"string","enum":["CONFIRMED_FRAUD","ACCOUNT_TAKEOVER","SOCIAL_ENGINEERING","OTHER"]}}},"LogEntry":{"type":"object","required":["id","type","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","description":"Event type, e.g. `LOGIN_REQUEST`, `LOGIN_CONFIRM`, `LOGIN_REJECT`, `WEBHOOK_FAILED`, `OFFLINE_VERIFY_OK`."},"integrationId":{"type":["string","null"]},"userId":{"type":["string","null"]},"externalUsername":{"type":["string","null"]},"loginId":{"type":["string","null"]},"ip":{"type":["string","null"]},"userAgent":{"type":["string","null"]},"latencyMs":{"type":["integer","null"]},"statusCode":{"type":["integer","null"]},"endpoint":{"type":["string","null"]},"previousDeviceId":{"type":["string","null"]},"newDeviceId":{"type":["string","null"]},"note":{"type":["string","null"]},"metadata":{"type":["object","null"],"additionalProperties":true},"createdAt":{"type":"string","format":"date-time"}}},"Health":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ok","degraded"],"description":"`degraded` (with HTTP 500) when the database is unreachable."},"securityControls":{"type":"string","enum":["ok","degraded"],"description":"Whether any security control ran in its fallback mode in the last 5 minutes."},"degraded":{"type":"array","items":{"type":"string"},"description":"Names of the degraded controls (only when degraded)."}}},"LoginCallbackPayload":{"type":"object","description":"Body of a login result callback. `signature` is the HMAC of every other field (see the webhook description).","required":["status","action","requestId","externalUsername","integrationId","deviceId","referenceId","timestamp","jti","riskScore","riskFactors","signature"],"properties":{"status":{"type":"string","enum":["SUCCESS","REJECTED"]},"action":{"type":"string","enum":["LOGIN_CONFIRM","LOGIN_REJECT"]},"requestId":{"type":"string","format":"uuid"},"externalUsername":{"type":"string"},"integrationId":{"type":"string"},"deviceId":{"type":["string","null"],"description":"Approving phone. For passwordless passkey sign-ins this is the passkey record id."},"referenceId":{"type":["string","null"]},"timestamp":{"type":"string","format":"date-time","description":"Send time (also in `x-timestamp`). Covered by the signature — use it for freshness checks."},"jti":{"type":"string","format":"uuid","description":"Unique per delivery attempt series (also sent as `x-nonce`). A redelivery from the retry queue gets a new `jti`; deduplicate on `requestId` / `Idempotency-Key`."},"riskScore":{"type":"number","minimum":0,"maximum":1},"riskFactors":{"type":"array","items":{"type":"string"},"description":"Risk factors (codes in `riskFactors`):\n- `tor_exit_node`: the end user's address is a Tor exit. Block-capable.\n- `known_malicious_ip` / `suspicious_ip_reputation`: address with a bad reputation. Block-capable.\n- `malicious_tool_detected` / `suspicious_user_agent_or_tool`: the browser is an attack or automation tool. Block-capable.\n- `impossible_travel`: too far from the user's last approved sign-in for the time between (accuracy radii subtracted). Step-up.\n- `datacenter_ip`: a cloud or hosting address. Step-up.\n- `origin_approval_country_mismatch`: the phone that approved is in another country than the browser. Step-up.\n- `ip_velocity_high` / `ip_velocity_critical`: many different people of your workspace from one address within an hour. Block-capable.\n- `mfa_fatigue_suspected` / `user_velocity_*`: many pushes to this user within an hour. Number matching.\n- `device_fingerprint_drift`: a browser this user never approved from. Block-capable.\n- `credential_stuffing_botnet_graph_anomaly`: one browser on many accounts within an hour. Block-capable.\n- `bot_pattern_no_mouse_movement` (step-up) / `bot_pattern_linear_movement` (block-capable): page behaviour of a script.\n- `client_ip_unusable`: the clientIp you sent is internal or reserved. Number matching.\n- `location_unverified`: you sent no usable clientIp (none, a loopback address, or not an address), so the end user's location and network could not be checked. Number matching; a recovery code still answers it. Only once Touchque enforces it.\n- `high_risk_action` / `high_risk_night_action`: the action is critical (at night in the user's time zone).\n- `new_device`, `repeated_failures`, `rapid_requests`, `trust_network_flagged_device`, `require_number_match`: local facts; number matching, never a block.\n- `risk_engine_unavailable`: no verdict could be obtained; the request fails closed (number matching, critical approvals refused).\n- `policy_step_up` / `policy_unavailable` / `geo_unverified` / `post_approval_step_up`: why the request was number-matched when nothing else on it says so: an access rule asked for step-up, the access policy could not be read, the end user's country could not be verified under a country rule (not recorded while the risk engine is unreachable), or an earlier approval was stepped up after the tap. Number matching; a recovery code or a typed offline code cannot approve such a request, nor one with `require_number_match` or `repeated_failures`.\n- `vishing_suspected`: the approving phone was on a call, or its screen was shared or remote-controlled, when the user tapped Approve (Vishing Shield, once enforced): the request was moved to a passkey (`requiresPasskey`), or refused (`reason: vishing_suspected`). No code (offline, time-based, recovery) can finish it.\n- `phishing_origin`: the sign-in page that showed this request is not on your site (Mirror Alarm): a real-time phishing proxy. The approval is refused (`reason: phishing_origin`) and no code can finish it.\n- `user_reported_coached`: the user said someone on the phone told them to approve (on the request, or on an approval taken back with `APPROVAL_REVOKED`). 2FA is frozen as for `user_reported_not_me`.\n- `approval_revoked`: the user took this approval back on the phone (`APPROVAL_REVOKED`).\n- `ctx_missing`: the phone sent its call / screen state with earlier approvals and none with this one.\nShadow-mode factors are not returned until they are enforced: `mfa_fatigue_slow`, `approver_device_push_burst`, `approver_device_reported_fraud`, `user_recently_reported_fraud`, `recent_factor_enrollment`, `new_country_for_user`, `new_asn_for_user`, `location_unverified`."},"assurance":{"$ref":"#/components/schemas/Assurance","description":"SUCCESS only."},"approvalProof":{"$ref":"#/components/schemas/ApprovalProof","description":"SUCCESS only, when the phone is hardware-bound."},"reason":{"type":"string","enum":["user_rejected","user_reported_fraud","user_reported_coached","user_blocked_agent","risk_reversed","risk_step_up","approval_signature_invalid","device_integrity_failed","vishing_suspected","phishing_origin"],"description":"REJECTED only. `user_reported_fraud`: the user said \"this wasn't me\" (2FA frozen). `user_reported_coached`: the user said someone on the phone told them to approve (2FA frozen; a caller is working this user, so reset the password and end their sessions). `user_blocked_agent`: an AI agent's request, and the user also blocked the agent from asking them again (a plain no otherwise; no 2FA freeze). `risk_reversed`: the user approved, then the risk engine refused it on evidence that appeared at the tap (riskFactors lists it). `risk_step_up`: refused for now; the user's next request asks for number matching. `approval_signature_invalid`: the approval carried a device signature that did not verify. `device_integrity_failed`: the phone reports itself jailbroken or tampered and the action is critical. `vishing_suspected`: the phone was on a call (or its screen shared or remote-controlled) when the user approved a critical action, and the user has no passkey to finish it with. `phishing_origin`: the sign-in page was not on your site (Mirror Alarm). Treat unknown values as a rejection."},"frozenUntil":{"type":"string","format":"date-time","description":"REJECTED only: present when this rejection froze the user's 2FA."},"provisional":{"type":"boolean","description":"SUCCESS only. Approved while the user was on a call (Vishing Shield): critical actions wait for `APPROVAL_RECONFIRMED`; `APPROVAL_REVOKED` means end the session."},"reconfirmedAt":{"type":["string","null"],"format":"date-time","description":"SUCCESS only (a redelivery may carry it)."},"revoked":{"type":"boolean","description":"SUCCESS only (a redelivery may carry it): the user took the approval back."},"revokeReason":{"type":["string","null"],"enum":["UNDO","COACHED",null],"description":"SUCCESS only."},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"Same value as the `x-signature` header."}},"additionalProperties":true},"EventWebhookPayload":{"type":"object","description":"Body of a device lifecycle event. `signature` is the HMAC of every other field.","required":["event","externalUsername","integrationId","timestamp","jti","deviceId","signature"],"properties":{"event":{"type":"string","enum":["DEVICE_LINKED","DEVICE_UNLINKED"],"description":"`DEVICE_LINKED`: the user scanned the enrollment QR, or moved the account to a new phone with a transfer code (`via: transfer`). `DEVICE_UNLINKED`: the user disabled 2FA from the phone."},"externalUsername":{"type":"string"},"integrationId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"jti":{"type":"string","format":"uuid"},"deviceId":{"type":"string","description":"The phone the account is now linked to (`DEVICE_LINKED`) or was unlinked from."},"via":{"type":"string","enum":["transfer","recovery","mobile_sdk"],"description":"`DEVICE_LINKED`: present when the account moved from the user's previous phone with a transfer code (`transfer`) or by a recovery (`recovery`, with `recoveryId` and `method`) instead of an enrollment QR, or when your own app enrolled with the Mobile SDK (`mobile_sdk`, with `platform`, `attested` and, on a replace, `replacedDeviceId`). Approvals now come from `deviceId`; if the user did not move phones, treat it as a takeover. Treat unknown values like a link. `DEVICE_UNLINKED` with `mobile_sdk`: you removed the device or the user signed out in your app (`reason`: `removed` | `signed_out`)."},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"additionalProperties":true},"ApprovalEventWebhookPayload":{"type":"object","description":"Body of an approval follow-up event (Vishing Shield). `signature` is the HMAC of every other field.","required":["event","externalUsername","integrationId","timestamp","jti","requestId","signature"],"properties":{"event":{"type":"string","enum":["APPROVAL_RECONFIRMED","APPROVAL_REVOKED"],"description":"`APPROVAL_RECONFIRMED`: after the call, the user confirmed on the phone that a provisional approval was theirs; critical actions are allowed again. `APPROVAL_REVOKED`: the user took an approval back (provisional, or any from the last 15 minutes): end the session or stop the action built on it; the user's 2FA is frozen."},"externalUsername":{"type":"string"},"integrationId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"jti":{"type":"string","format":"uuid"},"requestId":{"type":"string","format":"uuid","description":"The login request the event is about (its SUCCESS webhook came first)."},"reason":{"type":"string","enum":["UNDO","COACHED"],"description":"`APPROVAL_REVOKED` only. `COACHED`: someone on the phone told the user to approve; treat it as an account takeover in progress. Treat unknown values like `COACHED`."},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"additionalProperties":true},"PasskeyEventWebhookPayload":{"type":"object","description":"Body of a passkey lifecycle event. `signature` is the HMAC of every other field. Never carries the public key or the attestation.","required":["event","externalUsername","integrationId","timestamp","jti","id","credentialId","signature"],"properties":{"event":{"type":"string","enum":["PASSKEY_REGISTERED","PASSKEY_DELETED"],"description":"`PASSKEY_REGISTERED`: `POST /webauthn/register/verify` stored a new passkey for the user. `PASSKEY_DELETED`: `DELETE /webauthn/credentials/{id}` removed one. If the user did not add or remove it, treat it as a takeover."},"externalUsername":{"type":"string"},"integrationId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"jti":{"type":"string","format":"uuid"},"id":{"type":"string","format":"uuid","description":"Credential record id, as in `GET /webauthn/credentials` and `DELETE /webauthn/credentials/{id}`."},"credentialId":{"type":["string","null"],"description":"base64url WebAuthn credential id. `null` only on a `PASSKEY_DELETED` for a passkey removed at the same moment by another call."},"deviceType":{"type":["string","null"],"enum":["singleDevice","multiDevice",null],"description":"`PASSKEY_REGISTERED` only. `multiDevice`: a synced passkey (iCloud Keychain, Google Password Manager)."},"backedUp":{"type":"boolean","description":"`PASSKEY_REGISTERED` only: the passkey is backed up (synced) by its provider."},"label":{"type":["string","null"],"description":"`PASSKEY_REGISTERED` only: the display label the passkey was stored with."},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"additionalProperties":true},"PublicJwk":{"description":"An agent's public key: EC P-256 (ES256) or OKP Ed25519 (EdDSA). Never send `d`: the private key stays with the agent. Without `kid`, the RFC 7638 thumbprint becomes the kid.","type":"object","required":["kty","crv","x"],"properties":{"kty":{"type":"string","enum":["EC","OKP"]},"crv":{"type":"string","enum":["P-256","Ed25519"]},"x":{"type":"string"},"y":{"type":"string","description":"EC only."},"kid":{"type":"string","pattern":"^[A-Za-z0-9._:-]{1,64}$"},"alg":{"type":"string","enum":["ES256","EdDSA"]}}},"Agent":{"description":"An AI agent of the workspace. `blocked` says what stops it right now (null when it may act): `workspace_paused` (kill switch), `suspended`, `revoked`, `review_overdue`.","type":"object","properties":{"id":{"type":"string","pattern":"^agt_[a-z2-9]{20}$"},"name":{"type":"string"},"description":{"type":["string","null"]},"platform":{"type":"string","enum":["langchain","llamaindex","openai-agents","claude-agent-sdk","vercel-ai","mcp","custom"]},"owner":{"type":"object","properties":{"email":{"type":"string"},"userId":{"type":["string","null"]}}},"status":{"type":"string","enum":["active","suspended","revoked"]},"statusReason":{"type":["string","null"],"description":"Why it is suspended or revoked; `owner_left` when its owner left the organization."},"blocked":{"type":["string","null"],"enum":[null,"workspace_paused","suspended","revoked","review_overdue"]},"allowedTypes":{"type":"array","items":{"type":"string"},"description":"The action types this agent may ask approvals for."},"policy":{"$ref":"#/components/schemas/AgentPolicy"},"keys":{"type":"array","items":{"type":"object","properties":{"kid":{"type":"string"},"kty":{"type":"string"},"crv":{"type":"string"},"alg":{"type":"string"},"addedAt":{"type":"string","format":"date-time"}}}},"reviewAt":{"type":"string","format":"date-time","description":"After this the agent stops until renewed (`PATCH` with `reviewInDays`)."},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"CreateAgent":{"type":"object","required":["name","allowedTypes"],"properties":{"name":{"type":"string","maxLength":80},"description":{"type":"string","maxLength":500,"description":"What the agent is for."},"ownerEmail":{"type":"string","format":"email","description":"The accountable person: a member of the workspace's organization. Default: the creator of the API key. When the owner leaves, the agent is suspended (`owner_left`)."},"platform":{"type":"string","enum":["langchain","llamaindex","openai-agents","claude-agent-sdk","vercel-ai","mcp","custom"]},"allowedTypes":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string"},"description":"Active action types of the workspace."},"publicKey":{"$ref":"#/components/schemas/PublicJwk"},"publicKeys":{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"#/components/schemas/PublicJwk"}},"reviewInDays":{"type":"integer","minimum":1,"maximum":365,"description":"Default 90."}},"description":"Send `publicKey` or `publicKeys`."},"UpdateAgent":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"description":{"type":["string","null"],"maxLength":500},"ownerEmail":{"type":"string","format":"email","description":"A new owner also resumes an agent suspended because its owner left."},"platform":{"type":"string"},"allowedTypes":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string"}},"reviewInDays":{"type":"integer","minimum":1,"maximum":365,"description":"Renews the review date from now."}}},"AgentPolicy":{"description":"What the agent may ask for beyond its action types, checked before anything reaches the phone. PUT replaces it: fields left out fall back to the defaults.","type":"object","properties":{"requestsPer10Min":{"type":"integer","minimum":1,"maximum":1000,"description":"Every request of the agent (default 60)."},"requestsPerUserPer10Min":{"type":"integer","minimum":1,"maximum":30,"description":"Requests to one person (default 5)."},"amountPerRequest":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"TRY\": \"1000.00\" }: the most one approval may move."},"amountPerDay":{"type":"object","additionalProperties":{"type":"string"},"description":"Over 24 hours, pending and approved requests together."},"background":{"type":"string","enum":["non_critical","never"],"description":"Critical action types are never asked in the background."},"newRecipients":{"type":"string","enum":["ask","deny"],"description":"deny: an account, merchant, recipient or destination this person never approved for this agent is refused."},"recipientAllowlist":{"type":["array","null"],"items":{"type":"string"},"description":"IBANs or account numbers; any other is refused."},"fourEyes":{"type":["object","null"],"description":"After the person approves, a second person approves too (never the same person, never the agent's owner) when the action type is listed or an amount is over `above`. A request that needs four eyes cannot use a mandate.","properties":{"approvers":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string"}},"above":{"type":"object","additionalProperties":{"type":"string"}},"types":{"type":"array","items":{"type":"string"}}}}}},"Mandate":{"description":"A bounded standing permission the person approved once on the phone. Requests within it (POST /agents/approvals with mandateId) are approved without a push.","type":"object","properties":{"mandateId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","active","rejected","expired","provisional","revoked","exhausted"]},"externalUsername":{"type":"string"},"type":{"type":"string"},"maxCount":{"type":"integer"},"usedCount":{"type":"integer"},"maxTotal":{"type":"object","properties":{"currency":{"type":"string"},"amount":{"type":"string"}}},"used":{"type":"object","properties":{"currency":{"type":"string"},"amount":{"type":"string"}}},"perRequest":{"type":"object","properties":{"currency":{"type":"string"},"amount":{"type":"string"}}},"recipients":{"type":"array","items":{"type":"string"},"description":"iban:… / account:… identifiers; empty = any."},"validUntil":{"type":"string","format":"date-time"},"challengeCode":{"type":"string","description":"While pending: show it in the conversation."},"approvalExpiresAt":{"type":"string","format":"date-time"},"revokedAt":{"type":["string","null"],"format":"date-time"}}},"MandateRequest":{"type":"object","required":["externalUsername","type","maxCount","maxTotal"],"properties":{"externalUsername":{"type":"string"},"type":{"type":"string"},"maxCount":{"type":"integer","minimum":1,"maximum":100},"maxTotal":{"type":"object","required":["currency","amount"],"properties":{"currency":{"type":"string"},"amount":{"type":"string"}}},"perRequest":{"type":"string","description":"The most one request may move, in the same currency."},"recipients":{"type":"array","maxItems":20,"items":{"type":"string"},"description":"IBANs or account numbers it may pay; none = any (still subject to the agent's policy)."},"validFor":{"type":"integer","minimum":300,"maximum":604800,"description":"Seconds (default 86400)."},"reason":{"type":"string","maxLength":200},"presence":{"type":"string","enum":["interactive"],"description":"A mandate is always approved with the person in the conversation."},"expiresIn":{"type":"integer","minimum":60,"maximum":3600,"description":"Seconds the phone may approve it (default 300)."},"locale":{"type":"string","enum":["tr","en"]}}},"AgentVaultConnection":{"type":"object","properties":{"name":{"type":"string"},"provider":{"type":"string"},"clientId":{"type":"string"},"hasClientSecret":{"type":"boolean"},"options":{"type":"object","description":"{ tenant } for Microsoft, { issuer } for OpenID Connect."},"agentIds":{"type":"array","items":{"type":"string"}},"connected":{"type":"integer","description":"People whose tokens are kept (in the list)."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AgentVaultAccount":{"type":"object","properties":{"connection":{"type":"string"},"externalUsername":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"canRefresh":{"type":"boolean"},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"lastAgentId":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AgentKillSwitch":{"type":"object","properties":{"engaged":{"type":"boolean"},"since":{"type":"string","format":"date-time"},"by":{"type":"string"},"reason":{"type":["string","null"]}}},"AgentToken":{"type":"object","properties":{"access_token":{"type":"string","description":"`tqat_…`, opaque."},"token_type":{"type":"string","const":"DPoP"},"expires_in":{"type":"integer","const":600},"scope":{"type":"string","description":"Space separated: `agent:read`, `approvals:request`, `approvals:read`, `vault:read`."}}},"AuthorizationDetail":{"description":"One thing the agent asks the person to approve (RFC 9396 style). Touchque checks it and draws the phone's rows from it in its own words; text with invisible, control or direction-changing characters is refused. Types: `payment_initiation` { instructedAmount { currency, amount }, creditorName, creditorAccount { iban | accountNumber }, remittanceInformationUnstructured }, `purchase` { amount, merchant, item }, `data_access` { resource, actions[read|write|delete|export|share], records, destination }, `account_change` { setting, from, to }, `message_send` { channel, recipients[], subject }, `generic` { fields[{ label, value }] }. IBANs must pass their check digits.","type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["payment_initiation","purchase","data_access","account_change","message_send","generic"]}},"additionalProperties":true},"AgentApprovalRequest":{"type":"object","required":["externalUsername","type","authorizationDetails"],"properties":{"externalUsername":{"type":"string","description":"The person the agent acts for (their phone approves)."},"type":{"type":"string","description":"One of the agent's allowed action types."},"authorizationDetails":{"type":"array","minItems":1,"maxItems":5,"items":{"$ref":"#/components/schemas/AuthorizationDetail"}},"reason":{"type":"string","maxLength":200,"description":"The agent's own words, shown on the phone as unverified."},"presence":{"type":"string","enum":["interactive","background"],"description":"interactive (default): a person is in the agent's conversation and types the number shown there on the phone. background: no number; never for a critical action."},"expiresIn":{"type":"integer","minimum":60,"maximum":3600,"description":"Seconds the phone may approve it (default 300)."},"locale":{"type":"string","enum":["tr","en"],"description":"Language of the rows Touchque draws (default tr)."},"referenceId":{"type":"string","maxLength":128},"clientIp":{"type":"string","description":"The person's own address, when the agent's app knows it."},"userAgent":{"type":"string"},"mandateId":{"type":"string","format":"uuid","description":"Approve within this active mandate (no push): same person and type, payments/purchases in its currency, within its count, total, per-request amount and recipients."}}},"AgentApprovalStatus":{"type":"object","properties":{"approvalId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","awaiting_second","approved","rejected","blocked","expired","provisional","revoked","stale","redeemed"],"description":"awaiting_second: four eyes, the second person has not answered yet (secondChallengeCode, when their type needs a number). provisional: approved during a phone call (Vishing Shield), no token until the phone confirms it after the call. stale: approved more than 15 minutes ago; ask again. reason second_rejected: the second person said no. blocked with reason inhuman_approval: the number came back sooner than a person can read and type it (something drives the phone); no token."},"secondApprover":{"type":"string"},"secondChallengeCode":{"type":"string"},"mandateId":{"type":"string","format":"uuid"},"presence":{"type":"string"},"challengeCode":{"type":"string","description":"While pending and interactive: show it in the conversation."},"expiresAt":{"type":"string","format":"date-time"},"caseCode":{"type":"string"},"reason":{"type":"string"},"approvalToken":{"type":"string","description":"When approved: a JWS (ES256, typ touchque-approval+jwt, keys at GET /agents/jwks.json) with iss, aud touchque:workspace:<id>, sub (the person), act.sub (the agent), authorization_details, txn, jti, exp (5 minutes), cnf.jkt (the agent's DPoP key), amr, number_matched, details (the rows the phone showed) and proof (the phone's hardware signature over them)."},"tokenExpiresIn":{"type":"integer"}}},"RedeemedApproval":{"type":"object","properties":{"approval":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalUsername":{"type":"string"},"type":{"type":"string"},"agent":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]}}},"authorizationDetails":{"type":"array","items":{"$ref":"#/components/schemas/AuthorizationDetail"}},"details":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"}}}},"presence":{"type":"string"},"numberMatched":{"type":"boolean"},"decidedAt":{"type":["string","null"],"format":"date-time"},"approvalProof":{"type":["object","null"]},"caseCode":{"type":"string"}}}}},"OAuthError":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_request","invalid_client","invalid_dpop_proof","unauthorized_client","unsupported_grant_type","invalid_scope","slow_down","server_error"]},"error_description":{"type":"string"},"reason":{"type":"string","description":"With `unauthorized_client`: what stops the agent."}}},"MobileEnrollment":{"type":"object","required":["enrollmentId","enrollmentToken","externalUsername","expiresAt","replace"],"properties":{"enrollmentId":{"type":"string"},"enrollmentToken":{"type":"string","pattern":"^tqe_[A-Za-z0-9_-]{43}$","description":"One-time. Hand it to your app over your own signed-in API (never in a URL or QR code); the SDK spends it in `Touchque.enroll(token)`. Only the newest token of a user works."},"externalUsername":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"replace":{"type":"boolean"},"replacesDeviceId":{"type":"string","description":"With `replace`: the device that approves today and stops when the token is used."}}},"MobileDevice":{"description":"A device inside your app that approves for one of your users.","type":"object","properties":{"deviceId":{"type":"string"},"externalUsername":{"type":"string"},"platform":{"type":"string","enum":["ios","android"]},"appId":{"type":["string","null"],"description":"Bundle id / package name the app reported (proven when `attestation` is set)."},"sdkVersion":{"type":["string","null"]},"attestation":{"type":["string","null"],"enum":["apple-appattest","android-key",null],"description":"How the app and its key were proven genuine at enrollment; null when not attested."},"status":{"type":"string","enum":["active","revoked"]},"revokedReason":{"type":["string","null"],"enum":["replaced","removed","signed_out",null]},"revokedAt":{"type":["string","null"],"format":"date-time"},"enrolledAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"linked":{"type":"boolean","description":"GET by id only: whether it is the user's approver right now."},"hardwareKey":{"type":["object","null"],"properties":{"registered":{"type":"boolean","description":"Approvals are signed by a Secure Enclave / Android Keystore key."},"attestation":{"type":["string","null"]},"lostAt":{"type":["string","null"],"format":"date-time","description":"The phone reported its keys destroyed (screen lock removed): it can no longer approve."}}},"pushToken":{"type":"boolean","description":"Whether the app handed its push token to Touchque (then `MOBILE_PUSH` carries it)."},"posture":{"type":["object","null"],"description":"The phone's latest posture report (OS, app version, screen lock, biometrics, jailbreak/tamper), when it came and whether the hardware key signed it."}}},"MobileSdkSettings":{"type":"object","properties":{"iosAppIds":{"type":"array","maxItems":10,"items":{"type":"string","pattern":"^[A-Z0-9]{10}\\.[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*$"},"description":"Your iOS apps as `TEAMID.bundle.id`. When set, only these apps may enrol, and App Attest is checked against them."},"androidPackages":{"type":"array","maxItems":10,"items":{"type":"string"},"description":"Your Android package names. When set, only these apps may enrol, and Key Attestation is checked against them."},"androidCerts":{"type":"object","additionalProperties":{"type":"array","maxItems":5,"items":{"type":"string","pattern":"^([0-9A-F]{2}:){31}[0-9A-F]{2}$"}},"description":"SHA-256 of each Android app's signing certificate, by package name (the Play app signing key; your upload key while you test). When an app has some, its Key Attestation must name one: a repackaged copy keeps the package name but not your key."},"attestation":{"type":"string","enum":["off","optional","required"],"description":"`required`: an app that cannot prove itself (App Attest / Android Key Attestation) cannot enrol. Default `optional`: evidence is checked when sent and recorded."},"enrollTtlSeconds":{"type":"integer","minimum":60,"maximum":86400,"description":"Default lifetime of an enrollment token. Default 600."},"push":{"type":"object","description":"Who sends the push per platform: `touchque`, with the APNs key / FCM service account you gave (`apns`, `fcm` in PUT), or `webhook` (the `MOBILE_PUSH` webhook to you). The keys are never returned.","properties":{"ios":{"type":"string","enum":["touchque","webhook"]},"android":{"type":"string","enum":["touchque","webhook"]},"apns":{"type":["object","null"],"properties":{"keyId":{"type":"string"},"teamId":{"type":"string"},"topic":{"type":"string"},"production":{"type":"boolean"}}},"fcm":{"type":["object","null"],"properties":{"projectId":{"type":"string"},"clientEmail":{"type":"string"}}}}},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"MobilePushWebhookPayload":{"type":"object","description":"A request for a device inside your app, on a platform whose push key Touchque does not have (see `push` in the Mobile SDK settings). Send `data` with your own APNs/FCM to `pushToken` (or to the user's devices by your own mapping when it is null) and pass the received data to `Touchque.handlePush`. `data` carries no secret, no details and no matching number: the SDK reads the request over its encrypted connection. `signature` is the HMAC of every other field.","required":["event","externalUsername","integrationId","timestamp","jti","deviceId","platform","kind","data","signature"],"properties":{"event":{"type":"string","const":"MOBILE_PUSH"},"externalUsername":{"type":"string"},"integrationId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"jti":{"type":"string","format":"uuid"},"deviceId":{"type":"string"},"platform":{"type":"string","enum":["ios","android"]},"appId":{"type":["string","null"]},"pushToken":{"type":["string","null"],"description":"The APNs / FCM token your app handed to the SDK (`setPushToken`), or null."},"kind":{"type":"string","enum":["approval","notice","test"],"description":"`test`: a test push sent from the dashboard or POST /mobile/devices/{deviceId}/test-push."},"requestId":{"type":["string","null"]},"expiresAt":{"type":["string","null"],"format":"date-time","description":"Drop the push after this."},"alert":{"type":["object","null"],"properties":{"title":{"type":"string"},"body":{"type":"string"}},"description":"Suggested notification text (English); translate or replace it."},"data":{"type":"object","required":["touchque"],"properties":{"touchque":{"type":"string","description":"JSON: `{\"v\":1,\"k\":\"approval\",\"did\":\"<deviceId>\",\"rid\":\"<requestId>\",\"exp\":\"<ISO time>\"}`. Put it in the APNs payload as the custom key `touchque` / in the FCM `data` map unchanged."}}},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"additionalProperties":true},"AssistOpenRequest":{"type":"object","required":["externalUsername","agent"],"properties":{"externalUsername":{"type":"string","maxLength":255,"description":"The person IT is helping."},"agent":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":80,"description":"Shown on the person's phone: sign the agent in first and pass their verified name."},"title":{"type":"string","maxLength":80,"description":"E.g. `IT help desk`."},"ref":{"type":"string","maxLength":255,"description":"Your id for the agent; a new session of the same agent for the same person replaces the old one. Default `name`."}}},"reason":{"type":"string","maxLength":200,"description":"A ticket or a few words, for the log."},"ttlMinutes":{"type":"integer","minimum":5,"maximum":120,"description":"Default 30."},"notify":{"type":"boolean","description":"Send the person's phone the notice \"<agent> from IT is working on your account now\" (nothing to approve). Default true."}}},"AssistCheck":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"action":{"type":"string"},"actionText":{"type":"string"},"outcome":{"type":"string","enum":["pending","verified","wrong_number","not_me","coached","declined","expired"],"description":"`verified`: the person typed the number and approved with Face ID. `wrong_number`, `not_me` (\"this isn't me\"), `coached` (\"someone told me to\"), `declined`: blocked."},"createdAt":{"type":"string","format":"date-time"},"spentAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"}}},"AssistSession":{"description":"IT working on a person's account. `OPEN` until ended, expired or replaced; while it is open, the person's phone answers \"someone says they're from IT\" with this agent's name, role and since when.","type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalUsername":{"type":"string"},"status":{"type":"string","enum":["OPEN","ENDED"]},"agent":{"type":"object","properties":{"ref":{"type":"string"},"name":{"type":"string"},"title":{"type":["string","null"]}}},"reason":{"type":["string","null"]},"notified":{"type":"boolean"},"openedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"endedAt":{"type":["string","null"],"format":"date-time"},"endedReason":{"type":["string","null"],"description":"E.g. `ended`, `expired`, `replaced`."},"phone":{"type":"boolean","description":"The person has a working Touchque phone to check on. `false`: they move to a new phone with Never Locked Out."},"frozen":{"type":"boolean","description":"Their approvals are frozen (see `POST /assist/{id}/unfreeze`)."},"checks":{"type":"array","items":{"$ref":"#/components/schemas/AssistCheck"}}}},"AssistSessionList":{"type":"object","properties":{"sessions":{"type":"array","items":{"$ref":"#/components/schemas/AssistSession"}}}},"AssistVerifyRequest":{"type":"object","required":["action"],"properties":{"action":{"type":"string","maxLength":40,"pattern":"^[a-z][a-z0-9_]*$","description":"What the help desk is about to do, in your words: `reset_2fa`, `reset_password`, `activation_link`, `recovery_link`, `unlock`, `new_phone`, `remove_passkey`, ..."},"actionText":{"type":"string","maxLength":80,"description":"Shown on the phone: \"IT (<agent>) is about to <actionText>\". Default `action` with spaces."},"details":{"type":"array","items":{"type":"object"},"description":"Your own lines for the phone instead of the default (who, about to, why, is that you?)."}}},"AssistVerifyResponse":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"},"challengeCode":{"type":"string","pattern":"^[0-9]{2}$","description":"Show it to the agent only: they read it to the person on the call, who types it on the phone."},"expiresAt":{"type":"string","format":"date-time"},"action":{"type":"string"}}},"AssistSpendRequest":{"type":"object","required":["requestId","action"],"properties":{"requestId":{"type":"string","format":"uuid"},"action":{"type":"string","maxLength":40,"pattern":"^[a-z][a-z0-9_]*$","description":"What the help desk is about to do, in your words: `reset_2fa`, `reset_password`, `activation_link`, `recovery_link`, `unlock`, `new_phone`, `remove_passkey`, ..."}}},"AssistEndRequest":{"type":"object","properties":{"by":{"type":"string","maxLength":255},"reason":{"type":"string","pattern":"^[a-z_]{1,40}$"}}},"AssistUnfreezeResponse":{"type":"object","properties":{"unfrozen":{"type":"boolean"},"session":{"$ref":"#/components/schemas/AssistSession"}}},"AssistEventWebhookPayload":{"type":"object","description":"Body of an assist event. `signature` is the HMAC of every other field. `ASSIST_CLAIM_UNVERIFIED` has no `assistId`: the person asked their phone whether someone from IT is working on their account, and nobody was — someone is pretending to be IT. Page someone on it and on `ASSIST_CHECK_FAILED`.","required":["event","integrationId","timestamp","jti","signature"],"properties":{"event":{"type":"string","enum":["ASSIST_OPENED","ASSIST_ENDED","ASSIST_CHECK_PASSED","ASSIST_CHECK_FAILED","ASSIST_UNFROZEN","ASSIST_CLAIM_CONFIRMED","ASSIST_CLAIM_UNVERIFIED"]},"externalUsername":{"type":["string","null"]},"integrationId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"jti":{"type":"string","format":"uuid"},"assistId":{"type":"string","format":"uuid"},"agentRef":{"type":"string"},"agentName":{"type":"string"},"requestId":{"type":"string","format":"uuid"},"action":{"type":["string","null"]},"outcome":{"type":"string","enum":["pending","verified","wrong_number","not_me","coached","declined","expired"]},"reason":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"caseCode":{"type":["string","null"],"description":"As in `/logs/cases/{code}`."},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"additionalProperties":true},"DeviceRiskReport":{"type":"string","maxLength":8192,"description":"The encoded report from `@touchque/web` `deviceRisk.collect({ purpose })`, passed untouched (the relay alternative to `deviceRisk.identify`). Collect it only on sign-in, sign-up, password-reset and approval pages, and name it in your privacy notice. Touchque keeps no raw trait: keyed hashes with a key of your workspace alone (no device is linked across Touchque customers) and coarse labels, for the retention your workspace sets (30 to 90 days)."},"DeviceRiskSignal":{"type":"object","required":["code","text","strength"],"properties":{"code":{"type":"string","enum":["device_tampered","device_virtual","device_geo_mismatch","device_many_accounts","device_signup_burst","device_id_resets","device_many_countries","device_wrong_site","device_report_replayed","device_private_window","device_step_up_by_admin"]},"text":{"type":"string"},"strength":{"type":"string","enum":["number_match","passkey","none"],"description":"What a step-up for it should be. `none`: context only, it never asks on its own."},"evidence":{"type":"object","description":"Counts and finding names behind it (never a raw trait)."}}},"DeviceRiskIdentification":{"type":"object","description":"One identification in a search (GET /device-risk/events): no address and no raw trait.","properties":{"requestId":{"type":"string"},"at":{"type":"string","format":"date-time"},"visitorId":{"type":"string"},"kind":{"type":"string","enum":["sign_in","sign_up","password_reset","approval"]},"origin":{"type":["string","null"],"description":"The page's origin."},"externalUsername":{"type":["string","null"]},"country":{"type":["string","null"]},"network":{"type":["object","null"],"description":"The address as the risk engine saw it: place (no coordinates), operator, kind of network."},"confidence":{"type":["number","null"]},"matchedBy":{"type":["string","null"],"enum":["key","sid","traits","new",null]},"sealed":{"type":"boolean"},"stepUp":{"type":"boolean"},"signals":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"enforced":{"type":"boolean"}}}},"caseCode":{"type":["string","null"]},"bot":{"type":"object"}}},"DeviceRiskVerdictSignal":{"allOf":[{"$ref":"#/components/schemas/DeviceRiskSignal"},{"type":"object","properties":{"enforced":{"type":"boolean","description":"Scored now. false: reported only (shadow) until Touchque enforces it."}}}]},"DeviceRiskNetwork":{"type":["object","null"],"description":"The address as the risk engine saw it for this event: the place (never coordinates), the operator and the kind of network. null when no address was given.","properties":{"countryCode":{"type":["string","null"]},"region":{"type":["string","null"]},"city":{"type":["string","null"]},"accuracyKm":{"type":["number","null"],"description":"How far off the place can be."},"timeZone":{"type":["string","null"]},"asn":{"type":["integer","null"]},"asnOrg":{"type":["string","null"],"description":"The network operator."},"type":{"type":"string","enum":["isp","hosting","tor","unknown"],"description":"`hosting`: a cloud or hosting network (scripts, proxies and many VPNs come from one)."},"tor":{"type":"boolean"},"hosting":{"type":"boolean"},"hostingProvider":{"type":["string","null"]},"timezoneMismatch":{"type":["boolean","null"],"description":"The browser's clock is 2 h or more off the address's time zone: a VPN or proxy in another country."}}},"DeviceRiskVelocity":{"type":["object","null"],"description":"This visitor's counts: accounts, sign-ups, countries and stored-id resets (the risk engine's), identifications (Touchque's). null for a count not known.","properties":{"accounts24h":{"type":["integer","null"]},"accounts30d":{"type":["integer","null"]},"signUps24h":{"type":["integer","null"]},"countries24h":{"type":["integer","null"]},"idResets7d":{"type":["integer","null"]},"identifications1h":{"type":["integer","null"]},"identifications24h":{"type":["integer","null"]}}},"DeviceRiskBot":{"type":["object","null"],"description":"One answer to \"a bot?\", summing up the signals (the risk engine still decides): `detected` the browser says it is driven (webdriver, a headless browser), `suspected` signs that point that way (the pointer moved like software, a zero-size window, software graphics).","properties":{"result":{"type":"string","enum":["detected","suspected","not_detected"]},"reasons":{"type":"array","items":{"type":"string","enum":["webdriver","headless","pointer_synthetic","zero_window","soft_gpu"]}}}},"DeviceRiskEvent":{"type":"object","description":"An identification as Touchque saw it: trust this, never the copy on the page.","properties":{"requestId":{"type":"string"},"visitorId":{"type":"string"},"confidence":{"type":"number"},"matchedBy":{"type":"string","enum":["key","sid","traits","new"]},"sealed":{"type":"boolean"},"purpose":{"type":"string","enum":["sign_in","sign_up","password_reset","approval"]},"origin":{"type":["string","null"],"description":"The page origin the report was made on."},"country":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"usedAt":{"type":["string","null"],"format":"date-time"},"externalUsername":{"type":["string","null"],"description":"The account it was used for (one per identification)."},"visitor":{"type":"object","description":"firstSeenAt, lastSeenAt, seenCount, status (your admin's mark), coarse labels."},"facts":{"type":"object","description":"What the page showed: codes, evidence (finding names), reset, twins, privateWindow."},"verdict":{"type":["object","null"],"description":"The risk engine's answer: stepUp, requires, mode, signals (each enforced or not), caseCode."},"network":{"$ref":"#/components/schemas/DeviceRiskNetwork"},"velocity":{"$ref":"#/components/schemas/DeviceRiskVelocity"},"bot":{"$ref":"#/components/schemas/DeviceRiskBot"}}},"DeviceRiskCheckResult":{"type":"object","required":["report","signals","stepUp","wouldStepUp","requires","mode"],"properties":{"report":{"type":"string","enum":["ok","replayed","missing","unknown","expired","invalid"],"description":"`replayed`: this identification was used for another account, or the report was sent twice (`device_report_replayed`). `missing` (no requestId or an empty one), `unknown` (made up, or another workspace's), `expired` (older than 15 minutes) and `invalid` (a relayed report that cannot be read): the tag was removed or blocked, `device_identification_missing` is raised; ad blockers do it to honest people too, so it only ever asks for a number-matched step-up."},"requestId":{"type":["string","null"]},"visitorId":{"type":["string","null"]},"confidence":{"type":["number","null"]},"visitor":{"type":["object","null"]},"counts":{"type":["object","null"],"description":"The engine's counts for this visitor: accounts_24h, accounts_30d, signups_24h, countries_24h, resets_7d."},"network":{"$ref":"#/components/schemas/DeviceRiskNetwork"},"velocity":{"$ref":"#/components/schemas/DeviceRiskVelocity"},"bot":{"$ref":"#/components/schemas/DeviceRiskBot"},"signals":{"type":"array","items":{"$ref":"#/components/schemas/DeviceRiskVerdictSignal"}},"stepUp":{"type":"boolean","description":"Ask for more on this page (an emailed code, a passkey, a number-matched approval). Never a reason to refuse. True for your admin's \"step up always\" mark, for signals Touchque enforces, and when the risk engine cannot be asked (`engine: unavailable`)."},"wouldStepUp":{"type":"boolean"},"requires":{"type":"string","enum":["none","number_match","passkey"],"description":"What a step-up asks for (`none` without one). `passkey` only while Touchque's risk engine enforces phishing-resistant assurance; until then the same step-up asks for `number_match`."},"mode":{"type":"string","enum":["shadow","enforce"],"description":"`enforce` when at least one raised signal is enforced."},"caseCode":{"type":"string","description":"\"Why?\": the decision record, opened by this code in the dashboard."}}},"DeviceRiskSettings":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Off by default."},"retentionDays":{"type":"integer","minimum":30,"maximum":90},"retentionRange":{"type":"object","properties":{"min":{"type":"integer"},"max":{"type":"integer"}}},"siteKey":{"type":["string","null"],"description":"Public: the web SDK's deviceRisk.identify() sends it. Issued when Device risk is first turned on."},"origins":{"type":"array","items":{"type":"string"},"description":"Page origins that may identify, besides pages on your organization's verified domains and their subdomains (Organization → Domains)."},"pages":{"type":"array","items":{"type":"string"}},"signals":{"type":"array","items":{"$ref":"#/components/schemas/DeviceRiskSignal"}},"alerts":{"$ref":"#/components/schemas/DeviceRiskAlerts"},"alertRules":{"type":"array","items":{"type":"object"},"description":"Each rule: id, label, unit, min, max and the default threshold."},"alertDelivery":{"type":"object","properties":{"webhook":{"type":"boolean","description":"The workspace has a webhook URL (Developers → Webhooks)."},"email":{"type":"string","enum":["available","not_configured","no_organization"]}}},"verifiedDomains":{"type":["array","null"],"items":{"type":"string"},"description":"Your organization's verified domains (Organization → Domains); null when they cannot be read."},"offDomainOrigins":{"type":"array","items":{"type":"string"},"description":"Origins on none of the verified domains: allowed, only pointed out."},"firstParty":{"type":["object","null"],"description":"The workspace's verified Touchque Login custom domain, where the tag (`{scriptBase}/vX.Y.Z/device.js`, the same bytes and integrity as the CDN's) and its identify call (`data-api=\"{apiBase}\"`) are served first-party; null without one.","properties":{"host":{"type":"string"},"scriptBase":{"type":"string"},"apiBase":{"type":"string"}}},"ratePerMinute":{"type":"integer","minimum":60,"description":"Requests per minute: identifications per site key, and `POST /device-risk/check` calls per workspace (1200 unless Touchque raised it for you; ask support). Above it the call answers 429 with `Retry-After`. Read-only."},"proxySecret":{"type":"string","pattern":"^tqps_[A-Za-z0-9_-]{43}$","description":"Owners, admins and API keys only. Your own first-party proxy (/_tq/ on your domain) sends it as `X-Touchque-Proxy-Secret` on the identify call, with the visitor's address as `X-Touchque-Client-IP`. Keep it on the server like the API key; a new site key makes a new one."}}},"DeviceRiskEventWebhookPayload":{"type":"object","description":"Body of a Device risk event. `signature` is the HMAC of every other field.","required":["event","integrationId","timestamp","jti","signature"],"properties":{"event":{"type":"string","enum":["DEVICE_RISK_SIGNAL","DEVICE_RISK_ALERT"]},"externalUsername":{"type":["string","null"]},"integrationId":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"jti":{"type":"string"},"requestId":{"type":["string","null"],"description":"DEVICE_RISK_SIGNAL: the identification (`dre_…`)."},"visitorId":{"type":["string","null"],"description":"DEVICE_RISK_SIGNAL: the visitor (`tqv_…`); null when the page sent no identification."},"purpose":{"type":["string","null"],"enum":["sign_in","sign_up","password_reset","approval",null]},"report":{"type":"string","enum":["ok","replayed","missing","unknown","expired","invalid"]},"confidence":{"type":["number","null"]},"matchedBy":{"type":["string","null"]},"sealed":{"type":"boolean"},"country":{"type":["string","null"]},"signals":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"enforced":{"type":"boolean"},"strength":{"type":"string","enum":["number_match","passkey","none"]}}}},"verdict":{"type":"object","properties":{"stepUp":{"type":"boolean"},"requires":{"type":"string"},"mode":{"type":"string"},"caseCode":{"type":["string","null"]}}},"alert":{"type":"object","description":"DEVICE_RISK_ALERT: rule (signup_burst | many_accounts | tamper_spike), label, value, threshold, window, visitorId, requestId, purpose, at."},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$"}}},"DeviceRiskAlerts":{"type":"object","description":"Alerts: each rule alerts at most once an hour, to the webhook (DEVICE_RISK_ALERT) and/or by email to the owners and admins of the workspace's organization.","properties":{"rules":{"type":"object","properties":{"signup_burst":{"type":"object","properties":{"on":{"type":"boolean"},"threshold":{"type":"integer","minimum":2}}},"many_accounts":{"type":"object","properties":{"on":{"type":"boolean"},"threshold":{"type":"integer","minimum":2}}},"tamper_spike":{"type":"object","properties":{"on":{"type":"boolean"},"threshold":{"type":"integer","minimum":2}}}},"description":"`signup_burst`: sign-ups from one visitor in 24 h (default 5). `many_accounts`: accounts on one visitor in 24 h (default 5). `tamper_spike`: identifications with a tampered or virtual browser in the workspace in the last hour (default 20)."},"email":{"type":"boolean"},"webhook":{"type":"boolean"}}},"DeviceRiskVisitor":{"type":"object","properties":{"visitorId":{"type":"string"},"labels":{"type":"object"},"status":{"type":"string","enum":["none","trusted","step_up"]},"statusAt":{"type":["string","null"],"format":"date-time"},"signals":{"type":"array","items":{"type":"string"},"description":"The codes raised at its latest identification."},"flaggedAt":{"type":["string","null"],"format":"date-time"},"seenCount":{"type":"integer"},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"accounts30d":{"type":"integer"},"accounts":{"type":"array","description":"GET by id only.","items":{"type":"object"}},"events":{"type":"array","description":"GET by id only: the latest 50 identifications with their verdicts.","items":{"type":"object"}}}},"SafeTx":{"type":"object","required":["chainId","safe","guard","tx","config","approverIndex"],"description":"A Safe transaction as the approver's phone will decode it. The phone gets these raw fields only, decodes them itself, reads the Safe and the guard from the chain from two built-in nodes, and signs only its own view.","properties":{"chainId":{"oneOf":[{"type":"integer","minimum":1},{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"}]},"safe":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"guard":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","description":"The TouchQueSafeGuard contract."},"tx":{"type":"object","required":["to","data","nonce"],"properties":{"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"value":{"oneOf":[{"type":"integer","minimum":0},{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"}]},"data":{"type":"string","pattern":"^0x([0-9a-fA-F]{2})*$"},"operation":{"type":"integer","enum":[0,1]},"safeTxGas":{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"},"baseGas":{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"},"gasPrice":{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"},"gasToken":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"refundReceiver":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"nonce":{"oneOf":[{"type":"integer","minimum":0},{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"}]}}},"config":{"type":"object","required":["threshold","govThreshold","govDelay","exitDelay"],"description":"The guard's settings of this Safe as read from chain (the phone checks them on chain).","properties":{"threshold":{"type":"integer","minimum":0,"maximum":255},"govThreshold":{"type":"integer","minimum":0,"maximum":255},"govDelay":{"type":"integer","minimum":0},"exitDelay":{"type":"integer","minimum":0},"recipientLock":{"type":"boolean"}}},"recipients":{"type":"array","maxItems":500,"items":{"type":"object","required":["address"],"properties":{"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"label":{"type":"string","description":"At most 64 bytes, no control, bidi or invisible characters."}}}},"batchTargets":{"type":"array","maxItems":5,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"}},"approverIndex":{"type":"integer","minimum":0,"maximum":31,"description":"This approver's place in the guard."},"locale":{"type":"string","enum":["tr","en"],"default":"tr"}}},"SafeGuardApprovalResult":{"type":"object","description":"What the guard gets once approved.","properties":{"class":{"type":"string","enum":["tighten","ordinary","governance","exit"]},"flags":{"type":"array","items":{"type":"string"}},"safeTxHash":{"type":"string"},"governanceHash":{"type":["string","null"]},"domainHash":{"type":"string","description":"EIP-712 domain hash: what a hardware wallet shows as \"Domain hash\" when it blind-signs."},"messageHash":{"type":"string","description":"EIP-712 SafeTx struct hash: shown as \"Message hash\"."},"purpose":{"type":"string","enum":["approve","schedule"]},"approverIndex":{"type":"integer"},"approval":{"type":"string","pattern":"^0x[0-9a-f]{210}$","description":"The 105-byte approval to add to the Safe signatures (or to `schedule` for a governance change)."},"expiresAt":{"type":"integer","description":"Unix seconds."}}},"SafeGuardSafe":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"chainId":{"type":"integer"},"network":{"type":["string","null"]},"safe":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"guard":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"name":{"type":"string"},"policy":{"$ref":"#/components/schemas/SafeGuardPolicy"},"createdAt":{"type":"string","format":"date-time"},"chain":{"type":"object","description":"Read from chain (cached 30 s): `reachable`, `installed`, `otherGuard`, `setUp`, the guard's numbers and delays, `recipientLock`, counts of approvers, book entries and owners."},"alerts":{"type":"object","description":"In the overview only: what the chain watcher saw on this Safe in the last 7 days.","properties":{"critical":{"type":"integer"},"total":{"type":"integer"}}}}},"SafeGuardPolicy":{"type":"object","description":"Touchque's limits on top of the guard, checked before any phone is asked (`over_limit`). The guard does not know them.","properties":{"maxNativeWei":{"oneOf":[{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"},{"type":"null"}],"description":"Per transaction, the network's coin in wei (batches added up)."},"tokenLimits":{"type":"array","maxItems":20,"items":{"type":"object","required":["token","symbol","decimals","maxUnits"],"properties":{"token":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"symbol":{"type":"string","maxLength":12},"decimals":{"type":"integer","minimum":0,"maximum":36},"maxUnits":{"type":"string","pattern":"^(0|[1-9][0-9]{0,77})$"}}}}}},"IdentityAttackProtection":{"type":"object","description":"bruteForce { enabled, notifyUser }; suspiciousIp { enabled, maxFailures }; bot { provider: turnstile | hcaptcha | recaptcha, siteKey, secret (write-only), mode: off | risky | always }; breachedPassword { atLogin: block | notify | off }; newDeviceNotice; riskEngine (the adaptive policy also asks the Touchque risk engine); limits { name: { max } }.","additionalProperties":true},"IdentityDelivery":{"type":"object","description":"Body for PUT /identity/delivery/{channel}: provider and from (an email address; for SMS the approved sender name or, with Twilio, a phone number), fromName (email), and the provider's fields: smtp host, port, secure, user, password; ses region, accessKeyId, secretAccessKey; postmark serverToken, messageStream; sendgrid apiKey; twilio accountSid, authToken, messagingServiceSid; netgsm usercode, password. A secret left out keeps its stored value while the provider stays the same. An email from must be on a verified domain of your organization (Organization → Domains) or a verified custom sign-in domain, or a subdomain of one: otherwise 400 `domain_not_verified` with `field: from`, `verifiedDomains` and `loginDomains` (503 `domains_unavailable` while the organization's domains cannot be read). An unchanged from stays allowed.","additionalProperties":true},"IdentityTenant":{"type":"object","description":"The workspace's Touchque Login tenant: slug and sign-in domain, issuer, the customers' account page (`accountPageUrl`) and My Account API audience (`myAccountAudience`), branding, sign-up, MFA policy and factors, sign-in methods, password policy, session lifetimes, settings. `settings.accountPage`: { enabled, allowDelete, allowEmailChange, myAccountApi, homeUrl }. `settings.smsCountries`: calling codes texts may go to (empty: any). `settings.profileFields`: up to 10 { key, label, type: text | tel | date | select | checkbox, options, required, afterLogins } asked after sign-in (progressive profiling). `mfaFactors` may include sms; `mfaPolicy` adaptive asks for a second step only when a sign-in looks risky. The hosted pages always show \"Secured by Touchque\". `branding.approverAppName`: the phone app the hosted pages tell customers to approve on (null: your brand name when the Mobile SDK is set up, else Touchque); `approverApp` shows the name in use and its source. `customDomainOnly`: serve only on the primary custom domain ({slug}.login.touchque.com redirects page loads there and refuses other calls; needs a verified primary domain with a working certificate).","additionalProperties":true},"IdentityClient":{"type":"object","description":"An app that signs its users in (spa, web, native, m2m): client ID, redirect, sign-out and web origin URIs, grant types, first-party, sign-in route, organization use, token lifetimes. A secret is returned only when created or rotated (`clientSecret`).","additionalProperties":true},"IdentityApi":{"type":"object","description":"An API apps ask tokens for: identifier (the audience), scopes, token lifetime, refresh tokens, role-based access, permissions in the token, machine-to-machine grants.","additionalProperties":true},"IdentityConnection":{"type":"object","description":"A social (google, apple, microsoft, github, facebook, linkedin, x) or enterprise (oidc, saml) connection. Secrets are never returned (`hasClientSecret`, `hasPrivateKey`).","additionalProperties":true},"IdentityUser":{"type":"object","description":"A customer account: id (the OIDC `sub`), email and verification, profile, user and app metadata, blocked, linked accounts, second factors, roles, organizations, sign-in counts.","additionalProperties":true},"IdentityUserCreate":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"emailVerified":{"type":"boolean"},"password":{"type":"string","description":"Checked against the tenant's password policy."},"passwordHash":{"type":"string","description":"A bcrypt ($2a/$2b/$2y) or argon2 ($argon2id…) hash as it is; replaced with Argon2id at the next sign-in."},"customPasswordHash":{"type":"object","description":"An old system's hash described like Auth0's custom_password_hash: algorithm (bcrypt, argon2, pbkdf2, scrypt, sha1, sha256, sha512, md5), hash { value, encoding }, salt { value, encoding, position }, digest, iterations, keylen, cost, blockSize, parallelization.","additionalProperties":true},"name":{"type":"string"},"givenName":{"type":"string"},"familyName":{"type":"string"},"nickname":{"type":"string"},"picture":{"type":"string","format":"uri"},"phone":{"type":"string","description":"E.164."},"locale":{"type":"string"},"userMetadata":{"type":"object","description":"Editable by the user.","additionalProperties":true},"appMetadata":{"type":"object","description":"Only you and your Actions write it.","additionalProperties":true},"sendVerificationEmail":{"type":"boolean"}}},"IdentityJob":{"type":"object","description":"A bulk import or export: id, kind, status (running, done, failed), summary (inserted, updated, failed, errors; or users), timestamps, fileReady.","additionalProperties":true},"IdentityRole":{"type":"object","description":"A role: name, description, permissions [{ apiId, permission }], users.","additionalProperties":true},"IdentityOrganization":{"type":"object","description":"A business customer (B2B): id, name (the ?organization= value), display name, branding, metadata, members, connections.","additionalProperties":true}},"responses":{"Unauthorized":{"description":"Authentication missing or not acceptable. `error` is one of `API_KEY_REQUIRED`, `SIGNATURE_REQUIRED`, `TIMESTAMP_REQUIRED`, `TIMESTAMP_EXPIRED`, `NONCE_REQUIRED`, `NONCE_INVALID`, `SIGNATURE_LEGACY_REJECTED`, each with a `message`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Authentication rejected or operation not permitted. Authentication failures: `SIGNATURE_INVALID`, `SIGNATURE_VERIFY_FAILED` (legacy key without secret), `API_KEY_INVALID` / `IP_BLOCKED` (legacy keys), `Invalid API key` / `Credential has been revoked` / `IP not allowed` (tq_ keys, plain message in `error`), `{ code: \"credential_org_access_revoked\", error }` (tq_ key whose creator is no longer a member of the organization it was created in), `{ code: \"credential_expired\", error }` (tq_ key past the expiry it was created with), `ACCESS_DENIED` (WAF ban). Most operations also answer 403 `Integration not found` when the key resolves to no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"Rate limited. The WAF limits each IP per endpoint (default 100/min, 300/min across endpoints; tighter on sensitive endpoints) and answers `{ error: \"RATE_LIMITED\", message, retryAfter }` without a Retry-After header; repeated abuse escalates to temporary IP bans (429, then 403 `ACCESS_DENIED`). Requests from 127.0.0.1 bypass the WAF. Quota-guarded endpoints also limit each workspace to 600 requests/min (`INTEGRATION_QUOTA_PER_MINUTE`), answering `{ error: \"integration_quota_exceeded\", message, retryAfter }` with Retry-After.","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}},"ServerError":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ValidationError":{"description":"Missing or invalid request field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"OfflineNotConfigured":{"description":"Offline sign is not configured on this server (`offline_sign_not_configured`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}