Every /v1/* route: scope, parameters, a curl call and the 200, 202 and 403 it answers with.
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.
| Scope | Unlocks |
|---|---|
| none | GET /v1/access, GET /v1/openapi.json |
connectors:read | The catalogue, one connector's status, notes, automations |
signin | Tickets, the page script, the fallback signature, runs |
approvals:read | The open approvals of this connection, and one by id |
audit:read | This connection's own audit rows |
enrol | Adding a passkey by API, its status, confirming or withdrawing it; creating and reading subjects |
credentials:write | Revoking 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.
| Status | state | Meaning |
|---|---|---|
| 200 | ok | Done. Read the payload. |
| 202 | pending | A human is being asked, on their phone, right now. Wait next.waitMs, then poll the route next names. |
| 403 | denied | A decision on the merits — human, policy or origin said no. Terminal. Do not retry. |
403 + WWW-Authenticate: Bearer error="insufficient_scope" | needs_authorization | The key was never granted this scope. Mint one that has it; retrying changes nothing. |
| 401 | — | The key is expired, revoked or unknown. |
| 400 | error | A mistake in the call. because says which. |
| 404 | error | not_found, ticket_unknown, ticket_expired. |
| 429 | error | too_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.
| Param | In | Meaning |
|---|---|---|
subjectId | query, optional | Only 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.
| Param | In | Meaning |
|---|---|---|
domain | path | The 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.
| Param | In | Meaning |
|---|---|---|
domain | path | The connected domain the note is about. |
kind | body | "login" — how to get in. "site" — how to work the site once in. |
note | body | One 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).
| Param | In | Meaning |
|---|---|---|
credentialId | body | From /v1/connectors, base64url. Checked against the live grant. |
expectAccount | body, optional | Which 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. |
subjectId | body, required for a subject-bound credential | The subject the credential belongs to. Omitted or wrong → 403 subject_mismatch. |
runId | body, optional | An open automation run, so the signature is audited under it. |
pageUrl | body, optional | The 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.
phase | Meaning |
|---|---|
unused | No page has presented it yet — drive the page to the passkey prompt. |
in_flight | A page is presenting it right now. |
pending_approval | A human is being asked (202). The page re-presents the ticket on its own. |
used | Its one signature was produced. Watch the site for the navigation. |
denied | Terminal (403), with code and because. |
expired / unknown | 404. 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.
| Param | In | Meaning |
|---|---|---|
credentialId | body | base64url. |
clientDataJSON | body | base64url, byte-for-byte as the page built it. One differing byte produces a signature that verifies against nothing. |
origin | body | The page's origin — scheme and host, no path. Must share a registrable domain with an enrolled origin. |
waitUpToMs | body, optional | Let 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. |
subjectId | body, required for a subject-bound credential | As for /v1/signin/begin. |
expectAccount, runId | body, optional | As 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.
| Param | In | Meaning |
|---|---|---|
id | path | The approvalId. |
waitUpToMs | query, optional | Hold 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.
| Param | In | Meaning |
|---|---|---|
domain | body | The relying party, e.g. github.com. rpId is accepted as a synonym — the extension's own field name. |
origin | body, optional | The page origin the ceremony will run on. Checked against domain; recorded as the enrolled origin. |
signinUrl | body, optional | Where a later sign-in should start. |
userName | body, optional | The account name to record; the site's own user.name wins if the page supplies one. |
userHandle | body, optional | base64url user.id, if you already know it. |
displayLabel | body, optional | How the passkey is labelled on the dashboard. |
subjectId | body, optional | Bind the credential to a subject. Every later sign-in must then name it. |
policy | body, 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 / siteNotes | body, optional | Operating 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.
| Param | In | Meaning |
|---|---|---|
ticket | body | From /v1/enrol/begin. credentialId is accepted in its place, for a caller that kept the credential and not the ticket. |
ok | body | true if the site accepted the passkey. |
detail | body, optional | What 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.
| Param | In | Meaning |
|---|---|---|
domain | body | The relying party id. |
origin | body | The page origin the ceremony runs on. Checked against domain. |
userName | body | The account name. |
userHandle | body, optional | base64url user.id from the site's options, if it gave one. |
signinUrl, displayLabel, notes, siteNotes | body, optional | As 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.
| Param | In | Meaning |
|---|---|---|
id | path | The 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.
| Param | In | Meaning |
|---|---|---|
id | path | The 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.
| Param | In | Meaning |
|---|---|---|
externalId | body | Your identifier for the customer. Unique per key owner. |
displayLabel | body, optional | How the dashboard names them. |
consent.text | body | The consent wording the person saw. |
consent.at | body, optional | ISO-8601. Defaults to now. |
consent.ip, consent.userAgent | body, optional | Stored 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/subjectsGET /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/automationsPOST /v1/automations — connectors:read
Propose one. Nothing runs until a human activates it on the dashboard; the result carries the activateUrl to send them.
| Param | In | Meaning |
|---|---|---|
name | body | A short name. |
instructions | body | What to do once signed in, in prose. |
credentialIds | body | The connectors it uses; all must be in this grant. |
trigger | body | { "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.
| Param | In | Meaning |
|---|---|---|
automationId | body | From /v1/automations or a run.due event. |
runId | body, optional | Claim the specific scheduled run a run.due event named. |
trigger | body, optional | schedule / 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.
| Param | In | Meaning |
|---|---|---|
id | path | The runId. |
status | body | succeeded / failed / needs_human. |
summary | body, optional | One 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.
| Param | In | Meaning |
|---|---|---|
limit | query, optional | 1 to 200. Default 50. |
before | query, optional | ISO-8601. Only rows strictly older — the paging cursor. |
subjectId | query, optional | Only rows about one subject's credentials. |
credentialId | query, optional | Only 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.