Every /v1/* route: scope, parameters, a curl call and the 200, 202 and 403 it answers with.

View as Markdown

REST API reference

Every tool on the remote MCP server is also a plain-HTTP route under https://app.solospace.dev/v1/. This page is the route table, Stripe-style: one section per endpoint, the scope it needs, its parameters, a curl call and what comes back. The machine-readable version is `GET /v1/openapi.json` — same routes, same shapes, no bearer needed to fetch it.

The facade is not a second API. Each route authenticates the bearer exactly as /mcp does, calls the same tool with the same arguments, and returns the very object an MCP client gets in structuredContent. Nothing a key can do here is something it could not do over /mcp.

Authentication

Authorization: Bearer apk_key_…

The bearer is an API key minted on the dashboard, or an automation token, or an OAuth access token a connected agent already holds. All three are the same kind of grant: the accounts it may touch, the scopes it holds and its expiry were fixed when a human minted it, and no argument to any route can widen them.

ScopeUnlocks
noneGET /v1/access, GET /v1/openapi.json
connectors:readThe catalogue, one connector's status, notes, automations
signinTickets, the page script, the fallback signature, runs
approvals:readThe open approvals of this connection, and one by id
audit:readThis connection's own audit rows
enrolAdding a passkey by API, its status, confirming or withdrawing it; creating and reading subjects
credentials:writeRevoking passkeys and subjects this key manages

Coverage. A key minted with enrol has coverEnrolled on by default: every credential it enrols is automatically covered by the key, so an integrator does not mint a new key per customer. Turn it off on the API-key form to keep a key's accounts fixed to what was ticked.

Conventions

The envelope. Every response body — success, pending, refusal, mistake — carries the same four fields first, then the payload:

{
  "state": "ok | pending | denied | error",
  "retryable": false,
  "terminal": true,
  "next": { "kind": "tool", "tool": "…", "arguments": {}, "waitMs": 0, "why": "…" },
  "code": "a_stable_token",
  "because": "One or two sentences for the transcript a human will read."
}

retryable and terminal are derived from state on the server; they never disagree with it. next is the literal next move — a tool name and arguments an agent can call as written, an authorize step naming the scope, or ask_human — or null when there is genuinely nothing to do.

Status codes follow state. The status is a courtesy for a script that reads only the status line; the body is the contract.

StatusstateMeaning
200okDone. Read the payload.
202pendingA human is being asked, on their phone, right now. Wait next.waitMs, then poll the route next names.
403deniedA decision on the merits — human, policy or origin said no. Terminal. Do not retry.
403 + WWW-Authenticate: Bearer error="insufficient_scope"needs_authorizationThe key was never granted this scope. Mint one that has it; retrying changes nothing.
401—The key is expired, revoked or unknown.
400errorA mistake in the call. because says which.
404errornot_found, ticket_unknown, ticket_expired.
429errortoo_many_tickets — the per-connection ceremony cap.

Bodies. A POST body is the tool's arguments as one JSON object; a path parameter is laid over it, so /v1/runs/<id>/end cannot be told about a different run in its body. GET routes take their arguments from the path; where a GET's tool has optional arguments (audit paging, an approval wait) they go in the query string.

Tickets are idempotent handles. POST /v1/signin/begin and POST /v1/enrol/begin each mint a single-use ticket good for a few minutes. Polling the ticket's GET is free and safe to repeat; presenting the ticket a second time after it is used or created does nothing new. Minting a fresh ticket for the same credential while one is in_flight raises a *second* question to the same human — poll the one you have.

Byte-identical polls. On 202, the next call must carry exactly the arguments the first one did. A changed origin, a re-encoded clientDataJSON or a different credential is a new question, and pushes another notification at the same person.

CORS and caching. Every route answers cache-control: no-store and the same CORS headers as /mcp; OPTIONS /v1/* is unauthenticated.

Access

GET /v1/access — no scope

What this key may do right now: the scopes it holds and the ones it lacks, which routes are usable, which sites are connected, and when the token expires. Signs nothing, wakes nobody.

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/access
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "client": "nightly-report",
  "scopes": { "held": ["connectors:read", "signin"], "missing": [{ "scope": "enrol", "unlocks": "…" }] },
  "tools": [{ "name": "passkey_begin_signin", "usable": true, "why": "…" }],
  "expiresAt": "2026-10-16T09:00:00.000Z" }

Connectors

GET /v1/connectors — connectors:read

The catalogue, narrowed to this grant. Each connector carries notes first — where the passkey option hides on that site — then rpId, credentialId, userName, signinUrl, origins, originAdvice, mode, lastUsedAt and, for a credential enrolled for a subject, subject: { id, externalId, displayLabel }. A site that is missing was not granted.

ParamInMeaning
subjectIdquery, optionalOnly the connectors of one subject.
curl -sS -H "Authorization: Bearer $APK_KEY" "$APK/v1/connectors?subjectId=sub_…"
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "grant": { "accounts": ["github.com as octocat"], "scopes": ["connectors:read", "signin"], "expiresAt": "…" },
  "connectors": [{ "rpId": "github.com", "credentialId": "b64url…", "userName": "octocat",
                   "signinUrl": "https://github.com/login", "mode": "approval",
                   "notes": "…", "notesSource": "connector", "lastUsedAt": "…" }],
  "count": 1 }

GET /v1/connectors/:domain — connectors:read

What the service has recorded about one connector: its mode, its notes, its last successful signature, and whether a signature is waiting on a human right now. It cannot see your browser.

ParamInMeaning
domainpathThe domain exactly as /v1/connectors spells it.
curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/connectors/github.com
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "domain": "github.com", "connector": { "…": "…" }, "confidence": "recorded",
  "lastSignedAt": "…", "waitingForApproval": false, "pendingApprovals": [] }

A domain outside the grant answers 404 with the domains that are inside it.

POST /v1/connectors/:domain/notes — connectors:read

Write back what you learned about a site. Stored as provisional, attributed to this connection, shown to the owner. It gates nothing.

ParamInMeaning
domainpathThe connected domain the note is about.
kindbody"login" — how to get in. "site" — how to work the site once in.
notebodyOne or two sentences of prose. No selectors, no secrets.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"kind":"login","note":"The passkey option is behind Try another way."}' \
     $APK/v1/connectors/github.com/notes
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "noteId": "…", "domain": "github.com", "kind": "login", "status": "provisional", "truncated": false }

Sign-in

POST /v1/signin/begin — signin

Start here. Mints a single-use ceremony ticket for one credential and returns armUrl: navigate a browser that has the Agent Passkey extension to it, and the extension answers that tab's next matching passkey prompt from its background worker — on any site, strict CSP included. The result also carries shim, a page script for a browser without the extension, which works where the site's CSP allows the page to reach this service (shimProcedure says whether it does).

ParamInMeaning
credentialIdbodyFrom /v1/connectors, base64url. Checked against the live grant.
expectAccountbody, optionalWhich account you mean, when the grant holds more than one. The ticket is refused at mint and at redemption unless the credential belongs to it.
subjectIdbody, required for a subject-bound credentialThe subject the credential belongs to. Omitted or wrong → 403 subject_mismatch.
runIdbody, optionalAn open automation run, so the signature is audited under it.
pageUrlbody, optionalThe https page showing the passkey prompt now; armUrl returns there. Must be on the connector's site.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"credentialId":"<from /v1/connectors>","subjectId":"sub_…"}' $APK/v1/signin/begin
{ "state": "ok", "retryable": false, "terminal": true,
  "next": { "kind": "browser", "action": "navigate", "url": "https://app.solospace.dev/arm#ticket=5f0c…&to=https%3A%2F%2Fgithub.com%2Flogin",
            "why": "Navigate to armUrl, then click the passkey button on github.com. …" },
  "ticket": "5f0c…-…", "status": "unused", "expiresAt": "2026-09-16T17:37:09.000Z",
  "armUrl": "https://app.solospace.dev/arm#ticket=5f0c…&to=https%3A%2F%2Fgithub.com%2Flogin",
  "site": "github.com", "account": "octo@example.com", "aliases": ["octocat"], "mode": "approval",
  "signinUrl": "https://github.com/login",
  "ticketPath": { "likely": false, "why": "connect-src does not allow this service." },
  "ifNotArmed": { "kind": "ask_human", "say": "Agent Passkey cannot press the passkey prompt on github.com for me in this browser. …" } }

403 denied with code: "policy_denied", origin_rejected or subject_mismatch is terminal. 429 too_many_tickets means poll the ticket you already have.

GET /v1/signin/:ticket — signin

The server's view of one ticket. Free, safe to repeat.

phaseMeaning
unusedNo page has presented it yet — drive the page to the passkey prompt.
in_flightA page is presenting it right now.
pending_approvalA human is being asked (202). The page re-presents the ticket on its own.
usedIts one signature was produced. Watch the site for the navigation.
deniedTerminal (403), with code and because.
expired / unknown404. Mint another.
curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/signin/5f0c…
{ "state": "pending", "retryable": true, "terminal": false,
  "next": { "kind": "tool", "tool": "passkey_signin_status", "arguments": { "ticket": "5f0c…" }, "waitMs": 25000, "why": "A human is being asked." },
  "code": "waiting_for_approval", "ticket": "5f0c…", "status": "pending_approval",
  "approvalId": "…", "expiresAt": "…",
  "history": { "attempt": 1, "expiresAt": "…", "prior": [] } }

POST /v1/signin/complete — signin

The fallback, for a page whose CSP blocks it from reaching the service itself. The caller ferries clientDataJSON in and authData / signature / userHandle out.

ParamInMeaning
credentialIdbodybase64url.
clientDataJSONbodybase64url, byte-for-byte as the page built it. One differing byte produces a signature that verifies against nothing.
originbodyThe page's origin — scheme and host, no path. Must share a registrable domain with an enrolled origin.
waitUpToMsbody, optionalLet the server hold the call open for the human's tap instead of polling. Clamped; the result says waitedMs. Not part of the approval binding.
subjectIdbody, required for a subject-bound credentialAs for /v1/signin/begin.
expectAccount, runIdbody, optionalAs above.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"credentialId":"…","clientDataJSON":"eyJ0eXBl…","origin":"https://github.com","waitUpToMs":25000}' \
     $APK/v1/signin/complete
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "credentialId": "…", "authData": "b64url…", "signature": "b64url…", "userHandle": "b64url…",
  "flags": { "up": true, "uv": true }, "approvedBy": "human" }

202 carries approvalId and code: "waiting_for_approval" — resend the identical body. 403 carries one of approval_denied, approval_timeout, policy_denied, origin_rejected, subject_mismatch, argument_mismatch (you changed the arguments while a human was being asked).

Approvals

GET /v1/approvals — approvals:read

The approvals this connection has raised that are still open, each with its deadline. Approves nothing, hurries nobody.

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/approvals
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "approvals": [{ "approvalId": "…", "state": "pending", "rpId": "github.com",
                  "origin": "https://github.com", "createdAt": "…", "expiresAt": "…" }],
  "count": 1 }

GET /v1/approvals/:id — approvals:read

One approval, by the id a pending result carried, with its full history (every earlier prompt and how it ended). Use it instead of re-sending the sign-in call, which would notify the person again.

ParamInMeaning
idpathThe approvalId.
waitUpToMsquery, optionalHold the call open until a decision or the wait runs out.
curl -sS -H "Authorization: Bearer $APK_KEY" "$APK/v1/approvals/<id>?waitUpToMs=25000"

200 approved or 403 approval_denied / approval_timeout; 202 while the human is still being asked. An approval that is not this connection's answers 404.

Enrolment

The full procedure, the flow diagram and the limits are on Adding passkeys via the API. The routes:

POST /v1/enrol/begin — enrol

Mints an enrolment ticket and the page script that patches navigator.credentials.create(). The human must already be signed in to the site in the browser you drive; this service cannot do that step.

ParamInMeaning
domainbodyThe relying party, e.g. github.com. rpId is accepted as a synonym — the extension's own field name.
originbody, optionalThe page origin the ceremony will run on. Checked against domain; recorded as the enrolled origin.
signinUrlbody, optionalWhere a later sign-in should start.
userNamebody, optionalThe account name to record; the site's own user.name wins if the page supplies one.
userHandlebody, optionalbase64url user.id, if you already know it.
displayLabelbody, optionalHow the passkey is labelled on the dashboard.
subjectIdbody, optionalBind the credential to a subject. Every later sign-in must then name it.
policybody, optional{ "mode": "session", "expiresAt": "<ISO, at most 30 days out>" } for unattended signing within a bounded window. Omitted → approval mode. "silent" → 400 policy_refused; an unbounded or longer window → 400 policy_refused.
notes / siteNotesbody, optionalOperating notes to store with the new connector.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"domain":"github.com","signinUrl":"https://github.com/login","userName":"octocat"}' \
     $APK/v1/enrol/begin
{ "state": "ok", "retryable": false, "terminal": true,
  "next": { "kind": "browser", "why": "Evaluate shim on the site's add-a-passkey page, then click its Add passkey button." },
  "ticket": "9a41…", "expiresAt": "2026-09-16T17:42:00.000Z", "rpId": "github.com",
  "shim": "(()=>{ /* patches navigator.credentials.create */ })()" }

GET /v1/enrol/:ticket — enrol

Where the enrolment ticket is: unused, created (the page received an attestation), confirmed, withdrawn, expired. Carries credentialId and the new connector once created.

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/enrol/9a41…
{ "state": "ok", "retryable": false, "terminal": true,
  "next": { "kind": "tool", "tool": "passkey_complete_enrol", "arguments": { "ticket": "9a41…", "ok": true }, "why": "Tell the service whether the site accepted it." },
  "status": "created", "credentialId": "b64url…",
  "connector": { "rpId": "github.com", "userName": "octocat", "mode": "approval" } }

POST /v1/enrol/complete — enrol

The site's verdict, which the service cannot see. ok: true confirms the connector; ok: false withdraws it, and the credential is revoked in the same transaction.

ParamInMeaning
ticketbodyFrom /v1/enrol/begin. credentialId is accepted in its place, for a caller that kept the credential and not the ticket.
okbodytrue if the site accepted the passkey.
detailbody, optionalWhat the site said, in one sentence. Audited, never interpreted.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"ticket":"9a41…","ok":true}' $APK/v1/enrol/complete
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "status": "confirmed", "credentialId": "b64url…" }

POST /v1/credentials — enrol

The direct path, for a caller that runs the registration ceremony itself and only needs the attestation. It returns what navigator.credentials.create() would have: the caller hands it to the site.

ParamInMeaning
domainbodyThe relying party id.
originbodyThe page origin the ceremony runs on. Checked against domain.
userNamebodyThe account name.
userHandlebody, optionalbase64url user.id from the site's options, if it gave one.
signinUrl, displayLabel, notes, siteNotesbody, optionalAs for /v1/enrol/begin.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"domain":"github.com","origin":"https://github.com","userName":"octocat"}' \
     $APK/v1/credentials
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "credentialId": "b64url…", "attestationObject": "b64url…", "authData": "b64url…",
  "publicKeyDer": "b64url…", "transports": ["internal"], "credProps": { "rk": true } }

GET /v1/credentials/:id — connectors:read

One passkey, in full: who it belongs to, where it may sign, how it got here and what policy governs it. enrolledVia is extension or api; status is active or revoked.

ParamInMeaning
idpathThe credentialId, base64url.
curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/credentials/<credentialId>
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "credentialId": "b64url…", "rpId": "github.com", "origins": ["https://github.com"],
  "userName": "octocat", "displayLabel": "GitHub · octocat",
  "subject": { "id": "sub_…", "externalId": "cust_8812", "displayLabel": "Acme Ltd" },
  "status": "active", "enrolledVia": "api", "enrolledAt": "…", "lastUsedAt": "…",
  "policy": { "mode": "session", "expiresAt": "…", "rateLimitPerHr": 60 } }

A credential outside this grant — or another tenant's — answers 404, never 403: the id's existence is not confirmed.

POST /v1/credentials/:id/revoke — credentials:write

Revokes one passkey. Every grant that covered it loses it at once, and its audit rows stay. Idempotent: a second call answers 200 with already: true.

ParamInMeaning
idpathThe credentialId, base64url.
curl -sS -X POST -H "Authorization: Bearer $APK_KEY" $APK/v1/credentials/<credentialId>/revoke
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "credentialId": "b64url…", "revoked": true, "already": false }

A credential this key does not manage, or another tenant's, answers 404. Revoking is the one destructive act in the API, which is why it has its own scope and why neither enrol nor credentials:write is granted by default.

Subjects

A subject is an end customer of *your* product — a tenant-owned identity that owns passkeys you enrol on their behalf. Subjects exist so a relay that signs many people in can keep their credentials apart, name which one it means on every call, and revoke one customer without touching the rest. A credential bound to a subject can only be signed with that subject named.

POST /v1/subjects — enrol

Create a subject, or return the existing one with the same externalId (idempotent). The consent block records that the person agreed to this: the text they saw, when, and — hashed at rest, never readable back — the IP and user agent they agreed from.

ParamInMeaning
externalIdbodyYour identifier for the customer. Unique per key owner.
displayLabelbody, optionalHow the dashboard names them.
consent.textbodyThe consent wording the person saw.
consent.atbody, optionalISO-8601. Defaults to now.
consent.ip, consent.userAgentbody, optionalStored as hashes.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"externalId":"cust_8812","displayLabel":"Acme Ltd",
          "consent":{"text":"Let Acme Relay sign in to GitHub on my behalf.","ip":"203.0.113.9"}}' \
     $APK/v1/subjects
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "subjectId": "sub_…", "externalId": "cust_8812", "displayLabel": "Acme Ltd",
  "status": "active", "createdAt": "…", "existed": false }

GET /v1/subjects — enrol

Every subject this key owner has, newest first, each with its status and credential count.

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/subjects

GET /v1/subjects/:id — enrol

One subject, with its credentials in the same shape GET /v1/credentials/:id returns.

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/subjects/sub_…
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "subjectId": "sub_…", "externalId": "cust_8812", "displayLabel": "Acme Ltd", "status": "active",
  "consent": { "text": "…", "at": "…" },
  "credentials": [{ "credentialId": "…", "rpId": "github.com", "userName": "octocat", "displayLabel": null, "status": "active", "enrolledAt": "…" }] }

POST /v1/subjects/:id/revoke — credentials:write

Revokes the subject and every passkey it owns in one transaction. Idempotent. The customer is gone from this service; their audit rows stay.

curl -sS -X POST -H "Authorization: Bearer $APK_KEY" $APK/v1/subjects/sub_…/revoke
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "subjectId": "sub_…", "revoked": true, "credentialsRevoked": 2, "already": false }

Keys

POST /v1/keys/rotate — the key itself

Mints a replacement for the bearer that made the call — same grant, same accounts, same scopes, a fresh expiry — and starts a 24-hour grace in which the old key keeps working. Swap the secret in your deployment inside that window. The new key is shown once, here.

curl -sS -X POST -H "Authorization: Bearer $APK_KEY" $APK/v1/keys/rotate
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "key": "apk_key_…", "prefix": "apk_key_7f3c", "expiresAt": "2026-10-16T09:00:00.000Z",
  "previousExpiresAt": "2026-09-17T09:12:00.000Z" }

A key that is about to expire says so twice on your webhook — key.expiring seven days and one day before — so rotation can be scheduled rather than discovered as a 401.

Automations

GET /v1/automations — connectors:read

Every automation of this account with its status (proposed / active / paused), trigger, next due time and last run.

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/automations

POST /v1/automations — connectors:read

Propose one. Nothing runs until a human activates it on the dashboard; the result carries the activateUrl to send them.

ParamInMeaning
namebodyA short name.
instructionsbodyWhat to do once signed in, in prose.
credentialIdsbodyThe connectors it uses; all must be in this grant.
triggerbody{ "kind": "schedule", "cron": "0 9 * * 1-5", "tz": "Asia/Jerusalem" }, { "kind": "webhook" } or { "kind": "manual" }.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"name":"Morning bounces","instructions":"Open the bounces report and post the count.","credentialIds":["…"],"trigger":{"kind":"manual"}}' \
     $APK/v1/automations
{ "state": "ok", "retryable": false, "terminal": true,
  "next": { "kind": "ask_human", "why": "Only the person can activate it." },
  "automationId": "…", "status": "proposed", "name": "Morning bounces",
  "activateUrl": "https://app.solospace.dev/dashboard/automations/…" }

Runs

POST /v1/runs — signin

Begin a run. One run at a time per automation; a second call returns the open run rather than starting another.

ParamInMeaning
automationIdbodyFrom /v1/automations or a run.due event.
runIdbody, optionalClaim the specific scheduled run a run.due event named.
triggerbody, optionalschedule / webhook / manual. Defaults to manual.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"automationId":"…"}' $APK/v1/runs
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "runId": "…", "automationId": "…", "name": "Morning bounces", "resumed": false,
  "startedAt": "…", "instructions": "…", "connectors": [ { "…": "…" } ] }

An automation that is not active answers 403 denied.

GET /v1/runs/:id — signin

One run in any state: running, succeeded, failed, needs_human, missed, abandoned — with its trigger, timestamps, summary and the requestId its signatures were audited under.

curl -sS -H "Authorization: Bearer $APK_KEY" $APK/v1/runs/<runId>

POST /v1/runs/:id/end — signin

End a run. Always call it: a run left open is marked abandoned after 30 minutes and blocks the next one.

ParamInMeaning
idpathThe runId.
statusbodysucceeded / failed / needs_human.
summarybody, optionalOne or two sentences for the person. Ids and outcomes, never page content.
curl -sS -H "Authorization: Bearer $APK_KEY" -H "content-type: application/json" \
     -d '{"status":"succeeded","summary":"Posted 3 bounces to #ops."}' $APK/v1/runs/<runId>/end
{ "state": "ok", "retryable": false, "terminal": true, "next": null,
  "runId": "…", "status": "succeeded", "finishedAt": "…" }

Audit

GET /v1/audit — audit:read

This connection's own audit rows, newest first: every signature it asked for, the verdict, the reason phrase, the site, the origin, the credential and the requestId. Other agents and the owner's own activity are not readable from here, and no argument widens it.

ParamInMeaning
limitquery, optional1 to 200. Default 50.
beforequery, optionalISO-8601. Only rows strictly older — the paging cursor.
subjectIdquery, optionalOnly rows about one subject's credentials.
credentialIdquery, optionalOnly rows about one passkey.
curl -sS -H "Authorization: Bearer $APK_KEY" "$APK/v1/audit?limit=50"
{ "state": "ok", "retryable": false, "terminal": true,
  "next": { "kind": "tool", "tool": "passkey_audit", "arguments": { "limit": 50, "before": "…" }, "why": "There may be older rows." },
  "entries": [{ "at": "2026-09-16T17:32:09.000Z", "action": "assert", "verdict": "allowed",
                "reason": "approved by human", "rpId": "google.com",
                "origin": "https://accounts.google.com", "requestId": "…", "credentialId": "…" }],
  "count": 50, "nextBefore": "…" }

Not on this surface

passkey_signin (the one-call sign-in) exists only on the local stdio server, which has a browser; there is no route for it. passkey_howto is the documentation.