{
  "openapi": "3.1.0",
  "info": {
    "title": "Agent Passkey API",
    "version": "1.0.0",
    "description": "The `/v1` REST facade over the Agent Passkey MCP tools. Every route calls exactly one tool with the same arguments `tools/call` takes and returns the same result object an MCP client gets, with an HTTP status derived from its `state`. Authenticate with `Authorization: Bearer <token>`: an API key (`apk_key_…`), an OAuth access token from /authorize + /token, or an automation token (`apk_auto_…`). Each operation names the scope it needs in `x-scope`; `null` means any valid bearer.\n\nDescriptions are generated from the live tool descriptors for a placeholder grant, so where they mention \"example.com\" a real connection sees its own connected sites. GET /v1/connectors is the live answer."
  },
  "servers": [
    {
      "url": "https://app.solospace.dev"
    }
  ],
  "tags": [
    {
      "name": "access"
    },
    {
      "name": "approvals"
    },
    {
      "name": "audit"
    },
    {
      "name": "automations"
    },
    {
      "name": "connectors"
    },
    {
      "name": "credentials"
    },
    {
      "name": "enrol"
    },
    {
      "name": "keys"
    },
    {
      "name": "runs"
    },
    {
      "name": "signin"
    },
    {
      "name": "subjects"
    }
  ],
  "paths": {
    "/v1/connectors": {
      "get": {
        "operationId": "passkey_list",
        "summary": "Which sites you can sign into",
        "description": "USE THIS WHEN you have hit a login wall, or are about to, and need to know whether this person has connected a passkey for that site. Call it BEFORE you go hunting for a login page — the slow part of signing in was never the crypto, it is rediscovering a flow somebody already worked out.\n\nIt lists the sites you may sign into with a stored passkey — the domain, the identity, the sign-in URL, the connector's mode (whether a signature wakes a human), and the OPERATING NOTES for that site.\n\nThe `notes` field on each connector is the most useful thing here and it is first in the result for that reason: it is prose written by whoever last drove that site, saying where the passkey option hides (\"Google offers the passkey on the account-chooser, or after the email step; if it asks for a password first, click Try another way\"). `notesSource: \"connector\"` means a human wrote it for this exact credential; \"default\" means it is a general note for that site and you should verify it against the page; \"none\" means nobody has written one — if you work the flow out, say so, and the user can record it for the next agent.\n\nREAD BOTH NOTE FIELDS. When a human wrote a connector note, the general note for that site is still returned as `siteNotes`, because the two say different things: one tells you where the button is, the other tells you the site blocks browsers it thinks are automated. The second is often the one that decides whether the attempt works at all.\n\nThis list is already narrowed to what the person who connected you actually ticked. If a site you expect is missing, it was not granted; asking again in another way will not produce it, and the fix is for them to change it on their dashboard.\n\nA connector in \"approval\" mode means a human is asked on their phone before each signature, so treat a sign-in as something that can take up to 5 minutes and can legitimately be refused — on the ticket path the page waits for the tap by itself; on the fallback send `waitUpToMs` to passkey_complete_signin so the SERVER waits, rather than polling. \"silent\" mode signs with UP=0/UV=0, which most sites reject — if a silent connector fails at the site, that is expected, not a bug. Read `mode` off the connector rather than assuming those are the only two: the modes this deployment offers can change, and the value is reported live.\n\nIT ALSO TELLS YOU WHICH ACCOUNT YOU ARE. Every result here carries `actingAs`, and the `grant` block restates what a human actually consented to: the sites, the accounts, the scopes and when your authority expires. If more than one account is listed, you do not yet know which one you are meant to be working in — find out before you act, and say so with `expectAccount` on the call that matters.\n\nIf you learn something durable about a site while you are in it — a control that lies about having worked, a page that is enormous to snapshot, where the account switcher hides — write it back with passkey_note so the next agent does not buy it again.\n\nThis tool only reads a catalogue. Signing in is passkey_begin_signin plus a browser you drive; passkey_howto has the procedure.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.\n\nThis GET route carries only its path parameters; the tool's `expectAccount`, `subjectId` argument(s) are not available here. Use POST /mcp (tools/call) to pass them.",
        "tags": [
          "connectors"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_list",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "x-read-only": true
      }
    },
    "/v1/connectors/{domain}": {
      "get": {
        "operationId": "passkey_connector_status",
        "summary": "What the service has recorded about one connector",
        "description": "USE THIS WHEN you are about to ask for a signature on an approval-gated connector and want to know whether a human is ALREADY being asked — raising a second request buzzes the same person twice, and this call is free.\n\nIt reports what the SERVICE has recorded for one connector: its mode, its operating notes, when it last signed successfully, and whether a signature is waiting on a human right now.\n\nIt does NOT tell you whether your browser has a live session for the site. Nothing on this server can see your browser. If that is your question, open the site and look — and if you are also running the local Agent Passkey stdio server, that question is passkey_session_status there. (These two tools used to share the name passkey_status and answer different questions. They no longer do.)\n\nIt also restates the GRANT — which sites, which accounts, which scopes, and when your authority expires — and the account you are acting as. Read those at step 1 of a long task, not at step 30.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.\n\nThis GET route carries only its path parameters; the tool's `expectAccount` argument(s) are not available here. Use POST /mcp (tools/call) to pass them.",
        "tags": [
          "connectors"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_connector_status",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "One of the connected domains, exactly as passkey_list spells it. Connected when this list was built: example.com. THIS LIST IS A SNAPSHOT taken when your client fetched this tool list, and clients cache tool lists: a site connected since then will be missing from it, and a value missing here is NOT evidence that it does not exist. Never answer \"you have no passkey for that site\" from this text — call passkey_list, which is live. Any value is accepted here and checked against the current grant; if it is not covered, the refusal lists what is.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-read-only": true
      }
    },
    "/v1/connectors/{domain}/notes": {
      "post": {
        "operationId": "passkey_note",
        "summary": "Write down what you learned about a site, for the next agent",
        "description": "USE THIS WHEN you have just worked out something durable about a connected site that you would want to be told at the START of this task — where the passkey option hides, that a Create button fires no request and succeeds anyway, that a console renders in nested iframes so a full snapshot is enormous, that the account switcher lies about which profile a page is acting as. Write it now, while you still know it. Nothing else here persists what a session learned, so the alternative is that it is thrown away and the next agent pays for it again.\n\nWHAT IS WORTH WRITING: a fact that will still be true next month and that cost you something to find. `kind: \"login\"` is how to GET IN; `kind: \"site\"` is how to WORK THE SITE once in — the second is the one nobody records and the expensive one to rediscover. Write PROSE, not selectors: \"click Sign in with a passkey\" survives three redesigns, `div.x7Kd > button:nth-child(2)` was wrong the day the site shipped one. Do not write what you were told by the page; write what you OBSERVED it do. Do not write anything you would not want a stranger to read: it is not a scratchpad, it has no secrets in it, and it is not the place for a challenge, a token or a URL with one in it.\n\nWHAT IT DOES NOT DO, so you can judge whether to bother. It changes NOTHING about what anyone may sign, which origins are accepted, which credential you are offered, or whether a human is asked. It is text handed to the next reader, who is a model looking at the real page anyway. Your note is stored as PROVISIONAL, attributed to this connection (the service records who wrote it — you cannot write as somebody else), and shown to the account owner, who may promote, edit or delete it. It is returned to other agents inside a labelled fence that says it is an unreviewed observation.\n\nLIMITS: 2000 characters, truncated with a mark if you go over. The same sentence written twice is one note. A connector keeps a dozen live provisional notes per kind and the oldest unreviewed one falls off, so replace rather than append when you are correcting yourself.\n\nThis tool never signs anything and never wakes anybody.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "connectors"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_note",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The connected domain the note is about, exactly as passkey_list spells it. There is no shared pool and no way to write a note about a site somebody else connected: a domain outside this connection is refused, and the refusal lists the domains that are inside it. Connected when this list was built: example.com. THIS LIST IS A SNAPSHOT taken when your client fetched this tool list, and clients cache tool lists: a site connected since then will be missing from it, and a value missing here is NOT evidence that it does not exist. Never answer \"you have no passkey for that site\" from this text — call passkey_list, which is live. Any value is accepted here and checked against the current grant; if it is not covered, the refusal lists what is.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "login",
                      "site"
                    ],
                    "description": "\"login\" = how to get in (where the passkey option is, what the site does first). \"site\" = how to work the site once you are in (what a snapshot costs, which controls lie about having worked, how to tell which account a page is acting as)."
                  },
                  "note": {
                    "type": "string",
                    "description": "The note, in prose, addressed to the next agent that has to do this. One or two sentences of what is true and how you know. No selectors, no secrets, no instructions to the reader — an observation about a page."
                  },
                  "expectAccount": {
                    "type": "string",
                    "description": "Optional but strongly recommended: the account you believe you are acting as, e.g. \"elishavas2@gmail.com\". If it does not match a connector this connection holds, the call is REFUSED and nothing is signed. Declare it whenever the person who asked you named an account, and whenever more than one account for a site could plausibly be meant — doing the work in the wrong account is the most expensive mistake available here, and it is silent unless you declare. Matched case-insensitively and otherwise exactly: no aliasing of dots or +tags, because guessing that two addresses are one account is precisely the error this prevents. Where the owner gave an account a label (a national-ID account shown as \"Elisha (ת.ז.)\", say), the label is what results show and what you should declare; the site's own name for the account is still accepted. So is any of the account's `aliases` (passkey_list shows them): the other names the site itself shows for it, such as a GitHub @login beside the email. Declare the name you actually SEE on the page; if it is listed as an alias, it is the same account."
                  }
                },
                "required": [
                  "kind",
                  "note"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/access": {
      "get": {
        "operationId": "passkey_check_access",
        "summary": "What this connection may do (costs nothing, wakes nobody)",
        "description": "USE THIS WHEN you want to know what you are allowed to do here BEFORE doing it — which scopes this connection holds, which it does not, which tools are actually usable, which sites are connected, and when your token expires.\n\nThis is the call to make before the one that wakes a human. It reads nothing but your own grant: it never signs, never notifies anybody, and needs no scope beyond a valid token, so there is no situation in which calling it is the wrong move.\n\nIt exists because a scope you do not hold is otherwise invisible until you spend a call discovering it. If it reports a scope missing, no rewording of a tool call will produce it — a human authorizes it again, and the result tells you which scope to ask for.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "access"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": null,
        "x-tool": "passkey_check_access",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "x-read-only": true
      }
    },
    "/v1/signin/begin": {
      "post": {
        "operationId": "passkey_begin_signin",
        "summary": "Sign in with a passkey: mint a ticket and the page script (start here)",
        "description": "USE THIS WHEN you have hit a login wall — or a re-authentication prompt such as GitHub's \"Confirm access\" — on a connected site, in a browser you drive. This is the tool that gets you past it. It mints a single-use ceremony TICKET for one credential and returns TWO ways to spend it; `next` names the one to use on this site.\n\nPATH 1 — `armUrl`, when the browser you drive has the Agent Passkey extension (the person's own Chrome usually does). Navigate the tab to `armUrl`: the extension binds the ticket to that tab and sends you straight back to the page (pass `pageUrl` to come back to where you are). Then click the site's OWN passkey button (\"Use passkey\", \"Sign in with a passkey\"). The extension answers that one prompt from its background worker, so it works on strict-CSP sites (github.com) and needs NO page script, no javascript tool, and nothing base64 crossing you. Navigate and click — that is all. If the extension is not in that browser, the arm page says so in one line, and that line is your answer.\n\nPATH 2 — `shim`, for a browser without the extension, on a site whose CSP lets the page reach this service (`ticketPath.likely` true). `shim` is a page script with the ticket, this service's origin and the credentialId baked in. Evaluate the shim in the page as-is, drive the page to its passkey prompt, and THE PAGE does the rest — it sends the ceremony bytes to this service itself and hands the signature to the site. You never read, carry or paste a byte of the ceremony.\n\nWHY THIS AND NOT passkey_complete_signin: that tool makes YOU the courier of a base64url clientDataJSON that is ~30 KB on Google, and browser tools that block base64 returns (Claude in Chrome's javascript_tool does, correctly) cannot do it at all. With a ticket nothing base64 crosses from the page to you: the ticket is a UUID, the status you poll is a plain phrase, and the only base64 on the path is inside the script you send INTO the page. If your browser tool refuses base64, that is fine — you never need it to.\n\nWHAT A TICKET IS WORTH: one signature, for this one credential, under this connection, for 5 minutes (longer only while a human is deciding about it). Presenting it runs every gate a tool call runs — consent record, origin, policy, rate limit, approval or session mode, the account interlock — and the result is audited to this connection. It carries no authority this connection does not already have, and it cannot be pointed at a different credential. Minting one signs nothing and wakes nobody.\n\nAFTER YOU INSTALL THE SHIM, poll `window.__apk.status` in the page: `waiting` (site has not asked yet), `signing`, `signing: waiting for approval` (a human is being asked — the page keeps re-presenting the ticket by itself, you just check again), `signed`, or `failed: <reason>` (terminal for this ticket; report it, do not mint another to retry a denial). If you cannot read even that, passkey_signin_status({ ticket }) gives the server's view inside the four-field result.\n\nThe shim returns \"installed and armed\" when evaluated. If the site navigates to another host before it asks for the passkey (Google does), evaluate the same shim again there — the ticket is unspent until a signature is produced. Install it before you click anything: sites rarely ask before a user action, but the shim must be in place when they do.\n\nSAY WHICH ACCOUNT. If the person who asked you named an account, or this connection holds more than one account that could be meant, send `expectAccount`: the ticket is then bound to that declaration and refused — at mint and again at redemption — unless the credential belongs to that account.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "signin"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "signin",
        "x-tool": "passkey_begin_signin",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "credentialId": {
                    "type": "string",
                    "description": "The credentialId from passkey_list, base64url. Which is which, as of when this tool list was built: ZXhhbXBsZS1jcmVkZW50aWFs = example.com. THIS LIST IS A SNAPSHOT taken when your client fetched this tool list, and clients cache tool lists: a site connected since then will be missing from it, and a value missing here is NOT evidence that it does not exist. Never answer \"you have no passkey for that site\" from this text — call passkey_list, which is live. Any value is accepted here and checked against the current grant; if it is not covered, the refusal lists what is."
                  },
                  "expectAccount": {
                    "type": "string",
                    "description": "Optional but strongly recommended: the account you believe you are acting as, e.g. \"elishavas2@gmail.com\". If it does not match a connector this connection holds, the call is REFUSED and nothing is signed. Declare it whenever the person who asked you named an account, and whenever more than one account for a site could plausibly be meant — doing the work in the wrong account is the most expensive mistake available here, and it is silent unless you declare. Matched case-insensitively and otherwise exactly: no aliasing of dots or +tags, because guessing that two addresses are one account is precisely the error this prevents. Where the owner gave an account a label (a national-ID account shown as \"Elisha (ת.ז.)\", say), the label is what results show and what you should declare; the site's own name for the account is still accepted. So is any of the account's `aliases` (passkey_list shows them): the other names the site itself shows for it, such as a GitHub @login beside the email. Declare the name you actually SEE on the page; if it is listed as an alias, it is the same account."
                  },
                  "subjectId": {
                    "type": "string",
                    "description": "The subject (your end customer) this sign-in is on behalf of — the uuid from passkey_subject_create, or your externalId. REQUIRED when the credential was enrolled for a subject: a subject-bound passkey is never signed with unless the call names that subject (a mismatch or an omission is refused as subject_mismatch). Ignored for a credential with no subject."
                  },
                  "pageUrl": {
                    "type": "string",
                    "description": "Optional: the https URL of the page showing the passkey prompt right now (a GitHub \"Confirm access\" page, say). `armUrl` then returns you there instead of to the connector's sign-in URL. Must be on the connector's own site; anything else is ignored."
                  },
                  "runId": {
                    "type": "string",
                    "description": "Optional. The runId from passkey_run_begin, when this sign-in is part of an automation run. The signature is then audited under that run, so the person can see which run produced it. Omit outside a run."
                  }
                },
                "required": [
                  "credentialId"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/signin/{ticket}": {
      "get": {
        "operationId": "passkey_signin_status",
        "summary": "Where a ceremony ticket is (the server's view)",
        "description": "USE THIS WHEN you have minted a ticket with passkey_begin_signin and want to know what became of it without reading anything out of the page — or when your browser tool cannot return even the status string. It reports the server's view of one ticket: `unused` (no page has presented it yet — drive the page to the passkey prompt; this comes back as state \"ok\", phase \"unused\", because nobody is being asked and the next move is yours), `in_flight` (a page is presenting it right now), `pending_approval` (a human is being asked; the page will present it again on its own), `used` (its one signature was produced — watch the site for the navigation), `denied` with the code and reason (terminal; report it), `expired`, or `unknown` (not a ticket of this connection, or this service restarted — mint another).\n\nIt signs nothing, wakes nobody, and can be called as often as you like. It is not a substitute for looking at the page: `used` means the service signed, not that the site accepted.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "signin"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "signin",
        "x-tool": "passkey_signin_status",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "ticket",
            "in": "path",
            "required": true,
            "description": "The ticket UUID passkey_begin_signin returned.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-read-only": true
      }
    },
    "/v1/signin/complete": {
      "post": {
        "operationId": "passkey_complete_signin",
        "summary": "FALLBACK: finish a sign-in by carrying the bytes yourself",
        "description": "USE THIS WHEN passkey_begin_signin's page script cannot make a fetch from the page to this service (a page CSP that blocks connect-src, or an evaluate tool with no network from the page) — and only then. It is the FALLBACK. On this path a login page you are driving has called navigator.credentials.get(), the parked-mode shim from passkey_howto has parked the request in window.__apk.pending, and YOU ferry its clientDataJSON here and the signature back. That ferry is a base64url string of ~30 KB on Google; a browser tool that blocks base64 returns (Claude in Chrome's javascript_tool) cannot do it, and that is exactly why passkey_begin_signin exists. Prefer it.\n\n(It was called `passkey_assert` until an earlier revision. \"Assert\" named the crypto operation rather than the job, so nobody looking for a way past a login wall ever matched it. There is no alias: the old name is a hard error.)\n\nWHAT YOU NEED FIRST: a browser you drive, with the parked-mode shim from passkey_howto installed, which parks the page's credential request instead of letting the real device answer it. You hold the parked request's clientDataJSON. Send those bytes and the page origin; you get back authData, signature and userHandle, which you return to the page as the resolved credential (window.__apk.deliver). That is the whole exchange. You never see the private key and there is no argument here that could ask for it.\n\nPASS clientDataJSON THROUGH UNTOUCHED. The shim builds those bytes inside the page and the page keeps them to hand to the site; your job is to move the base64url string, not to re-create it. Do not decode and re-encode it, reformat the JSON or reorder its keys. The service hashes what you send and the site hashes what the page holds; one differing byte produces a signature that verifies against nothing, after every gate has passed.\n\nThe origin is the origin of the page the ceremony is running on, which is NOT necessarily the origin of the connector's signinUrl — a site may navigate to another host before it asks for a passkey. It is CHECKED twice: against the origin inside the clientDataJSON (they must be identical), and against this credential's scope. That second check is NOT an exact match against the enrolled `origins` list, and believing it is has already cost one agent a sign-in it could have completed: the origin must share a REGISTRABLE DOMAIN with an enrolled origin, and the rpId must be a registrable-domain suffix of the origin's host. A site that enrols on one host and authenticates on a sibling host is the ordinary case and it works. Send the origin the page is actually on and read the answer — the server decides, passkey_list's originAdvice line only describes the rule. If it does come back denied with code \"origin_rejected\", THAT is the control working: stop, and do not try origins until one is accepted.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nTHE STATES, and what each asks of you:\n  state \"ok\"      — authData, signature and userHandle are in the result. Hand them to the page. terminal: true; each assertion is good once, so do not call again for the same ceremony.\n  state \"pending\" — a human is being asked, right now, on their phone. NOT a failure and NOT a rate limit. `next` names this same tool, the wait in milliseconds, and THE EXACT ARGUMENTS to send: they are the ones you just sent, plus `waitUpToMs`. Send them unchanged apart from that — and use `waitUpToMs` rather than polling, which is what it is for.\n  state \"denied\"  — terminal, retryable: false, next: null. Something DECIDED against this request. `code` says which gate: approval_denied (a human said no), approval_timeout (nobody answered in time), policy_denied (a rate limit or an expiry), origin_rejected (the origin does not share a registrable domain with one the passkey was enrolled on, or disagrees with the clientDataJSON), argument_mismatch (see below). Whichever it is: stop, do not retry, and do not look for another route into the account; tell whoever asked you.\n  state \"error\", code \"bad_request\" — YOU got an argument wrong. Nobody was asked and nothing was decided, so this one IS retryable once you fix it: retryable is true and terminal is false, which is what separates it from a denial.\n\nYOU HAVE NO SLEEP, SO ASK THE SERVER TO WAIT. Send `waitUpToMs` and this call parks server-side until the human answers, up to 25000 ms. That is the difference between one call and a dozen, each of which costs you a round trip and a result blob. Ask for more and you get 25000 ms, and the result says what was actually waited (`waitedMs`, `waitCappedToMs`): the cap is set below every client transport timeout we know of, deliberately, so you always get a structured result instead of a socket error. An unanswered approval expires about 5 minutes after it was raised, so a couple of waits covers the whole window. It changes nothing about the request itself: it is not part of what the human approves and not part of the argument binding below.\n\nDO NOT CHANGE THE ARGUMENTS WHILE AN APPROVAL IS PENDING. The approval is bound to a hash of exactly these three arguments (`waitUpToMs` and `expectAccount` are not among them and are free to change). If you poll with anything different — a re-encoded clientDataJSON, a fresh challenge from a reloaded page, a different origin — the call is REFUSED with code \"argument_mismatch\", because the human is looking at the request you raised and not at the one you have since drifted to. This is enforced, not advised.\n\nSAY WHICH ACCOUNT. If the person who asked you named an account, or if this connection holds more than one account that could plausibly be meant, send `expectAccount`. The call is then REFUSED unless the credential you picked belongs to that account — nothing is signed and nobody is woken. Signing into the wrong one of a person's eleven Google accounts is the most expensive thing you can do here, and without the declaration it is completely silent. Do not respond to a refusal by trying account names until one is accepted; report which accounts are actually connected.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "signin"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "signin",
        "x-tool": "passkey_complete_signin",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "credentialId": {
                    "type": "string",
                    "description": "The credentialId from passkey_list, base64url. Which is which, as of when this tool list was built: ZXhhbXBsZS1jcmVkZW50aWFs = example.com. THIS LIST IS A SNAPSHOT taken when your client fetched this tool list, and clients cache tool lists: a site connected since then will be missing from it, and a value missing here is NOT evidence that it does not exist. Never answer \"you have no passkey for that site\" from this text — call passkey_list, which is live. Any value is accepted here and checked against the current grant; if it is not covered, the refusal lists what is. If the page's allowCredentials list is non-empty and does not contain the one you pick, the site is asking for a different key and signing will not help."
                  },
                  "waitUpToMs": {
                    "type": "number",
                    "description": "Optional. How long, in milliseconds, the SERVER should wait for the human to answer before replying — you have no sleep primitive, so this is how you wait. Clamped to 25000 ms, which is under every client transport timeout we know of: you always get a structured result rather than a socket error, and the result reports `waitedMs` and `waitCappedToMs` so a clamp is never silent. Omit it (or send 0) to get the old behaviour — an immediate `pending` you then have to poll. It is not part of the approval binding, so adding or changing it between polls is safe."
                  },
                  "clientDataJSON": {
                    "type": "string",
                    "description": "The base64url clientDataJSON for THIS ceremony, passed through exactly as the page holds it — copy the string out of the parked request; do not decode and re-encode it, reformat the JSON or reorder its keys. Limit 64 KiB. Most sites produce a couple of hundred bytes; the limit is generous because Google's sign-in challenge has been observed at ~25 KiB on its own, which a 4 KiB limit used to reject."
                  },
                  "origin": {
                    "type": "string",
                    "description": "The exact origin of the page the ceremony is happening on: scheme and host only, no path and NO TRAILING SLASH — \"https://accounts.google.com\", not \"https://accounts.google.com/\". (A connector's signinUrl usually has a trailing slash; the origin never does.) It must equal the origin inside clientDataJSON, and it must share a registrable domain with one of the connector's enrolled origins — a sibling host the site navigated to is accepted, an unrelated domain is not. Read it off the parked request rather than typing it."
                  },
                  "expectAccount": {
                    "type": "string",
                    "description": "Optional but strongly recommended: the account you believe you are acting as, e.g. \"elishavas2@gmail.com\". If it does not match a connector this connection holds, the call is REFUSED and nothing is signed. Declare it whenever the person who asked you named an account, and whenever more than one account for a site could plausibly be meant — doing the work in the wrong account is the most expensive mistake available here, and it is silent unless you declare. Matched case-insensitively and otherwise exactly: no aliasing of dots or +tags, because guessing that two addresses are one account is precisely the error this prevents. Where the owner gave an account a label (a national-ID account shown as \"Elisha (ת.ז.)\", say), the label is what results show and what you should declare; the site's own name for the account is still accepted. So is any of the account's `aliases` (passkey_list shows them): the other names the site itself shows for it, such as a GitHub @login beside the email. Declare the name you actually SEE on the page; if it is listed as an alias, it is the same account."
                  },
                  "subjectId": {
                    "type": "string",
                    "description": "The subject (your end customer) this sign-in is on behalf of — the uuid from passkey_subject_create, or your externalId. REQUIRED when the credential was enrolled for a subject: a subject-bound passkey is never signed with unless the call names that subject (a mismatch or an omission is refused as subject_mismatch). Ignored for a credential with no subject."
                  },
                  "runId": {
                    "type": "string",
                    "description": "Optional. The runId from passkey_run_begin, when this sign-in is part of an automation run. The signature is then audited under that run, so the person can see which run produced it. Omit outside a run."
                  }
                },
                "required": [
                  "credentialId",
                  "clientDataJSON",
                  "origin"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/approvals": {
      "get": {
        "operationId": "passkey_approvals",
        "summary": "Sign-ins waiting on a human",
        "description": "USE THIS WHEN a sign-in came back pending and you have lost track of whether the human has answered or the request has expired.\n\nIt lists the signatures this connection has asked a human to approve and that are still open, with the deadline on each. It shows only approvals raised by this connection — never anybody else's activity.\n\nIt does not approve anything and cannot hurry anyone. If an approval you raised is gone from this list, it was decided or it expired: call passkey_complete_signin once more with the SAME arguments to find out which — with `waitUpToMs` set, so that call also does your waiting for you.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "approvals"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "approvals:read",
        "x-tool": "passkey_approvals",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "x-read-only": true
      }
    },
    "/v1/approvals/{id}": {
      "get": {
        "operationId": "passkey_approval_status",
        "summary": "What became of one approval (with a server-side wait)",
        "description": "USE THIS WHEN a result gave you an `approvalId` — a pending passkey_complete_signin, a passkey_signin_status in `pending_approval`, or passkey_connector_status — and you want to know whether the human has answered, without re-sending the sign-in and without polling. It reports the state of that one approval: `pending` (still being asked), `approved` (their yes is waiting to be collected), `consumed` (it already bought its one signature), `denied`, or `expired`.\n\nYOU HAVE NO SLEEP, SO ASK THE SERVER TO WAIT. Send `waitUpToMs` and this call parks server-side until the human answers, up to 25000 ms — the same cap as passkey_complete_signin, set below every client transport timeout we know of, so you always get a structured result rather than a socket error. The result says what was actually waited (`waitedMs`, `waitCappedToMs`).\n\nIT COLLECTS NOTHING. An approved decision is redeemed by the call that raised it: on the fallback path, re-send the identical passkey_complete_signin; on the ticket path the page re-presents the ticket by itself. This tool only tells you when that is worth doing. It shows only approvals raised by this connection — an id that is not yours answers `not_found`, exactly like one that does not exist.\n\nEvery `pending` and `denied` result carries `history`: which arming is live and how the earlier prompts ended, so you can tell the person which notification still works.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.\n\nThis GET route carries only its path parameters; the tool's `waitUpToMs` argument(s) are not available here. Use POST /mcp (tools/call) to pass them.",
        "tags": [
          "approvals"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "approvals:read",
        "x-tool": "passkey_approval_status",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The approvalId a pending result carried.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-read-only": true
      }
    },
    "/v1/automations": {
      "get": {
        "operationId": "passkey_automation_list",
        "summary": "The automations this person has",
        "description": "USE THIS WHEN you are asked to run, check on, or change an automation, or before proposing one that may already exist. Lists every automation of this account with its status (proposed / active / paused), trigger, next due time and last run. Costs nothing, wakes nobody.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_automation_list",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "x-read-only": true
      },
      "post": {
        "operationId": "passkey_automation_propose",
        "summary": "Propose an automation (a human activates it)",
        "description": "USE THIS WHEN the person wants something done on a connected site REPEATEDLY or UNATTENDED — \"every morning\", \"whenever X happens\", \"without me\" — and you have worked out how to do it once. It records the task so a runner can do it later without you improvising: a name, plain-prose instructions (what to do once signed in), which connected accounts it uses, and a trigger.\n\nNOTHING RUNS WHEN YOU CALL THIS. The automation is `proposed`. The person activates it on their dashboard — that activation is a consent screen naming the accounts and a window, and it is what makes unattended sign-in honest. Tell them the URL the result gives you and what it will do. You cannot activate it, and asking again does not.\n\nINSTRUCTIONS ARE PROSE FOR THE NEXT AGENT, not a script: say what to do and how to tell it worked, never CSS selectors. The connector notes already say how to sign in; do not repeat them.\n\nTRIGGER: {kind:\"schedule\", cron:\"0 9 * * *\", tz:\"Asia/Jerusalem\"} (five-field cron, IANA zone), {kind:\"webhook\"} (the person gets a URL to POST to), or {kind:\"manual\"} (run when an agent calls passkey_run_begin).\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "automations"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_automation_propose",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "A short name, up to 80 characters. \"Morning bounce report\"."
                  },
                  "instructions": {
                    "type": "string",
                    "description": "What to do once signed in, in prose, up to 4000 characters. Include how to tell it worked."
                  },
                  "credentialIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The credentialIds from passkey_list this automation uses. Must all be in this connection. THIS LIST IS A SNAPSHOT taken when your client fetched this tool list, and clients cache tool lists: a site connected since then will be missing from it, and a value missing here is NOT evidence that it does not exist. Never answer \"you have no passkey for that site\" from this text — call passkey_list, which is live. Any value is accepted here and checked against the current grant; if it is not covered, the refusal lists what is."
                  },
                  "trigger": {
                    "type": "object",
                    "description": "{kind:\"schedule\", cron, tz} | {kind:\"webhook\"} | {kind:\"manual\"}",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "schedule",
                          "webhook",
                          "manual"
                        ]
                      },
                      "cron": {
                        "type": "string"
                      },
                      "tz": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "kind"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "name",
                  "instructions",
                  "credentialIds",
                  "trigger"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/runs": {
      "post": {
        "operationId": "passkey_run_begin",
        "summary": "Begin an automation run",
        "description": "USE THIS WHEN you are executing an automation — because a schedule fired, a `run.due` event arrived, or the person asked you to run it now. It opens the run and returns the instructions and the connectors the run may use. Pass the runId it returns to passkey_begin_signin so every signature is audited under this run, and call passkey_run_end when you are done, whatever happened.\n\nIt refuses an automation that is not active (nothing runs on `proposed`; only the person can activate or resume one), and one whose accounts this connection does not cover. One run at a time per automation: calling it again while a run is open returns that run rather than starting a second.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "runs"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "signin",
        "x-tool": "passkey_run_begin",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "automationId": {
                    "type": "string",
                    "description": "The automation id from passkey_automation_list or a run.due event."
                  },
                  "runId": {
                    "type": "string",
                    "description": "Optional. The runId from a run.due event, to claim that specific scheduled run."
                  },
                  "trigger": {
                    "type": "string",
                    "enum": [
                      "schedule",
                      "webhook",
                      "manual"
                    ],
                    "description": "Why this run is happening. Defaults to \"manual\"."
                  }
                },
                "required": [
                  "automationId"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/runs/{id}": {
      "get": {
        "operationId": "passkey_run_status",
        "summary": "What became of one automation run",
        "description": "USE THIS WHEN you hold a runId — from passkey_run_begin, a run.due event, or a previous session — and need to know whether the run is still open, who finished it and how. It reports one run of this account in any state: running, succeeded, failed, needs_human, missed (never claimed) or abandoned (left open too long), with its trigger, timestamps, summary and the requestId its signatures were audited under. Costs nothing, wakes nobody. A run that is not this account's answers `not_found`.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "runs"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "signin",
        "x-tool": "passkey_run_status",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The run uuid.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-read-only": true
      }
    },
    "/v1/runs/{id}/end": {
      "post": {
        "operationId": "passkey_run_end",
        "summary": "End an automation run",
        "description": "USE THIS WHEN a run you began is over — it worked, it did not, or it needs a human. Always call it: a run left open is reported as abandoned after a while and blocks the next one. `summary` is one or two sentences for the person, in plain words (ids and outcomes, never page content or anything from the account).\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "runs"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "signin",
        "x-tool": "passkey_run_end",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "From passkey_run_begin.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "succeeded",
                      "failed",
                      "needs_human"
                    ]
                  },
                  "summary": {
                    "type": "string",
                    "description": "Up to 2000 characters."
                  }
                },
                "required": [
                  "status"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/audit": {
      "get": {
        "operationId": "passkey_audit",
        "summary": "What this connection has asked for, and what it was told",
        "description": "USE THIS WHEN you need to reconstruct what happened — a person asks \"what did you do with my account\", a sign-in you thought worked did not, or you are picking up a task another session of this same connection left half done. It returns the audit rows this connection itself produced, newest first: each signature it asked for and whether it was allowed or denied, with the reason, the site, the origin, the credential and the requestId — plus the approvals a human answered about it.\n\nIT SHOWS YOUR OWN ROWS AND NOBODY ELSE'S. Not other agents, not the account owner's whole log, not their dashboard activity. The scope is called `audit:read`; what it reads is the part of the log that is about you. Nothing here can be widened by an argument.\n\nPAGING: up to 200 rows per call (`limit`); pass the oldest `at` you received as `before` to page further back. `reason` is a closed vocabulary written by the service, never free text, and no row ever carries a challenge, a token or a key.\n\nThis tool reads a log. It signs nothing and wakes nobody.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nThis GET route carries only its path parameters; the tool's `limit`, `before`, `subjectId`, `credentialId` argument(s) are not available here. Use POST /mcp (tools/call) to pass them.",
        "tags": [
          "audit"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "audit:read",
        "x-tool": "passkey_audit",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "x-read-only": true
      }
    },
    "/v1/enrol/begin": {
      "post": {
        "operationId": "passkey_begin_enrol",
        "summary": "Add a passkey to a site: mint an enrol ticket and the page script",
        "description": "USE THIS WHEN the person wants a NEW site connected — \"add a passkey to X\", \"connect my Y account\", \"make it so you can sign in to Z next time\" — or when passkey_list has no connector for a site you need and the person has said to add one. It mints a single-use ENROL TICKET for one site and returns `shim`: a page script that patches navigator.credentials.create() so that when the site asks to register a passkey, THE PAGE fetches its attestation from this service and hands the site an ordinary credential. The passkey is minted here, on the person's account, and added to THIS connection's accounts with the same mode this connection already has (session, or ask-me-every-time). You never read, carry or paste a byte of the ceremony.\n\nTHE ONE THING THIS SERVICE CANNOT DO: sign the person in to the site the first time. A site only offers \"add a passkey\" to an account that is ALREADY signed in, and this service holds no password. So before you mint anything, the person must be signed in to the site in the browser you drive — they do that themselves (a password, a magic link, whatever the site uses), or you ask them to. If you are at a login wall for a site with no connector, that is the situation: ask the human to sign in, then come back here.\n\nTHE PROCEDURE, in order: (1) the person is signed in to the site in your browser; (2) navigate to the site's security / passkeys settings and stop on its \"add a passkey\" screen — do not click the button yet; (3) call this tool with the site's domain, evaluate `shim` in the page as-is (it returns \"installed and armed\"), THEN click \"add a passkey\"; the site calls create(), the page fetches the attestation, `window.__apkEnrol.status` goes waiting → creating → created; (4) LOOK at the page: did the site list the new passkey? Then passkey_complete_enrol({ ticket, ok: true }) — or `ok: false` with what you saw, which revokes the credential so nothing half-enrolled is left behind. Until you complete it the ticket sits at `created`. Pass `autoConfirm: true` only if you will not be able to look.\n\nWHAT A TICKET IS WORTH: one credential, on the ONE site named, under this connection, for 5 minutes. Presenting it runs the same gates a direct enrolment runs — consent record, origin↔rpId, the owner's identity — and both the mint and the grant append are audited to this connection. Minting one creates nothing and wakes nobody.\n\nIf the page cannot reach this service (its CSP blocks connect-src) the status reads `failed: this page cannot reach the signing service…`; use passkey_enrol_direct then, and build the credential yourself.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "enrol"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "enrol",
        "x-tool": "passkey_begin_enrol",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "The site's domain — the WebAuthn rpId, which is the registrable domain the page will use: \"google.com\" for accounts.google.com, \"gov.il\" for login.gov.il. The page must be on an origin under it; a mismatch is refused at redemption."
                  },
                  "rpId": {
                    "type": "string",
                    "description": "Alias of `domain` — the extension's field name, so a relay can send its /enrol/begin body unchanged."
                  },
                  "origin": {
                    "type": "string",
                    "description": "Optional. THE EXTENSION'S SHAPE: when present, no ticket is minted — the attestation bundle comes straight back for a caller already on the site's page (identical to passkey_enrol_direct). The exact origin of that page, scheme and host only."
                  },
                  "userHandle": {
                    "type": "string",
                    "description": "Optional, base64url. With `origin`: the site's user.id from its creation options."
                  },
                  "signinUrl": {
                    "type": "string",
                    "description": "Optional. Where the next agent should go to SIGN IN with this passkey (not the \"add a passkey\" page). https:// only. Stored on the connector."
                  },
                  "userName": {
                    "type": "string",
                    "description": "Optional. The account name as the site shows it (usually an email). The page's own creation options override it when they carry a user.name, which they nearly always do."
                  },
                  "displayLabel": {
                    "type": "string",
                    "description": "Optional. What the OWNER calls this account — \"Work Google\", \"Elisha (ID)\". Shown in place of userName."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional, up to 2000 characters. How to GET IN next time: where the passkey option hides on the sign-in page. Prose, never selectors."
                  },
                  "siteNotes": {
                    "type": "string",
                    "description": "Optional, up to 2000 characters. How to WORK THE SITE once in — what a snapshot costs, which controls lie about having worked. Prose, never selectors."
                  },
                  "subjectId": {
                    "type": "string",
                    "description": "Optional. The subject (your end customer) this passkey is for — the uuid from passkey_subject_create. The credential is then bound to them: every later sign-in must name the same subject, and revoking the subject revokes the passkey."
                  },
                  "policy": {
                    "type": "object",
                    "description": "Optional. {mode: \"session\", expiresAt} makes the new passkey sign WITHOUT asking anybody until expiresAt — at most 30 days out and never past this key's own expiry. Allowed because the person just completed their own step-up on the site and enrolled the passkey: that is their consent, and the audit row names them (the subject) as the consenting human. \"silent\" is refused (policy_refused). Omit for approval mode (a human is asked per signature).",
                    "properties": {
                      "mode": {
                        "type": "string",
                        "enum": [
                          "session",
                          "approval"
                        ]
                      },
                      "expiresAt": {
                        "type": "string",
                        "description": "ISO-8601. Required for \"session\"."
                      }
                    },
                    "required": [
                      "mode"
                    ],
                    "additionalProperties": false
                  },
                  "autoConfirm": {
                    "type": "boolean",
                    "description": "Default false. When true the page script confirms the enrolment itself the moment it hands the site the credential, without anybody checking whether the site accepted it. Prefer looking, then passkey_complete_enrol."
                  }
                },
                "required": [
                  "domain"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/enrol/{ticket}": {
      "get": {
        "operationId": "passkey_enrol_status",
        "summary": "Where an enrol ticket is (the server's view)",
        "description": "USE THIS WHEN you have minted an enrol ticket and want to know what became of it without reading the page: `unused` (no page has presented it — drive the page to its add-a-passkey button), `created` (the credential exists and the site was handed it — LOOK at the page and then passkey_complete_enrol), `confirmed`, `withdrawn` (with the reason), `expired`, or `unknown` (not this connection's, or this service restarted). Once created it also carries the new connector as passkey_list will show it. Costs nothing, wakes nobody.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "enrol"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "enrol",
        "x-tool": "passkey_enrol_status",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "ticket",
            "in": "path",
            "required": true,
            "description": "The ticket UUID passkey_begin_enrol returned.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-read-only": true
      }
    },
    "/v1/enrol/complete": {
      "post": {
        "operationId": "passkey_complete_enrol",
        "summary": "Report whether the site accepted the new passkey",
        "description": "USE THIS WHEN an enrol ticket is at `created` and you have LOOKED at the page. `ok: true` when the site now lists the passkey (it usually shows a name and a date on its security page, or a success toast): the ticket closes `confirmed` and the connector is live. `ok: false`, with `detail` saying what you saw, when the site refused it or the flow broke: the credential is REVOKED so the person is not left with a passkey the site never stored, and the ticket closes `withdrawn`. Always call one or the other; a ticket left at `created` is a credential of uncertain worth that only the owner can tidy up.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "enrol"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "enrol",
        "x-tool": "passkey_complete_enrol",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ticket": {
                    "type": "string",
                    "description": "The ticket UUID passkey_begin_enrol returned. Either this or credentialId."
                  },
                  "credentialId": {
                    "type": "string",
                    "description": "THE EXTENSION'S SHAPE: the base64url credentialId a direct enrolment returned, instead of a ticket."
                  },
                  "ok": {
                    "type": "boolean",
                    "description": "true = the site accepted the passkey; false = it did not, revoke it."
                  },
                  "detail": {
                    "type": "string",
                    "description": "Optional, up to 512 characters. What you saw, for the record on the ticket."
                  },
                  "rpResponse": {
                    "type": "string",
                    "description": "Optional, accepted and not stored: the relying party's own echo."
                  }
                },
                "required": [
                  "ok"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/credentials": {
      "post": {
        "operationId": "passkey_enrol_direct",
        "summary": "Enrol a passkey and get the attestation bundle back (no page script)",
        "description": "USE THIS WHEN you drive the WebAuthn ceremony yourself — a browser tool with its own virtual authenticator, a page whose CSP blocks the shim's fetch, or a client that is not a browser at all — and can build the PublicKeyCredential the site expects from raw parts. Prefer passkey_begin_enrol: it does the same thing with nothing crossing your context. This returns the attestation bundle directly: credentialId, attestationObject, authData, publicKeyDer (all base64url), transports [\"internal\"], credProps {rk: true}. Build the site's clientDataJSON in the page ({type:\"webauthn.create\", challenge, origin, crossOrigin:false}) and hand the site the pieces. The credential is real the moment this returns: added to the person's account and to this connection. If the site then refuses it, passkey_revoke it.\n\nTHE ONE THING THIS SERVICE CANNOT DO: sign the person in to the site the first time. A site only offers \"add a passkey\" to an account that is ALREADY signed in, and this service holds no password. So before you mint anything, the person must be signed in to the site in the browser you drive — they do that themselves (a password, a magic link, whatever the site uses), or you ask them to. If you are at a login wall for a site with no connector, that is the situation: ask the human to sign in, then come back here.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nNo key material crosses this boundary. The passkey never leaves the service: you send the bytes the site asked to have signed and get a signature back. There is no argument to any tool here that could carry a private key, and no result that could return one.",
        "tags": [
          "credentials"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "enrol",
        "x-tool": "passkey_enrol_direct",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "The rpId — the registrable domain of the page."
                  },
                  "origin": {
                    "type": "string",
                    "description": "The exact origin of the page the ceremony runs on: scheme and host, no path, no trailing slash. Must be under `domain`."
                  },
                  "userName": {
                    "type": "string",
                    "description": "The account name as the site shows it — user.name from the creation options."
                  },
                  "userHandle": {
                    "type": "string",
                    "description": "Optional, base64url. The site's user.id from the creation options. Send it when the site gave one: the site hands it back at sign-in and expects to recognise it."
                  },
                  "signinUrl": {
                    "type": "string",
                    "description": "Optional. Where the next agent should go to SIGN IN with this passkey (not the \"add a passkey\" page). https:// only. Stored on the connector."
                  },
                  "displayLabel": {
                    "type": "string",
                    "description": "Optional. What the OWNER calls this account — \"Work Google\", \"Elisha (ID)\". Shown in place of userName."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional, up to 2000 characters. How to GET IN next time: where the passkey option hides on the sign-in page. Prose, never selectors."
                  },
                  "siteNotes": {
                    "type": "string",
                    "description": "Optional, up to 2000 characters. How to WORK THE SITE once in — what a snapshot costs, which controls lie about having worked. Prose, never selectors."
                  },
                  "subjectId": {
                    "type": "string",
                    "description": "Optional. The subject (your end customer) this passkey is for — the uuid from passkey_subject_create. The credential is then bound to them: every later sign-in must name the same subject, and revoking the subject revokes the passkey."
                  },
                  "policy": {
                    "type": "object",
                    "description": "Optional. {mode: \"session\", expiresAt} makes the new passkey sign WITHOUT asking anybody until expiresAt — at most 30 days out and never past this key's own expiry. Allowed because the person just completed their own step-up on the site and enrolled the passkey: that is their consent, and the audit row names them (the subject) as the consenting human. \"silent\" is refused (policy_refused). Omit for approval mode (a human is asked per signature).",
                    "properties": {
                      "mode": {
                        "type": "string",
                        "enum": [
                          "session",
                          "approval"
                        ]
                      },
                      "expiresAt": {
                        "type": "string",
                        "description": "ISO-8601. Required for \"session\"."
                      }
                    },
                    "required": [
                      "mode"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "domain",
                  "origin",
                  "userName"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/credentials/{id}": {
      "get": {
        "operationId": "passkey_credential_status",
        "summary": "One passkey: status, policy, subject, last use",
        "description": "USE THIS WHEN you hold a credentialId and want to know whether it is active or revoked, which subject it is bound to, which mode governs it (session until when, or approval), when it last signed, and whether this connection covers it. Any passkey of this account can be asked about; anything else is not_found. Costs nothing, wakes nobody.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "credentials"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_credential_status",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "base64url, as passkey_list spells it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-read-only": true
      }
    },
    "/v1/credentials/{id}/revoke": {
      "post": {
        "operationId": "passkey_revoke",
        "summary": "Revoke a passkey (scope credentials:write)",
        "description": "USE THIS WHEN the person asks for a passkey to be removed, or a site refused one you enrolled with passkey_enrol_direct. It revokes the credential — the service will never sign with it again, and it drops out of passkey_list on the next call. It cannot remove the passkey from the SITE's side; tell the person to delete it there too if they care. Needs `credentials:write`. Any passkey of this account can be named by credentialId; anything else is `not_found`, exactly like one that does not exist. Idempotent: one already revoked answers ok with `already: true`. Not reversible.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "credentials"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "credentials:write",
        "x-tool": "passkey_revoke",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The credentialId from passkey_list, base64url. Either this or `domain`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "A connected domain, when this connection holds exactly one credential for it. Ambiguous → the refusal lists the credentialIds."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/subjects": {
      "post": {
        "operationId": "passkey_subject_create",
        "summary": "Record a subject: the person a passkey is enrolled for",
        "description": "USE THIS WHEN you act for MANY people — a relay enrolling a passkey for each of its own customers — before enrolling for one of them. A subject is that person: your own id for them (`externalId`), a label, and the consent they gave (the text they were shown, when, and hashes of the address and browser — the raw values are never stored). Pass the returned subjectId to passkey_begin_enrol / passkey_enrol_direct; the passkey is then bound to them, every sign-in with it must name them, and revoking them revokes it.\n\nIdempotent on externalId: calling again returns the existing subject unchanged. Costs nothing, wakes nobody.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "subjects"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "enrol",
        "x-tool": "passkey_subject_create",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "externalId": {
                    "type": "string",
                    "description": "Your own id for this person, up to 256 characters. Unique per account."
                  },
                  "displayLabel": {
                    "type": "string",
                    "description": "Optional. A label for the dashboard."
                  },
                  "consent": {
                    "type": "object",
                    "description": "Optional. What the person consented to and when.",
                    "properties": {
                      "text": {
                        "type": "string",
                        "description": "The consent text they were shown, up to 4000 characters."
                      },
                      "at": {
                        "type": "string",
                        "description": "Optional ISO-8601. Defaults to now."
                      },
                      "ip": {
                        "type": "string",
                        "description": "Optional. Stored only as sha256."
                      },
                      "userAgent": {
                        "type": "string",
                        "description": "Optional. Stored only as sha256."
                      }
                    },
                    "required": [
                      "text"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "externalId"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      },
      "get": {
        "operationId": "passkey_subject_list",
        "summary": "The subjects this account has",
        "description": "USE THIS WHEN you need to find a subject's id, or to see which people this account holds passkeys for. Lists this account's subjects (yours only; there is no cross-account read). Costs nothing, wakes nobody.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.\n\nThis GET route carries only its path parameters; the tool's `includeRevoked` argument(s) are not available here. Use POST /mcp (tools/call) to pass them.",
        "tags": [
          "subjects"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_subject_list",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "x-read-only": true
      }
    },
    "/v1/subjects/{id}": {
      "get": {
        "operationId": "passkey_subject_status",
        "summary": "One subject, with their passkeys",
        "description": "USE THIS WHEN you hold a subjectId (or your externalId) and want the subject's record and every passkey enrolled for them, active or revoked. A subject that is not this account's answers not_found, exactly like one that does not exist.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "subjects"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "connectors:read",
        "x-tool": "passkey_subject_status",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The subject uuid, or your externalId.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-read-only": true
      }
    },
    "/v1/subjects/{id}/revoke": {
      "post": {
        "operationId": "passkey_subject_revoke",
        "summary": "Revoke a subject and every passkey enrolled for them",
        "description": "USE THIS WHEN a person withdraws — the customer declined to keep the passkey, or asked to be forgotten. The subject is marked revoked and EVERY passkey enrolled for them is revoked in the same transaction; this service will never sign for them again. Idempotent. Not reversible. It cannot remove the passkeys from the sites' side.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "subjects"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": "credentials:write",
        "x-tool": "passkey_subject_revoke",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The subject uuid, or your externalId.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {},
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/keys/rotate": {
      "post": {
        "operationId": "passkey_key_rotate",
        "summary": "Rotate this API key (API keys only)",
        "description": "USE THIS WHEN this API key is about to expire (a key.expiring webhook arrived, or passkey_check_access shows the expiry near) or it may have leaked. Mints a NEW key for the same grant — same scopes, same accounts, the grant's remaining lifetime — and puts the key you called with on a 24-hour grace, after which only the new one works. The new key is returned ONCE. Needs no scope: the key itself is the authority. An OAuth connection cannot use this; it refreshes its token instead.\n\nREADING THE RESULT: every result here carries `state` (ok | pending | denied | needs_authorization | needs_refresh | error), `retryable`, `terminal`, and `next` — the literal next call to make, or null. Act on those fields, not on the HTTP status and not on `isError`. `retryable: false` means this exact call will never succeed, however you reword it.",
        "tags": [
          "keys"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "x-scope": null,
        "x-tool": "passkey_key_rotate",
        "responses": {
          "200": {
            "description": "`state: \"ok\"` — it worked; read the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "202": {
            "description": "`state: \"pending\"` — a human is being asked; wait `next.waitMs` and call again unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "400": {
            "description": "`state: \"error\"` — a mistake in the call (`code` says which). Not retryable as sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "401": {
            "description": "`state: \"needs_refresh\"` — the access token is expired or revoked. `WWW-Authenticate` carries the challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "403": {
            "description": "`state: \"denied\"` (a decision on the merits, terminal) or `state: \"needs_authorization\"` (re-run OAuth requesting `next.scope`; `WWW-Authenticate: … error=\"insufficient_scope\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "404": {
            "description": "`state: \"error\"` with a not-found code (`not_found`, `ticket_unknown`, `ticket_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          },
          "429": {
            "description": "`state: \"error\"` with `code: \"too_many_tickets\"` or `code: \"rate_limited\"`; `retryable: true`, honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolResult"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {},
                "additionalProperties": false
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key (`apk_key_…`), an OAuth 2.1 access token minted for this resource, or an automation token (`apk_auto_…`). A 401 carries `WWW-Authenticate` with the protected-resource metadata URL; a 403 with `error=\"insufficient_scope\"` names the scope to request."
      }
    },
    "schemas": {
      "ToolResult": {
        "type": "object",
        "description": "The one result shape every tool and every route returns. Control flow lives in `state` / `retryable` / `terminal` / `next`, not in the HTTP status: the status is a courtesy for scripts, the body is the contract.",
        "required": [
          "state",
          "retryable",
          "terminal",
          "next"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "ok",
              "pending",
              "denied",
              "needs_authorization",
              "needs_refresh",
              "error"
            ],
            "description": "What happened."
          },
          "retryable": {
            "type": "boolean",
            "description": "May this exact call be made again, ever."
          },
          "terminal": {
            "type": "boolean",
            "description": "Is this question over."
          },
          "next": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/NextCall"
              },
              {
                "type": "null"
              }
            ],
            "description": "The literal next call to make, or null."
          },
          "code": {
            "type": "string",
            "description": "A machine-readable reason, on every non-ok result."
          },
          "because": {
            "type": "string",
            "description": "The reason in one sentence, on every non-ok result."
          }
        },
        "additionalProperties": true
      },
      "NextCall": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "tool",
              "authorize",
              "refresh_token",
              "ask_human",
              "browser"
            ]
          },
          "tool": {
            "type": "string",
            "description": "For kind: tool — the tool name, spelled exactly."
          },
          "arguments": {
            "type": "object",
            "additionalProperties": true,
            "description": "For kind: tool — the arguments to send."
          },
          "waitMs": {
            "type": "integer",
            "minimum": 0,
            "description": "Wait at least this long before making the call."
          },
          "scope": {
            "type": "string",
            "description": "For kind: authorize — the scope string to request."
          },
          "why": {
            "type": "string"
          }
        },
        "additionalProperties": true
      }
    },
    "x-scopes": {
      "connectors:read": "The `connectors:read` scope.",
      "signin": "The `signin` scope.",
      "approvals:read": "The `approvals:read` scope.",
      "audit:read": "The `audit:read` scope.",
      "enrol": "The `enrol` scope.",
      "credentials:write": "The `credentials:write` scope."
    }
  },
  "security": [
    {
      "bearer": []
    }
  ]
}