{"openapi":"3.1.0","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"Haven Agent Payment API","version":"0.1.0","summary":"Machine-readable contract for Haven agent payments.","description":"Haven is non-custodial smart account software. These endpoints let authenticated agents create payment intents, fetch payment state, and relay independently signed payment payloads. Haven never receives the agent delegate private key and never treats an API key as payment authority."},"servers":[{"url":"https://havenbackend-production-8a00.up.railway.app","description":"This deployment, as reached by your request"},{"url":"http://localhost:3001","description":"Local development backend"}],"tags":[{"name":"Health"},{"name":"Agents"},{"name":"Organizations"},{"name":"Connect Agent 2"},{"name":"Payments"},{"name":"x402","description":"Agent-side x402 payment authorization for independently signed payment payloads. Haven never receives the agent delegate private key and never treats an API key as payment authority."},{"name":"Machine payments"},{"name":"Transactions"},{"name":"Delegations"},{"name":"SubBudgets","description":"Sub-budgets (#3330): a budget one agent re-delegates to another agent in the same account, as an ERC-7710 child of the delegating agent's own budget delegation. Payments authorized through one redeem the three-link chain [grant, parent child, budget]."},{"name":"Webhooks","description":"Inbound provider callbacks (#3019). Authenticated by a per-connection capability URL token plus the provider HMAC signature — never a session."},{"name":"Ops","description":"The internal ops console (#3509, epic #3507): founders-only, read-only, GitHub sign-in. Every route answers 404 on a deployment that does not configure it. Nothing here moves funds, signs, or acts as a user."}],"paths":{"/openapi.json":{"get":{"tags":["Health"],"operationId":"getOpenApiSpec","summary":"Fetch this OpenAPI document.","security":[],"responses":{"200":{"description":"OpenAPI 3.1 document for the Haven Agent Payment API.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/":{"get":{"tags":["Health"],"operationId":"getApiRoot","summary":"What this service is, and where its machine-readable contract lives.","description":"Unauthenticated root document (#2530). An agent handed only a backend URL had nothing to read and had to guess the spec path. Deliberately thin and non-sensitive: names, paths, and which credential each door wants — no version or build identifier, which would fingerprint the deployment and buy an agent nothing.","security":[],"responses":{"200":{"description":"The API root document.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiRootDocument"}}}}}}},"/discovery":{"get":{"tags":["Health"],"operationId":"getDiscovery","summary":"Public, read-only facts an agent client needs to configure itself.","description":"The environment as data (#2531): which connector package this deployment hands out, which hosted MCP it points at, which chains it serves, and where its spec is. Every value is already public elsewhere — this route re-serves them together so the frontend capability manifest does not restate the backend's env logic. Never per-user or per-agent data, no relayer address, nothing from /health.","security":[],"responses":{"200":{"description":"Public deployment facts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoveryDocument"}}}}}}},"/auth/device/start":{"post":{"tags":["Auth"],"operationId":"startDeviceAuthorization","summary":"Begin a browser-approved CLI login (#2526).","description":"RFC 8628-shaped device authorization. Unauthenticated: this is where a CLI begins, so that an agent driving it never has to hold its user's password. Returns a user code the human types at `verification_url` and a device code the client polls with. Both are stored hashed; the grant expires in 10 minutes.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_label":{"type":"string","description":"What the client calls itself, shown on the approval screen. Free text from an unauthenticated caller: TRUNCATED to 80 characters and stripped of control characters server-side (never refused for length — a long hostname must not fail `haven login`, #3030), and rendered as text, never as markup."}},"additionalProperties":false}}}},"responses":{"201":{"description":"A pending grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceAuthorizationStart"}}}}}}},"/auth/device/lookup":{"post":{"tags":["Auth"],"operationId":"lookupDeviceAuthorization","summary":"Read what a pending CLI login is asking for, before deciding.","description":"Requires an ordinary owner session, like `approve`, and is likewise absent from the owner-CLI allow-list. It exists so the approval screen can show the requester's own `client_label` BEFORE the button rather than after it: every code looks alike, so the label is the only thing that lets a human notice they are being asked to approve somebody else's login. The label is attacker-controlled text — bounded and stripped of control characters on the way in, and rendered as text, never as markup, on the way out. Wrong, expired and already-decided codes all answer 404 alike, so this is not an enumeration oracle.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_code"],"properties":{"user_code":{"type":"string","description":"As shown to the human; case and dashes are ignored."}},"additionalProperties":false}}}},"responses":{"200":{"description":"The pending grant's label and expiry — deliberately nothing else.","content":{"application/json":{"schema":{"type":"object","properties":{"client_label":{"type":"string","nullable":true},"expires_at":{"type":"string","format":"date-time"}}}}}},"404":{"description":"No pending approval for that code."}}}},"/auth/device/approve":{"post":{"tags":["Auth"],"operationId":"approveDeviceAuthorization","summary":"Approve or deny a pending CLI login (dashboard session only).","description":"Requires an ordinary owner session. An `owner_cli` token is deliberately NOT accepted here — a CLI session approving further CLI sessions would turn one human approval into an unbounded grant. A wrong, expired or already-decided code all answer 404 alike, so codes cannot be enumerated by a signed-in caller.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_code"],"properties":{"user_code":{"type":"string","description":"As shown to the human; case and dashes are ignored."},"deny":{"type":"boolean","description":"Deny instead of approving."}},"additionalProperties":false}}}},"responses":{"200":{"description":"The grant's new state."},"404":{"description":"No pending approval for that code."}}}},"/auth/device/token":{"post":{"tags":["Auth"],"operationId":"redeemDeviceAuthorization","summary":"Poll for the session token once a human has approved.","description":"Unauthenticated by design: the device code IS the credential. Answers `authorization_pending` until approved, then the session token exactly once — redemption is a single-use claim, so a poll loop that fires twice mints one session, not two. The token carries `purpose: owner_cli`, which every authenticated route refuses unless it is on the owner-CLI allow-list (`middleware/owner-cli.ts`): agents, connect setups and read-only account context — never signer changes, re-keying, credentials, provisioning, transfers, or delegation build/activate/revoke. The human keeps every signature.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["device_code"],"properties":{"device_code":{"type":"string"}},"additionalProperties":false}}}},"responses":{"200":{"description":"The owner-CLI session token."},"400":{"description":"authorization_pending | slow_down | expired_token | access_denied. An unknown code and an expired one answer alike, so one cannot be used to probe the other."}}}},"/health":{"get":{"tags":["Health"],"operationId":"getHealth","summary":"Check public backend and database health.","security":[],"responses":{"200":{"description":"Backend is healthy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"Backend is reachable but degraded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/health/ops":{"get":{"tags":["Health"],"operationId":"getOperationsHealth","summary":"Read operator-only backend diagnostics.","description":"Requires the deployment-configured `HAVEN_OPS_TOKEN` in `X-Haven-Ops-Token`. Returns 404 when the token is not configured, so deployments do not expose diagnostics by default.","security":[],"parameters":[{"name":"X-Haven-Ops-Token","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operator diagnostics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthOpsResponse"}}}},"401":{"description":"The operator token is missing or invalid.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Operator diagnostics are not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/auth/github/start":{"get":{"tags":["Ops"],"operationId":"startOpsSignIn","summary":"Begin an ops console sign-in with GitHub.","description":"Redirects the browser to GitHub (no scopes requested) with a signed, 10-minute `state` carrying `return_to` and `nonce`. `return_to` must exactly equal one of the deployment's `OPS_REDIRECT_ORIGINS` (scheme, host and port; no prefix, suffix or wildcard matching). Returns 404 when the ops console is not configured on this deployment.","security":[],"parameters":[{"name":"return_to","in":"query","required":true,"schema":{"type":"string","maxLength":2048},"description":"The ops app origin to return to."},{"name":"nonce","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$"},"description":"The ops app's nonce, echoed back in the redirect fragment."}],"responses":{"302":{"description":"Redirect to GitHub's authorize page."},"400":{"description":"`return_to` is not an allowed ops origin.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console is not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/auth/github/callback":{"get":{"tags":["Ops"],"operationId":"finishOpsSignIn","summary":"Finish an ops console sign-in (GitHub redirects here).","description":"Verifies the `state` this backend issued, re-checks its origin against `OPS_REDIRECT_ORIGINS`, exchanges the code, reads the GitHub user and discards GitHub's token. An allowlisted numeric GitHub id (with 2FA, when GitHub reports it) is redirected to `<origin>/#token=<ops token>&nonce=<nonce>`; every other outcome to `<origin>/#error=<code>&nonce=<nonce>` (`not_allowed`, `two_factor_required`, `github_denied`, `github_unavailable`, `missing_code`). Every sign-in that reaches a GitHub identity (allowed, `not_allowed` or `two_factor_required`) is audited first; a failed audit write answers 503 and issues nothing. `github_denied`, `github_unavailable` and `missing_code` have no identity to record.","security":[],"parameters":[{"name":"code","in":"query","schema":{"type":"string","maxLength":512},"description":"Authorization code from GitHub."},{"name":"state","in":"query","schema":{"type":"string","maxLength":4096},"description":"The signed state `start` issued."},{"name":"error","in":"query","schema":{"type":"string","maxLength":256},"description":"Present when the user declined."},{"name":"error_description","in":"query","schema":{"type":"string","maxLength":2048}},{"name":"error_uri","in":"query","schema":{"type":"string","maxLength":2048}}],"responses":{"302":{"description":"Redirect back to the ops app with a token or an error code in the fragment."},"400":{"description":"The state is missing, forged, expired, or names an origin no longer allowed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console is not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The sign-in could not be audited, so nothing was issued.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/me":{"get":{"tags":["Ops"],"operationId":"getOpsSession","summary":"Read the signed-in ops operator.","security":[{"OpsJwt":[]}],"responses":{"200":{"description":"The operator the ops token belongs to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsSession"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console is not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/overview":{"get":{"tags":["Ops"],"operationId":"getOpsOverview","summary":"Platform-wide counts for the ops console.","description":"Counts only: users, smart accounts per chain split by account_type (retired legacy_safe rows stay visible apart), agents by status, active agent delegations, and payment intents and payment refusals in the last 24 h. Reads through the read-only ops database role and writes one audit row before answering; a failed audit write answers 503 with nothing returned. Returns 404 while the deployment has no read-only ops database configured.","security":[{"OpsJwt":[]}],"responses":{"200":{"description":"The counts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsOverview"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console (or its read-only database) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The read could not be audited, so nothing was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/sponsored-gas":{"get":{"tags":["Ops"],"operationId":"getOpsSponsoredGas","summary":"Sponsored UserOp gas cost per merchant per day.","description":"What the delegation-rail gas sponsorship costs (#3837): every UserOp submitted through the payment path, tagged direct vs x402 funding leg, bucketed per UTC day — x402 funding legs by the merchant host from the joined payment intent (null when the intent has no resource URL), direct payments as their own null-host bucket. Gas is the EntryPoint receipt actualGasCost (includes preVerificationGas; the l1Fee is not added) and is priced at VIEW time — eth_priced_at says so. Value moved is the confirmed intents' usd_value. Reads through the read-only ops database role and writes one audit row before answering.","security":[{"OpsJwt":[]}],"responses":{"200":{"description":"The per-merchant-per-day buckets for the trailing window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsSponsoredGas"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console (or its read-only database) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The read could not be audited, so nothing was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/feedback":{"get":{"tags":["Ops"],"operationId":"getOpsFeedback","summary":"The last 7 days of CLI feedback, masked, one page.","description":"The console’s Feedback page (#3602): messages sent with `haven feedback submit` (#3597), newest first, capped at 50. Only unexpired rows are listed (every feedback read filters expires_at > now(); retention is 7 days). The message text is masked to a character count — no content — and the submitter’s email is masked; the unmasked text leaves only through POST /ops/reveal (target_type feedback, field text), audited. Reads through the read-only ops database role and writes one audit row before answering; a failed audit write answers 503 with nothing returned. Returns 404 while the deployment has no read-only ops database configured.","security":[{"OpsJwt":[]}],"responses":{"200":{"description":"The masked feedback page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsFeedbackList"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console (or its read-only database) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The read could not be audited, so nothing was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/search":{"get":{"tags":["Ops"],"operationId":"searchOps","summary":"Find customer records by a pasted id, address, tx hash or email prefix.","description":"Detects the key type: a UUID matches users, agents and payment intents; an address (0x + 40 hex) matches smart accounts and agent delegate addresses; a tx hash (0x + 64 hex) matches payment intents and, as a typed system_tx hit with no user link, outbound system transactions; anything else is a case-insensitive email prefix of at least 3 characters. At most 20 hits per lookup. The lookups run one after another inside a 5 s total budget; lookups that did not complete are listed in timed_out and the hits found so far are returned. Emails are masked. The audit row stores the key type and the masked term, never the raw query. Returns 404 while the deployment has no read-only ops database configured.","security":[{"OpsJwt":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":320},"description":"What to look up: a UUID, an address, a tx hash, or an email prefix."}],"responses":{"200":{"description":"The hits, and any lookups that ran out of time.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsSearchResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console (or its read-only database) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The search could not be audited, so nothing was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/users/{id}":{"get":{"tags":["Ops"],"operationId":"getOpsUser","summary":"One customer's record, masked, for support.","description":"The user (email and name masked), their smart accounts per chain with account_type, their agents with status, their agents' active delegations (budget shape, recipient pin, window), and their last 50 payment intents and last 50 payment refusals. Payment-intent error messages are stored redacted of vendor secrets. Company details, machine metadata, hashes, signatures and delegation bodies are never returned. Reads through the read-only ops database role and writes one audit row before answering. Returns 404 for an unknown user, or while the deployment has no read-only ops database configured.","security":[{"OpsJwt":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The masked record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsUserDetail"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No such user, or the ops console (or its read-only database) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The read could not be audited, so nothing was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/users/{id}/onchain":{"get":{"tags":["Ops"],"operationId":"getOpsUserOnchain","summary":"The chain's view of one customer's accounts, next to the database's.","description":"Per smart account: whether code exists on-chain (deployed vs counterfactual, best-effort — a lagging RPC node can report a deployed account as having no code), and per ACTIVE stored delegation whether the DelegationManager reports it disabled on-chain and the period enforcer's remaining budget. A failed budget read renders 'unavailable', never a remaining figure. Accounts that are not delegation-rail rows (legacy_safe), on a chain this environment does not serve, or on a chain without pinned delegation contracts are listed with their reason and are NEVER read on-chain — zero RPC calls for them. Addresses are masked; delegation hashes and bodies are never returned. Reads through the read-only ops database role; the chain reads are batched per chain and cached per (chain, user) for 60 s with single-flight. Writes one audit row before answering. The signer set is DB-only in v1 (no on-chain signer-set reader exists). Returns 404 for an unknown user, while the deployment has no read-only ops database or no chain readers configured.","security":[{"OpsJwt":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The masked DB-vs-chain view.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsOnchainView"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No such user, or the ops console (its read-only database or its chain readers) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The read could not be audited, so nothing was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/health":{"get":{"tags":["Ops"],"operationId":"getOpsSystemHealth","summary":"List the operational problems: sweepable intents, stuck revocations, stuck lanes, delegate balances.","description":"The ops console’s system-health read (#3514). ERC-7710 payment intents the settlement sweeper works on (in its retry window, and past its horizon — the payments actually lost without an operator, 24 h to 30 days), confirmed payments whose evidence row never landed (#2213), revocations and re-anchors unreconciled past 1 h, broadcast outbound transactions unmined past the bump worker’s stale threshold per served chain, the delegate balance monitor’s last report, and the same payload GET /health/ops serves (built by the same function). READ-ONLY about the operational world: no RPC call on request, no claim, no receipt read — a listed lane MAY already be mined. Delegate balances are the monitor’s in-memory last report, never a scan on request; a replica without the monitor leader lock answers not_available_on_this_replica. Atomic amounts are decimal strings. Every list is capped at 50. Reads through the read-only ops database role and writes one audit row before answering; a failed audit write answers 503 with nothing returned. Returns 404 while the deployment has no read-only ops database or no diagnostics builder configured.","security":[{"OpsJwt":[]}],"responses":{"200":{"description":"The operational problems, and the backend’s own diagnostics.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsSystemHealth"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The ops console (its read-only database or its diagnostics builder) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The read could not be audited, so nothing was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/ops/reveal":{"post":{"tags":["Ops"],"operationId":"revealOpsField","summary":"Reveal one masked field of one record, audited.","description":"Accepts only a closed set of `(target_type, field)` pairs and reads through the read-only ops database role. Every reveal writes an audit row before the value is returned; a failed audit write answers 503 with no value. Returns 404 while the deployment has no read-only ops database configured.","security":[{"OpsJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsRevealRequest"}}}},"responses":{"200":{"description":"The unmasked value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpsRevealResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No such record, or the ops console (or its read-only database) is not configured.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The reveal could not be audited, so no value was returned.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/organizations":{"get":{"tags":["Organizations"],"operationId":"listOrganizations","summary":"List the signed-in user's organizations.","description":"Flat rows with parent ids; the caller builds the tree. Multiple roots are allowed (one per company); a null parent is the top level. `agent_count` is the number of agents filed DIRECTLY under the folder — sub-organization members are not counted.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The user’s organizations, name-sorted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationListResponse"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Organizations"],"operationId":"createOrganization","summary":"Create an organization, optionally inside another one.","description":"The name must be unique among siblings under the same parent (case-insensitively); a repeat is a 409. Omit `parent_organization_id` to create a top-level organization.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrganizationRequest"}}}},"responses":{"201":{"description":"The created organization. A new folder has no members yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/organizations/{id}":{"put":{"tags":["Organizations"],"operationId":"updateOrganization","summary":"Rename an organization and/or move it inside another one.","description":"Rename changes the name everywhere it renders, never which agents file under it. The move is expressed by `parent_organization_id`: present means move (null = the top level), absent means keep. Moving into the organization’s own subtree is refused with 400 — it would make the folder its own ancestor. A rename or move onto a sibling name that already holds is a 409.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrganizationRequest"}}}},"responses":{"200":{"description":"The updated organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["Organizations"],"operationId":"deleteOrganization","summary":"Delete an organization; its contents move up one level.","description":"Deleting never orphans anything: the folder’s sub-organizations and member agents take the deleted folder’s own parent (agents of a deleted root return to the top level). Agents are never deleted, hidden, or changed in any way beyond the placement. The response is `{ ok: true }`.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"description":"Deleted. The contents were promoted one level up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteOrganizationResponse"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/company-details":{"get":{"tags":["User"],"operationId":"getCompanyDetails","summary":"Read the signed-in owner's company details.","description":"Behind `HAVEN_OWNER_COMPANY_DETAILS` — 404 when the feature is off, and ONLY then. When the feature is on and the owner has never saved details (or deleted them), 200 with a JSON `null` body, so a client can tell \"feature off\" from \"nothing saved\" by status alone. A row stuck `pending` for longer than a few minutes (a crash between the write and its VIES check completing, or a genuine DB failure recording the check's result) is re-checked asynchronously (an atomic claim, so concurrent reads start at most one check) as a side effect of this read; the response still reflects the row as read, `pending` included, not the re-check's eventual outcome.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The owner's saved company details, or `null` when none are saved.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CompanyDetails"},{"type":"null"}]}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"put":{"tags":["User"],"operationId":"putCompanyDetails","summary":"Create or replace the company details.","description":"Full replacement, not a patch. Behind `HAVEN_OWNER_COMPANY_DETAILS` — 404 when the feature is off. Setting a `vat_number` that is new or different from the stored one moves `vies_status` to `pending` and starts a VIES check asynchronously; the response returns before that check completes. Clearing `vat_number` (omit or null) clears both `vies_status` and `vies_checked_at`. Rate-limited per session credential (`ownerProfileRateLimit`).","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCompanyDetailsRequest"}}}},"responses":{"200":{"description":"The saved company details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDetails"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Rate limited: 20/min per session credential (`ownerProfileRateLimit`, #3332 review M3). The count is shared with every other rate-limited route the same credential calls.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["User"],"operationId":"deleteCompanyDetails","summary":"Delete the company details.","description":"The owner's erasure path — deliberately NOT gated by `HAVEN_OWNER_COMPANY_DETAILS` (every other route on this and the vies-check path answers 404 when the feature is off; this one does not), so an owner can always remove details they saved while the feature was on, even after an operator turns it back off. Deleting the Haven account itself would also remove these details (the table's `user_id` foreign key is `ON DELETE CASCADE`), but account deletion is an operator action today — there is no self-serve delete-my-account route — so this is the only erasure path that exists. The response is `{ ok: true }` whether or not a row existed.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Deleted (or there was nothing to delete).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteCompanyDetailsResponse"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/company-details/vies-check":{"post":{"tags":["User"],"operationId":"recheckCompanyDetailsVies","summary":"Re-run the VIES check for the saved VAT number.","description":"Behind `HAVEN_OWNER_COMPANY_DETAILS` — 404 when the feature is off, and 404 when no details (or no VAT number) are saved. Moves `vies_status` to `pending` (and clears `vies_checked_at`, since a check now in flight has no completion time yet) and starts a fresh check asynchronously; the response reflects `pending`, not the eventual outcome — poll `GET /user/company-details`. Rate-limited per session credential (`ownerProfileRateLimit`).","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The details, with `vies_status` now `pending`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDetails"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Rate limited: 20/min per session credential (`ownerProfileRateLimit`, #3332 review M3). The count is shared with every other rate-limited route the same credential calls.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents":{"get":{"tags":["Agents"],"operationId":"listAgents","summary":"List Haven agents for the signed-in user.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Agents owned by the user.","content":{"application/json":{"schema":{"type":"object","required":["agents"],"properties":{"agents":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}}},"additionalProperties":false}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Agents"],"operationId":"createAgent","summary":"Create a Haven agent identity and API key.","description":"Creates the API identity for an agent — identity and credential only. Payment authority arrives separately as an owner-signed budget delegation, enforced on-chain (#1440/#2020: the per-token allowance mirror is retired; the response’s `allowances` is always empty at creation).","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentRequest"}}}},"responses":{"201":{"description":"Agent created. The api_key is shown once and should be stored by the user or agent runtime.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}":{"get":{"tags":["Agents"],"operationId":"getAgent","summary":"Fetch one Haven agent.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Agent details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"put":{"tags":["Agents"],"operationId":"updateAgent","summary":"Rename an agent or change its description.","description":"Display metadata only — it changes no authority, no allowance and no key. The response carries the agent with its current allowances so a client can re-render without a second call.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Trimmed."},"description":{"type":"string","description":"Trimmed."},"organization_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"The organization to file the agent under; null = the top level."}}}}}},"responses":{"200":{"description":"The updated agent, with allowances.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["Agents"],"operationId":"deleteAgent","summary":"RETIRED — always answers 410. Archive instead.","description":"Deleting an agent is retired (#1401) and this route is a tombstone: **it always answers 410 and writes nothing.** Hard deletion failed outright on any agent with payment history (a foreign-key violation surfacing as a 500) and, where it did succeed, cascaded away seven tables of money-path audit trail. Removal is now an ARCHIVE that keeps the history: end the agent's budgets (POST /agents/{id}/delegations/revoke-all, owner-signed), revoke the agent, then POST /agents/{id}/archive. The typed route survives for reversibility, in the same spirit as the session-rail retirement.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"410":{"description":"Always. The message names the archive route to use instead.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/tax-declaration":{"put":{"tags":["Agents"],"operationId":"putAgentTaxDeclaration","summary":"Opt this agent in or out of the buyer-side x402 tax declaration.","description":"The OWNER's per-agent opt-in (#3426, wg-tax #5 §2.1). Opting IN is refused with a structured 409 unless the deployment has `HAVEN_OWNER_COMPANY_DETAILS` on and the owner's saved company details carry a VAT number whose VIES status is `valid` AT THE MOMENT of the write — the refusal names the reason (`feature_disabled`, `no_company_details` or `vies_not_valid`) and writes nothing. Opting OUT always succeeds: an owner whose VIES result dropped is exactly the owner who needs to withdraw the opt-in. The toggle changes only this agent's own opt-in bit — it grants no spending authority and sends nothing; the declaration itself is only ever carried on EIP-3009 payments (#3427). An agent API key is refused with a named 403: this is an owner-session surface, the same as the company-details routes.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertAgentTaxDeclarationRequest"}}}},"responses":{"200":{"description":"The new state of the opt-in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentTaxDeclarationState"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"The opt-in is not currently possible. The body carries `reason` (`feature_disabled`, `no_company_details` or `vies_not_valid`) and `available: false`; nothing was written.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"get":{"tags":["Agents"],"operationId":"getAgentTaxDeclaration","summary":"Read the unsigned buyer-side tax declaration content for this agent.","description":"The AGENT's own read (#3426, wg-tax #5 §2.1) — authenticated by this agent's API key. Answers 200 with the UNSIGNED §2.1 content when the deployment flag is on, this agent is opted in, and the owner's VAT number is VIES `valid` AT READ TIME: `version` (`x402-tax-1`), `jurisdiction` (the owner's saved country — never derived from the VAT number's prefix; a `GR…` VAT number with country `GR` declares `GR`, and Northern Ireland's `XI…` prefix declares the saved country), `taxableStatus` (`TAXABLE_PERSON`), `taxId` (the saved VAT number) and `validUntil` (integer MILLISECONDS, the lesser of now + 24h and the VIES check's completion time + 24h — the check's own freshness caps the declaration). Answers 200 with `{ available: false, reason }` otherwise — `feature_disabled` (flag off), `disabled` (this agent not opted in, or a foreign agent id), `no_company_details` (no VAT number saved) or `vies_not_valid` (VIES anything-but-valid right now, `pending` included). Availability is the body, not the status code: a temporarily unavailable declaration is a state to poll, not an error. The response is NEVER signed and carries no `signature`, `principalId` or `principalAttributionHash` — the SDK computes those locally (#3427) — and none of the company-details fields the declaration does not state. This slice sends nothing; the declaration is only ever carried on EIP-3009 payments (#3427).","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"The declaration content, or the structured not-available answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentTaxDeclarationContent"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegate-balance":{"get":{"tags":["Agents"],"operationId":"getDelegateBalance","summary":"Get on-chain USDC and ETH balance of the agent delegate EOA.","description":"Reads on-chain balances for the delegate EOA linked to this agent. Used by the dashboard to surface stranded funds and by the sweep flow to show exact amounts. Haven never holds the delegate key; this endpoint only reads balances from the chain.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Delegate balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DelegateBalance"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"422":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/archive":{"post":{"tags":["Agents"],"operationId":"archiveAgent","summary":"Archive an agent (soft removal — history is kept).","description":"Replaces agent deletion (#1401). Delegation agents require status=revoked and no live budget delegations (pending, active or replaced — anything revoke-all would still target) because archiving is a filing action and never the thing that stops spending. Linked legacy Safe records may be archived at any status; that only removes the Haven-side record and leaves the old Safe permission untouched. An agent whose Safe was already unlinked is archivable when no live delegation remains. The agent row and every dependent audit row (payments, approvals, evidence, delegations, passports) remain; the agent leaves the primary list. Idempotent: re-archiving keeps the original archived_at.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Archived (or already archived).","content":{"application/json":{"schema":{"type":"object","required":["success","archived_at"],"properties":{"success":{"type":"boolean"},"archived_at":{"type":"string","format":"date-time"}},"additionalProperties":false}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/unarchive":{"post":{"tags":["Agents"],"operationId":"unarchiveAgent","summary":"Return an archived agent to the primary list.","description":"Clears archived_at and nothing else — the agent keeps the status it had when archived. For delegation agents, the archive contract requires revoked status and no live budgets; legacy Safe records can return with their prior active, paused, pending_approval, or revoked status. Un-archiving restores no authority of any kind. Idempotent on a non-archived agent.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"No longer archived (or was not archived).","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}},"additionalProperties":false}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/revoke":{"post":{"tags":["Agents"],"operationId":"revokeAgent","summary":"Mark an agent as revoked in Haven.","description":"Blocks Haven API access for the agent. Revoking is permitted from `active`, `paused` and `pending_approval` — nothing re-activates a revoked agent, so the credential cannot return to life (on-chain revocation remains the authority boundary). Revoking also cancels the agent's open connection setup in the same transaction, so a connect flow that has not finished cannot approve a budget for an agent that no longer exists. The agent's `api_key_hash` is kept: sweep recovery for a stranded delegate balance stays available. Users can also revoke or change Safe module permissions outside Haven.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Agent revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"The agent exists and is owned by the caller but cannot be revoked: `error_code` `already_revoked` (nothing to do) or `not_revocable` (the status is outside the revocable set).","content":{"application/json":{"schema":{"type":"object","required":["error","error_code"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["already_revoked","not_revocable"]}},"additionalProperties":false}}}}}}},"/agents/{id}/labels":{"put":{"tags":["Agents"],"operationId":"replaceAgentLabels","summary":"Replace an agent's labels with the given set.","description":"Full replacement: the agent ends up carrying exactly the labels named, in any order, duplicates collapsed. Labels are the user's own display tags; deleting a label elsewhere removes it from every agent without touching the agents themselves. The response carries the agent's labels as they now are, so a client re-renders without a second call. Repeating an id is accepted and collapses to one (#3200) — the set is what the label_ids array names distinctly, and a duplicated id is not a missing label.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceAgentLabelsRequest"}}}},"responses":{"200":{"description":"The agent's labels after the replacement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentLabelsResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/labels":{"get":{"tags":["Agents"],"operationId":"listLabels","summary":"List the signed-in user's labels.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The label vocabulary, name-sorted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabelListResponse"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Agents"],"operationId":"createLabel","summary":"Create a label (or re-use one of the same name).","description":"Names are one per user on the lowercased name: creating \"Prod\" when \"prod\" exists re-uses that label rather than failing. An omitted colour keeps the existing label's colour (#3200) — the fold onto an existing row recolours only when the request explicitly names a colour; a genuinely new label takes the palette's neutral entry.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLabelRequest"}}}},"responses":{"201":{"description":"The label as it now exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Label"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/labels/{id}":{"put":{"tags":["Agents"],"operationId":"updateLabel","summary":"Rename and/or recolor a label.","description":"Agents carrying the label follow it — a rename changes the name everywhere it renders, never which agents carry it. A rename onto a name another of the user's labels already holds is a 409.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/LabelId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLabelRequest"}}}},"responses":{"200":{"description":"The label as it now is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Label"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["Agents"],"operationId":"deleteLabel","summary":"Delete a label.","description":"Removes the label and its assignments (every agent loses the tag). Agents are never deleted or altered by this — only the label and the join rows go.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/LabelId"}],"responses":{"200":{"description":"Deleted. The agents that carried it keep everything else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegations":{"get":{"tags":["Delegations"],"operationId":"listAgentDelegations","summary":"List an agent's budget delegations with lifecycle status.","description":"Every grant the agent has, newest first, including pending (built but not owner-signed), replaced and revoked rows — the dashboard renders exactly what is and isn't live (#802). Without the include parameter the response is the plain lifecycle list, byte-identical to the pre-#3693 shape. With include=remaining, ACTIVE rows additionally carry the on-chain remaining-this-period figure: remaining_atomic, remaining_from_chain and period_end (#3693); non-active rows carry null for all three. The route is polled every 10 s, so the chain reads behind those fields run only when asked for. The signed delegation object itself is deliberately NOT in the list: it is api_key_hash-class data returned only by the explicit flows that need it.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["remaining"]},"description":"Opt-in enrichment. remaining: each ACTIVE delegation carries the on-chain remaining-this-period fields (remaining_atomic, remaining_from_chain, period_end); non-active rows carry them as null. Anything outside the enum is refused with the 400 envelope before the handler runs."}],"responses":{"200":{"description":"Delegations ordered by created_at DESC.","content":{"application/json":{"schema":{"type":"object","required":["delegations"],"properties":{"delegations":{"type":"array","items":{"$ref":"#/components/schemas/Delegation"}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegations/build":{"post":{"tags":["Delegations"],"operationId":"buildAgentDelegation","summary":"Grant step 1: build an unsigned budget delegation for the owner to sign.","description":"Builds the EIP-712 typed data for a period-budget delegation (token, atomic budget, refill period, optional recipient pin, expiry — defaulting to 90 days) and stores it as a pending row. Nothing is signed and nothing moves: the OWNER signs signing_payload client-side (one signature, zero transactions) and then calls activate. A rebuilt (token, recipient) slot gets a fresh version so replacements never collide (#827) — EXCEPT an identical (token, recipient, budget, period, merchant) slot whose build is still pending and unexpired: that returns the SAME row (same delegation_hash and version) with nothing inserted, so a retried grant or the #2539 CLI handing off a signing link converges instead of minting a competitor the owner never sees. The response also carries build_id and typed_data_hash (both the delegation_hash, named for API clarity) and signing_url — the dashboard grant form with ?grant= prefill, whose host comes from FRONTEND_URL.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token_address","budget_atomic","period_seconds"],"properties":{"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"recipient_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Optional recipient pin. Omit (or null) for an open budget — unless merchant_slug is set, in which case the server pins it to the merchant's verified payTo."},"budget_atomic":{"type":"string","pattern":"^[0-9]+$","description":"Positive atomic token amount; must fit uint96 (the enforcer word size)."},"period_seconds":{"type":"integer","minimum":60,"description":"Native refill period; ≥ 60."},"expires_at":{"type":"integer","description":"Unix seconds, must be in the future. Default: now + 90 days."},"merchant_slug":{"type":["string","null"],"pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","description":"A merchant-locked budget (#3331): the server pins the recipient to this live merchant's verified payTo on the agent's chain and records the merchant on the row. recipient_address may be omitted; when sent it must equal that payTo (409 otherwise). 404 for an unknown or non-live merchant; 409 when the merchant has no verified payTo there, not every offer there advertises ERC-7710, or the payTo is one of this agent's own addresses (delegate key, delegate account or treasury)."}}}}}},"responses":{"201":{"description":"Pending delegation stored (or an identical still-pending build reused); the owner signs next.","content":{"application/json":{"schema":{"type":"object","required":["delegation_hash","version","delegate_account_address","signing_payload","build_id","typed_data_hash","signing_url"],"properties":{"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"version":{"type":"integer","description":"Fresh per (agent, token, recipient) slot — replacement identity (#827). Repeated UNCHANGED when build reuses an identical still-pending row (#2539)."},"delegate_account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"signing_payload":{"type":"object","description":"EIP-712 typed data (primaryType 'Delegation') the owner signs verbatim.","additionalProperties":true},"build_id":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The build identifier. It IS the delegation hash — there is no second id and no new column (#2539)."},"typed_data_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The hash of the EIP-712 typed data to sign — the delegation_hash, named for the issue’s response shape (#2539)."},"signing_url":{"type":"string","format":"uri","description":"Dashboard grant form for this agent with the pending build prefilled (?grant=<delegation hash>). Host from the backend’s FRONTEND_URL, the same source buildApprovalUrl uses (#2539)."}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Revoked agents cannot receive a new budget delegation; other delegation-account conflicts also return 409, as do the merchant-locked refusals (#3331: no verified payTo, not ERC-7710, the payTo is one of the agent’s own addresses, recipient_address differs from the current payTo).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegations/{hash}/activate":{"post":{"tags":["Delegations"],"operationId":"activateAgentDelegation","summary":"Grant step 2: attach the owner signature and make the budget live.","description":"Stores the owner's signature on the pending delegation and flips it active, marking any previously active grant in the same (token, recipient) slot replaced — atomically, so the slot never ends up with zero live grants mid-replacement. Deploys the counterfactual delegator account via the relayer first when needed (#860; permissionless factory call, no owner signature). Activating an agent's FIRST budget also activates a pending_approval agent — on this rail the grant signature IS the approval (#1069). Signature validation is a shape check only (65-byte ECDSA or longer WebAuthn assertion); the real validator is EIP-1271 at redemption.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"hash","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"description":"Delegation hash from the build/list response."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","pattern":"^0x(?:[0-9a-fA-F]{2}){65,}$"}}}}}},"responses":{"200":{"description":"Budget is live.","content":{"application/json":{"schema":{"type":"object","required":["activated","delegation_hash"],"properties":{"activated":{"type":"boolean"},"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Revoked agents cannot activate a new budget delegation; non-pending delegation and account conflicts also return 409. A row belonging to a re-key (`rekey_id` set) is refused with `error_code: \"REKEY_DELEGATION_NOT_ACTIVATABLE\"` — it is activated only by that re-key's own completion (#3439).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Relayer gas budget exhausted — retry later.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"500":{"description":"Stored owner config no longer derives the stored account address.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Account deploy failed; the grant stays pending and activate can be retried.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegations/{hash}/revoke":{"post":{"tags":["Delegations"],"operationId":"prepareDelegationRevocation","summary":"Revoke step 1: prepare the disableDelegation UserOp for the owner to sign.","description":"Prepares a sponsored treasury UserOp executing disableDelegation. The response branches on the signature scheme: an EOA owner signs EIP-712 typed data (signing_payload); a pure-passkey account signs the userOpHash via WebAuthn (user_op_hash). Multi-signer accounts pick per request with signature_scheme. A row already disabled on-chain is healed to revoked and answered 409 instead of preparing a doomed op (#1423).","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"hash","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"description":"Delegation hash from the build/list response."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"],"description":"Multi-signer accounts choose per request; omitted, an EOA owner defaults to eip712_userop."}}}}}},"responses":{"200":{"description":"Prepared revocation, shaped by the signature scheme.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["signature_scheme","signing_payload","user_operation","treasury_address","instructions","revocation_url"],"properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"signing_payload":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true},"treasury_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"revocation_url":{"type":"string","format":"uri"},"instructions":{"type":"string"}}},{"type":"object","required":["signature_scheme","user_op_hash","user_operation","treasury_address","instructions","revocation_url"],"properties":{"signature_scheme":{"type":"string","enum":["webauthn_userop"]},"user_op_hash":{"type":"string"},"user_operation":{"type":"object","additionalProperties":true},"treasury_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"revocation_url":{"type":"string","format":"uri"},"instructions":{"type":"string"}}}]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegations/{hash}/revoke/submit":{"post":{"tags":["Delegations"],"operationId":"submitDelegationRevocation","summary":"Revoke step 2: submit the signed UserOp; the row flips only after it lands.","description":"The submitted user_operation is BOUND to the delegation being revoked (#3343): its calldata must disable THAT delegation on the pinned DelegationManager (identity comparison, signature excluded), checked before submission. A userop that disables something else — or nothing — is refused 400 and nothing is recorded. On success the delegation is disabled on-chain and the row is marked revoked.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"hash","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"description":"Delegation hash from the build/list response."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature","user_operation"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"},"user_operation":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"Delegation disabled on-chain and marked revoked. The recorded revocation was verified against the signed calldata (#3343).","content":{"application/json":{"schema":{"type":"object","required":["revoked","tx_hash"],"properties":{"revoked":{"type":"boolean"},"tx_hash":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/task-budgets":{"get":{"tags":["Delegations"],"operationId":"listAgentTaskBudgets","summary":"List an agent's task budgets.","description":"Every task budget on this agent, newest first — the owner-facing twin of the agent-auth list at GET /task-budgets. Scoped by BOTH agent id and the caller's ownership of it. #3501: open rows carry spent_atomic and remaining_atomic, read live from the enforcer's spentMap for the child's delegation hash; a failed on-chain read reports null with remaining_is_from_chain: false — never the full cap as remaining.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Task budgets ordered by created_at DESC.","content":{"application/json":{"schema":{"type":"object","required":["task_budgets"],"properties":{"task_budgets":{"type":"array","items":{"$ref":"#/components/schemas/TaskBudget"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/sub-budgets":{"post":{"tags":["SubBudgets"],"operationId":"issueAgentSubBudget","summary":"Issue a sub-budget: agent A re-delegates a narrower budget to agent B.","description":"Owner-authorised two-party flow (#3330): the owner picks the sub-agent, amount, expiry and optional recipient pin; the route refuses a child WIDER than the parent budget in amount, expiry or recipient BEFORE signing (400 sub_budget_wider_than_parent), and refuses 409 sub_budget_exceeds_remaining when the slice plus already-open slices under the same parent would exceed the parent's remaining budget. Creates TWO pending rows: A's self-delegated parent-child and B's grant chained under it. Both are signed by A's delegate key agent-side: the agent lists its rows (GET /sub-budgets?status=awaiting_signature), fetches each sign-context, and submits each signature itself (POST /sub-budgets/{id}/submit; next_action 'agent_signs_then_submits'). The owner relay via POST /agents/{id}/sub-budgets/{sub}/sign still works but is optional. A sub_budget_id naming the delegating agent itself is refused: that is a task budget (#3329). Lifecycle gate (#3553), checked before the handler's body checks and writing nothing: 409 issuer_retired when the issuing agent is revoked or archived (including a half-revoked agent whose budget delegation is still live), and 409 sub_agent_retired when the receiving sub-agent is revoked, archived or pending_approval (only active or paused agents may receive); paused agents pass the gate. Opening a grant later is also refused (409 sub_agent_retired, row stays pending) if its receiving agent was retired after issuance, on both the owner relay and the agent submit.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sub_agent_id","period_amount_atomic","expires_at"],"properties":{"sub_agent_id":{"type":"string","format":"uuid","description":"The sub-agent B — a different agent in the same account."},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Defaults to the chain USDC when omitted."},"period_amount_atomic":{"type":"string","pattern":"^[0-9]+$","description":"Per-period atomic amount; must be <= the parent budget period amount (checked pre-sign)."},"expires_at":{"type":"integer","description":"Unix seconds; must be in the future and <= the parent budget expiry."},"recipient_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$","description":"Optional recipient pin; may only NARROW the parent pin, never widen it."},"label":{"type":["string","null"],"maxLength":120}},"additionalProperties":false}}}},"responses":{"201":{"description":"Both rows of the tree created pending.","content":{"application/json":{"schema":{"type":"object","required":["sub_budget","parent_child_sub_budget","next_action","sign_targets"],"properties":{"sub_budget":{"$ref":"#/components/schemas/SubBudget"},"parent_child_sub_budget":{"$ref":"#/components/schemas/SubBudget"},"next_action":{"type":"string"},"sign_targets":{"type":"array","items":{"type":"object","required":["sub_budget_id","who","what"],"properties":{"sub_budget_id":{"type":"string","format":"uuid"},"who":{"type":"string"},"what":{"type":"string"}},"additionalProperties":false}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"get":{"tags":["SubBudgets"],"operationId":"listAgentSubBudgets","summary":"List this agent's sub-budget rows, newest first.","description":"Every row of the trees THIS agent issued (its parent-child narrowings and the grants nested under them) — the owner-facing twin of the agent-auth list at GET /sub-budgets. Scoped by BOTH agent id and the caller's ownership of it.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Sub-budget rows ordered by created_at DESC.","content":{"application/json":{"schema":{"type":"object","required":["sub_budgets"],"properties":{"sub_budgets":{"type":"array","items":{"$ref":"#/components/schemas/SubBudget"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/sub-budgets/tree":{"get":{"tags":["SubBudgets"],"operationId":"getAgentSubBudgetTree","summary":"The parent→child sub-budget tree this agent issued.","description":"A's parent-child rows on top, each grant nested under the parent-child row its parent_sub_budget_id names (#3330 dashboard tree). Grants whose parent-child row is gone come back under `unattached` rather than being dropped.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"The tree.","content":{"application/json":{"schema":{"type":"object","required":["agent_id","trees","unattached"],"properties":{"agent_id":{"type":"string","format":"uuid"},"trees":{"type":"array","items":{"type":"object","required":["parent_child_sub_budget","grants"],"properties":{"parent_child_sub_budget":{"$ref":"#/components/schemas/SubBudget"},"grants":{"type":"array","items":{"$ref":"#/components/schemas/SubBudget"}}},"additionalProperties":false}},"unattached":{"type":"array","items":{"$ref":"#/components/schemas/SubBudget"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/sub-budgets/{sub}/sign":{"post":{"tags":["SubBudgets"],"operationId":"signAgentSubBudget","summary":"Relay the delegating agent's signature over one pending sub-budget child.","description":"OPTIONAL owner relay (#3330; since #3506 the agent submits its own signature via POST /sub-budgets/{id}/submit and this relay is not required): verifies the signature recovers A's OWN delegate key over the stored child typed data (recoverSubBudgetChildSigner), then flips the row open. Both rows of a tree are signed this way (one call per row). A signature by any other key answers 400 signature_mismatch. Refuses 409 issuer_retired (#3553) when the issuing agent is revoked or archived, leaving the pending row pending; paused agents pass. Also refuses 409 sub_agent_retired when the row is a grant whose receiving agent is revoked, archived or pending_approval; the row stays pending.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"sub","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$","description":"A's delegate-key signature over the row's sign-context typed data."}},"additionalProperties":false}}}},"responses":{"200":{"description":"The opened sub-budget.","content":{"application/json":{"schema":{"type":"object","required":["sub_budget","status"],"properties":{"sub_budget":{"$ref":"#/components/schemas/SubBudget"},"status":{"type":"string","enum":["open"]}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/sub-budgets/{sub}":{"delete":{"tags":["SubBudgets"],"operationId":"revokeAgentSubBudget","summary":"Revoke one sub-budget row (the parent-child, or a single grant).","description":"Authority-reducing only (#3329-2's rule): the delegating agent's own delegate account prepares disableDelegation of THIS row's child. Revoking a PARENT-CHILD row strands every grant under it (their chain's middle link dies — revoking A's budget delegation closes this row too, which is what makes B's child unredeemable); revoking a GRANT row ends B's slice and leaves A intact. status=pending or expired: closes immediately, nothing signed. status=open and live: returns the prepared close operation for the agent to sign (then relay via the agent close endpoint).","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"sub","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The closed row, or the prepared close awaiting the agent signature.","content":{"application/json":{"schema":{"type":"object","required":["sub_budget","status"],"properties":{"sub_budget":{"$ref":"#/components/schemas/SubBudget"},"status":{"type":"string","enum":["closed","closing"]},"next_action":{"type":["string","null"]}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/rekey":{"post":{"tags":["Delegations"],"operationId":"startAgentRekey","summary":"Re-key step 0: preflight — residual check, and open the re-key (#1698).","description":"Opens an owner-authorised re-key: same agent identity, new delegate key, new API key, old authority revoked. Delegation rail only — a legacy AllowanceModule account is refused 409 with re-onboarding named as the path. An agent API key is refused 403: an agent can never re-key itself. new_delegate_address is the PUBLIC address of a keypair generated on the target machine; Haven never receives the private half. Preflight reads the residual balance on the OLD delegate EOA and refuses 409 on a non-zero one until residual_disposition says what happened to it, because sweeping needs the old key's signature and after the rotation that balance is unrecoverable. A failed balance read is 503 rather than a shrug.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["new_delegate_address"],"properties":{"new_delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"PUBLIC address of the new delegate keypair, generated locally. Must differ from the current delegate and must not collide with another live agent."},"residual_disposition":{"type":"string","enum":["swept","acknowledged_unrecoverable"],"description":"Required only when the old delegate EOA holds a non-zero balance. \"swept\" after sweeping it with the old key; \"acknowledged_unrecoverable\" to proceed knowing the key is lost and the balance is written off."}}}}}},"responses":{"201":{"description":"Re-key opened at stage preflight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRekeyPreflight"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"An agent credential was presented — an agent can never re-key itself.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Not on the delegation rail, a re-key already in flight (the body names its rekey_id, stage and the new_delegate_address it is bound to, #1868), a colliding delegate address, or an undispositioned residual balance.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"The residual balance read failed; proceeding would retire the only key that could sweep it.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/rekey/{rekeyId}/revoke":{"post":{"tags":["Delegations"],"operationId":"prepareRekeyRevocation","summary":"Re-key step 1a: prepare the batched revoke of every live delegation (#1698).","description":"Revoke comes FIRST, always. If the revoke lands and the issue does not, the agent has no authority — recoverable, and the correct posture when a key is lost. The reverse ordering would leave two simultaneously live keys. The response branches on the signature scheme, exactly as the per-hash and batch delegation revokes do: an EOA owner signs EIP-712 typed data (signing_payload); a passkey signs the userOpHash via WebAuthn (user_op_hash). A multi-signer account (EOA owner AND enrolled passkeys) picks per request with signature_scheme — without it the server would infer the owner path and estimate verification gas for a 65-byte signature the device may not be able to produce (#1870). An agent with no live delegations short-circuits: nothing to revoke, so the re-key advances straight to the metered stage — inheriting an abandoned predecessor’s frozen carry when one qualifies (#1868), otherwise with an empty carry.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"rekeyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Re-key id from the preflight response."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"],"description":"Multi-signer accounts choose per request; omitted, an EOA owner defaults to eip712_userop."}}}}}},"responses":{"200":{"description":"Prepared revocation, shaped by the signature scheme — or the no-authority short-circuit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRekeyRevokePrepare"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Wrong stage — this re-key is past the revoke; the account signer configuration is unknown; or the requested signature_scheme is one this account cannot sign. Every one of these lands BEFORE the revoke, so the re-key stays retryable (#1868).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/rekey/{rekeyId}/revoke/submit":{"post":{"tags":["Delegations"],"operationId":"submitRekeyRevocation","summary":"Re-key steps 1b + 2: land the revoke, THEN read the now-frozen meter (#1698).","description":"Submits the owner-signed disableDelegation UserOp and, only once it has landed, reads each revoked delegation's remaining period budget and boundary into a frozen carry snapshot. The ordering is the point: reading before the revoke leaves a window in which a payment lands and the carried remainder over-counts it by that amount; after the revoke the on-chain state cannot move. It is safe because the revoke writes to the DelegationManager while the meter is read from the ERC20PeriodTransferEnforcer — two different contracts, and the read consults nothing the revoke writes. On a failed submit nothing is written and the old key is still live, so a retry is safe. The revoked set is derived SERVER-side (#3343): every delegation this agent still holds enabled on-chain (pending, active and replaced rows), and the signed calldata must disable exactly that set before anything is recorded — a subset or stale op answers 409 re-prepare, an unreadable one 400, and the stage stays preflight either way.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"rekeyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Re-key id from the preflight response."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature","user_operation","delegation_hashes"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"},"user_operation":{"type":"object","additionalProperties":true},"delegation_hashes":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}}}}}}},"responses":{"200":{"description":"Revoked on-chain and metered. The agent now has NO authority until the issue step completes.","content":{"application/json":{"schema":{"type":"object","required":["revoked","stage","agent_has_no_authority"],"properties":{"revoked":{"type":"boolean"},"tx_hash":{"type":"string","nullable":true},"delegation_hashes":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}},"stage":{"type":"string","enum":["metered"]},"carry":{"type":"array","description":"The frozen measurement, one entry per revoked delegation.","items":{"type":"object","properties":{"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"remaining_atomic":{"type":"string"},"from_chain":{"type":"boolean","description":"False means the read fell back to the full budget. The carry REFUSES these rather than granting a fresh full period."}}}},"agent_has_no_authority":{"type":"boolean"},"next_step":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/rekey/{rekeyId}/issue":{"post":{"tags":["Delegations"],"operationId":"issueRekeyDelegations","summary":"Re-key step 3: build the carried delegations for the new delegate (#1698).","description":"Refuses unless the re-key is at stage \"metered\" — which is only reachable through the revoke, so issue-before-revoke is forbidden structurally rather than by convention. Each replaced budget yields up to two grants: a \"carry\" capped at the frozen remainder and EXPIRING at the old period boundary, and a paired \"steady\" carrying the original budget and cadence starting at that same instant. The two never overlap, so no re-key can shorten a period or grant more than the original budget within one. Returns EIP-712 payloads for the owner to sign.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"rekeyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Re-key id from the preflight response."}],"responses":{"201":{"description":"Replacement delegations built, pending the owner signature.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRekeyIssueResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Wrong stage (issue may never precede the revoke), or the carry was refused because the meter read did not come from the chain. Also returned if this re-key stopped being `metered` between the stage check and an individual delegation insert — an abandon, or another issue call for the same re-key that already reached `issued`, including one still in flight CONCURRENTLY: the whole piece-build loop and the stage flip to `issued` run in one transaction (#3450), so a second call on the same re-key either loses on the first insert or — for a re-key whose snapshot has no pieces — on the stage flip itself, and inserts nothing either way (#3439, #3450).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/rekey/{rekeyId}/complete":{"post":{"tags":["Delegations"],"operationId":"completeAgentRekey","summary":"Re-key steps 4 + 5: activate, rotate BOTH credentials, invalidate old-payer intents (#1698).","description":"Activates every signed replacement delegation and, in the same transaction, swaps the agent's delegate address AND mints a new API key — one operation retires the whole old credential set, because a stale API key is its own hazard (#1681 finding A). Every unexecuted intent stamped with the old payer is expired in that same transaction; #1690's signer-side payer guard is a backstop, not the primary defence. The new API key is returned ONCE, to the authenticated owner.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"rekeyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Re-key id from the preflight response."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signatures"],"properties":{"signatures":{"type":"array","description":"One entry per delegation from the issue step. Every one must be signed — a partial completion would rotate credentials while some replacement authority stayed unsigned.","items":{"type":"object","required":["delegation_hash","signature"],"properties":{"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"}}}}}}}}},"responses":{"200":{"description":"Re-key complete. The old API key stops authenticating immediately.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRekeyCompleteResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/rekey/{rekeyId}/abandon":{"post":{"tags":["Delegations"],"operationId":"abandonAgentRekey","summary":"Record a stopped re-key, and say plainly if it left the agent without authority (#1698).","description":"Recorded, not deleted: an abandoned re-key that got past the revoke left the agent with no authority, and the dashboard has to be able to say so rather than letting it read as a mysterious 403.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"rekeyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Re-key id from the preflight response."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Re-key abandoned.","content":{"application/json":{"schema":{"type":"object","required":["abandoned","stage","agent_has_no_authority"],"properties":{"abandoned":{"type":"boolean"},"stage":{"type":"string","enum":["abandoned"]},"agent_has_no_authority":{"type":"boolean"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegations/revoke-all":{"post":{"tags":["Delegations"],"operationId":"prepareRevokeAllDelegations","summary":"Batch revoke step 1: ONE signature kills every non-revoked budget (#1400).","description":"Bundles one disableDelegation call per pending/active delegation into a single sponsored UserOp (capped at 25 per batch; a coarser 100-row ceiling refuses before any reconciliation reads). Rows already disabled on-chain are healed to revoked and dropped from the batch first (#1423). Response shape matches the per-hash prepare, plus the delegation_hashes the batch will kill.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"],"description":"Multi-signer accounts choose per request; omitted, an EOA owner defaults to eip712_userop."}}}}}},"responses":{"200":{"description":"Prepared batch revocation, shaped by the signature scheme.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["signature_scheme","signing_payload","user_operation","treasury_address","delegation_hashes","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"signing_payload":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true},"treasury_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegation_hashes":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}},"instructions":{"type":"string"}}},{"type":"object","required":["signature_scheme","user_op_hash","user_operation","treasury_address","delegation_hashes","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["webauthn_userop"]},"user_op_hash":{"type":"string"},"user_operation":{"type":"object","additionalProperties":true},"treasury_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegation_hashes":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}},"instructions":{"type":"string"}}}]}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"422":{"description":"Batch too large — revoke per hash, then retry.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/delegations/revoke-all/submit":{"post":{"tags":["Delegations"],"operationId":"submitRevokeAllDelegations","summary":"Batch revoke step 2: submit the signed batch; rows flip only after the UserOp lands.","description":"The revoked set is derived SERVER-side (#3343) — every delegation this agent still holds enabled on-chain (pending, active and replaced rows); the request's delegation_hashes is accepted for compatibility but does not decide anything. The signed calldata must disable exactly that set (checked before submission): a subset or stale op answers 409 re-prepare, an unreadable op 400. The response reports the hashes that actually flipped (scoped to this agent), never an echo of the request.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature","user_operation","delegation_hashes"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"},"user_operation":{"type":"object","additionalProperties":true},"delegation_hashes":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}}}}}}},"responses":{"200":{"description":"Batch disabled on-chain; the listed rows are marked revoked.","content":{"application/json":{"schema":{"type":"object","required":["revoked","tx_hash","delegation_hashes"],"properties":{"revoked":{"type":"boolean"},"tx_hash":{"type":"string"},"delegation_hashes":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/account-signers":{"get":{"tags":["Delegations"],"operationId":"getAccountSigners","summary":"Read the treasury account's signer set (public key material only).","description":"The exact owner configuration the account address was derived from (#887) — the dashboard rebuilds the WebAuthn signer from this. Nothing secret: an address and P256 public-key coordinates.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"The signer set. Same shape as the account-scoped read (#1679).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HybridAccountSigners"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/account-signers/prepare":{"post":{"tags":["Delegations"],"operationId":"prepareSignerChange","summary":"Prepare a signer-set change (enroll a backup, remove a key) for an EXISTING signer to sign.","description":"Signer changes are ACCOUNT operations prepared here and signed by an existing signer — Haven prepares, never signs (#824). The chain's CannotRemoveLastSigner rule is mirrored as a clear 409 instead of an opaque revert; an informed two-to-one transition is permitted (#1199). Unlike the revocation prepares, this response carries no treasury_address.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["add_passkey","remove_passkey","add_owner","remove_owner"]},"passkey":{"type":"object","description":"Required for add_passkey/remove_passkey.","properties":{"key_id":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"}}},"owner_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Required for add_owner."},"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"]}}}}}},"responses":{"200":{"description":"Prepared signer change, shaped by the signature scheme.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["signature_scheme","signing_payload","user_operation","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"signing_payload":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true},"instructions":{"type":"string"}}},{"type":"object","required":["signature_scheme","user_op_hash","user_operation","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["webauthn_userop"]},"user_op_hash":{"type":"string"},"user_operation":{"type":"object","additionalProperties":true},"instructions":{"type":"string"}}}]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/account-signers/submit":{"post":{"tags":["Delegations"],"operationId":"submitSignerChange","summary":"Submit the signed signer-set change; storage syncs only after the on-chain op succeeds.","description":"Body precedence is envelope first, config second: a malformed body is a 400 regardless of account state. The DB sync is pinned to the SIGNED calldata — a user_operation that does not match the signed action is refused.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature","user_operation"],"properties":{"action":{"type":"string","enum":["add_passkey","remove_passkey","add_owner","remove_owner"]},"passkey":{"type":"object","description":"Required for add_passkey/remove_passkey.","properties":{"key_id":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"}}},"owner_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Required for add_owner."},"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"]},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"},"user_operation":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"Signer set updated on-chain and in storage.","content":{"application/json":{"schema":{"type":"object","required":["updated","tx_hash"],"properties":{"updated":{"type":"boolean"},"tx_hash":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/passport":{"get":{"tags":["Agents"],"operationId":"getAgentPassport","summary":"Read an agent's passport state and its authoritative standing.","description":"`passport: null` means the agent has none — the normal case for a basic agent, never an error (issuance is opt-in). The two fields answer different questions and are deliberately not collapsed into one badge: `standing` is the DATABASE's authoritative answer about the agent, while `passport.status`/the anchor describe how far the on-chain attestation has got. The chain lags; the database does not.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Passport state (or null) plus the agent's standing.","content":{"application/json":{"schema":{"type":"object","required":["passport","standing"],"properties":{"passport":{"oneOf":[{"type":"object","required":["status","assurance_level","attestation_uid","tx_hash","chain_id","attempts","last_error","requested_at","anchored_at"],"properties":{"status":{"type":"string","enum":["pending","anchored","failed"],"description":"Issuance progress. The enum is the table CHECK (migration 048), so a client can branch on it safely."},"assurance_level":{"type":"integer","description":"L0 only. The table CHECK pins this to 0 — higher tiers are not issuable (#970)."},"attestation_uid":{"type":["string","null"],"description":"EAS UID once anchored — the evidence pointer, never the decision."},"tx_hash":{"type":["string","null"]},"chain_id":{"type":["integer","null"]},"attempts":{"type":"integer"},"last_error":{"type":["string","null"]},"requested_at":{"type":["string","null"],"format":"date-time"},"anchored_at":{"type":["string","null"],"format":"date-time"}}},{"type":"null"}]},"standing":{"type":"object","description":"The standing OBJECT (not a bare string): the database-authoritative answer plus the chain-lag transparency fields. Always present, even for an agent with no passport row.","required":["agentId","standing","anchor","attestationUid","chainLagging","revocationConfirmedAt"],"properties":{"agentId":{"type":"string"},"standing":{"type":"string","enum":["active","suspended","revoked","unknown"],"description":"THE answer, derived from agents.status alone — an agent revoked before its passport ever anchored is still revoked."},"anchor":{"type":"string","enum":["not_anchored","anchored","re_anchoring","revocation_pending","revoked_onchain"],"description":"Describes the chain, for transparency — not for deciding. `re_anchoring` means a re-key rotated the delegate key and the attestation naming the old one is being retired and reissued (#1699); standing is unaffected."},"attestationUid":{"type":["string","null"]},"chainLagging":{"type":"boolean","description":"True when the database says revoked but the chain has not caught up — a merchant reading only the chain in this window would be WRONG."},"revocationConfirmedAt":{"type":["string","null"],"format":"date-time"}}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Agents"],"operationId":"requestAgentPassport","summary":"Opt an existing agent in to a passport.","description":"Owner action, never agent-authenticated: an agent must not be able to issue itself a credential. Records the request synchronously and returns **202** — the EAS write is fire-and-forget, so poll the GET (or the public verifier) for the anchored state. Idempotent: an already-anchored passport returns **200** with `already_issued: true` rather than minting a second attestation. Refusals are shaped by whose problem it is — a revoked agent is a 409 (terminal, and anchoring now would spend gas on an attestation that must be revoked immediately), an account on a RETIRED rail is also a 409 (#2138: passports are issued on the delegation rail only, because a rail that cannot transact has no spending for a contract to govern), an unbound or unsupported chain is a 400, and a deployment that has not configured issuance is a 503, because that is the operator's gap and not the caller's mistake. The rail refusal is ordered AFTER the idempotency check, so a passport already anchored on a legacy account still returns 200 — existing passports are left alone rather than revoked. A PAUSED agent is deliberately not blocked: pausing is reversible and `standing` already reports it as suspended.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Already anchored — nothing was minted and no gas was spent.","content":{"application/json":{"schema":{"type":"object","required":["passport","already_issued"],"properties":{"passport":{"type":"object","required":["status","assurance_level","attestation_uid","tx_hash","chain_id","attempts","last_error","requested_at","anchored_at"],"properties":{"status":{"type":"string","enum":["pending","anchored","failed"],"description":"Issuance progress. The enum is the table CHECK (migration 048), so a client can branch on it safely."},"assurance_level":{"type":"integer","description":"L0 only. The table CHECK pins this to 0 — higher tiers are not issuable (#970)."},"attestation_uid":{"type":["string","null"],"description":"EAS UID once anchored — the evidence pointer, never the decision."},"tx_hash":{"type":["string","null"]},"chain_id":{"type":["integer","null"]},"attempts":{"type":"integer"},"last_error":{"type":["string","null"]},"requested_at":{"type":["string","null"],"format":"date-time"},"anchored_at":{"type":["string","null"],"format":"date-time"}}},"already_issued":{"type":"boolean","enum":[true]}}}}}},"202":{"description":"Passport requested; the anchor is in progress.","content":{"application/json":{"schema":{"type":"object","required":["passport"],"properties":{"passport":{"oneOf":[{"type":"object","required":["status","assurance_level","attestation_uid","tx_hash","chain_id","attempts","last_error","requested_at","anchored_at"],"properties":{"status":{"type":"string","enum":["pending","anchored","failed"],"description":"Issuance progress. The enum is the table CHECK (migration 048), so a client can branch on it safely."},"assurance_level":{"type":"integer","description":"L0 only. The table CHECK pins this to 0 — higher tiers are not issuable (#970)."},"attestation_uid":{"type":["string","null"],"description":"EAS UID once anchored — the evidence pointer, never the decision."},"tx_hash":{"type":["string","null"]},"chain_id":{"type":["integer","null"]},"attempts":{"type":"integer"},"last_error":{"type":["string","null"]},"requested_at":{"type":["string","null"],"format":"date-time"},"anchored_at":{"type":["string","null"],"format":"date-time"}}},{"type":"null"}]}}}}}},"400":{"description":"Agent has no bound account, or passports are not issued on its chain.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Agent is revoked (terminal), or its account is on a retired rail — passports are issued on the delegation rail only (#2138).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"Passport issuance is not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/passport/issuer":{"get":{"tags":["Agents"],"operationId":"getPassportIssuer","summary":"PUBLIC: the issuer address a merchant pins to verify receipts offline.","description":"Published so pinning is a one-time setup step rather than something a merchant extracts from a receipt it has not yet verified — trusting the issuer field of an unverified artifact is circular. Rate-limited.","security":[],"responses":{"200":{"description":"The issuer and the receipt envelope parameters.","content":{"application/json":{"schema":{"type":"object","required":["issuer","version","receipt_ttl_seconds","signature_scheme"],"properties":{"issuer":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"version":{"type":"string"},"receipt_ttl_seconds":{"type":"integer"},"signature_scheme":{"type":"string","examples":["eip191-personal-sign-over-canonical-json"]}}}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"Verification is not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/passport/verify":{"get":{"tags":["Agents"],"operationId":"verifyPassport","summary":"PUBLIC: fetch a signed governance receipt for an agent.","description":"Unauthenticated by design — the caller is a merchant deciding whether to serve an agent; it has no Haven account and cannot be asked to get one. That makes the disclosure boundary the only protection, so the receipt carries booleans and public on-chain addresses and nothing else: no budget, no balance, no owner, and no Safe/treasury identifier beyond the spend accounts a merchant already needs in order to recognise the payer (the delegate EOA on an EIP-3009 header, the smart account in erc7710 redemption). Query by EXACTLY ONE of `address` or `uid`; both or neither is a 400. **An agent with no passport is 200 with `found: false`, not a 404** — issuance is opt-in so most agents have none, and an error status invites integrations to treat a lookup failure as a pass. Only passports already public on-chain resolve; a pending or failed one is indistinguishable from having none. Caching follows the same logic: a negative answer is `no-store` (today's no can be tomorrow's yes), a receipt is cacheable for half its TTL so an HTTP cache can never outlive the signed envelope.","security":[],"parameters":[{"name":"address","in":"query","required":false,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"The agent's delegate EOA or smart account."},{"name":"uid","in":"query","required":false,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"description":"EAS attestation UID."}],"responses":{"200":{"description":"Either a signed receipt, or a clean `found: false` — both are normal answers.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["found","receipt","signature"],"properties":{"found":{"type":"boolean","enum":[true]},"receipt":{"type":"object","required":["version","issuer","agentId","agentEoa","smartAccount","assuranceLevel","standing","anchor","evidenceUid","chainId","controls","standingEpoch","issuedAt","expiresAt"],"properties":{"version":{"type":"string"},"issuer":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The signing address a merchant pins — fetch it from GET /passport/issuer."},"agentId":{"type":"string","description":"Haven's opaque agent id. Not PII and not a wallet."},"agentEoa":{"type":["string","null"],"description":"The delegate EOA a merchant sees on an EIP-3009 header."},"smartAccount":{"type":["string","null"],"description":"The Hybrid delegator a merchant sees in erc7710 redemption."},"assuranceLevel":{"type":"integer","enum":[0]},"standing":{"type":"string","enum":["active","suspended","revoked","unknown"],"description":"THE answer, sourced from the database — never derived from the chain."},"anchor":{"type":"string","enum":["not_anchored","anchored","re_anchoring","revocation_pending","revoked_onchain"],"description":"The on-chain anchor's progress, for transparency. Never the authority. `re_anchoring` is the re-key window (#1699): the attestation on-chain is live but names the agent's RETIRED delegate key, because EAS attestations are immutable — the agent's standing is unaffected."},"evidenceUid":{"type":["string","null"]},"chainId":{"type":["integer","null"]},"controls":{"oneOf":[{"type":"object","required":["rail","policyEnforcedOnchain","treasuryBound"],"properties":{"rail":{"type":"string","enum":["delegation","allowance_module","session_key"],"description":"The account's execution rail, verbatim from smart_accounts. Only 'delegation' is live; 'allowance_module' (#1440) and 'session_key' (#834) are retired and cannot transact. This field named a shorter, non-existent rail value until #2110 — one the column CHECK has never permitted."},"policyEnforcedOnchain":{"type":"boolean","description":"True only on the delegation rail, where the caveat enforcers revert an out-of-policy redemption on-chain. False on both retired rails: a legacy account may still hold a real on-chain allowance, but every agent payment entry point answers 410, so there is no spend for a contract to govern."},"treasuryBound":{"type":"boolean"}}},{"type":"null"}]},"standingEpoch":{"type":"integer","description":"Monotonic ORDERING marker (ms) over changes to the agent record, not a causation signal; 0 means no timestamp. Equal epochs do NOT imply equal receipts — anchor progress moves without it (#1015)."},"issuedAt":{"type":"integer"},"expiresAt":{"type":"integer","description":"issuedAt + the TTL published by GET /passport/issuer."}}},"signature":{"type":"string","description":"EIP-191 signature over the canonical JSON of `receipt`."}}},{"type":"object","required":["found"],"properties":{"found":{"type":"boolean","enum":[false]},"reason":{"type":"string"}}}]}}}},"400":{"description":"Not exactly one of address/uid, or a malformed value.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"Receipt signing is not configured — fail closed rather than serve an unsigned receipt.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/safes":{"get":{"tags":["Dashboard"],"operationId":"listUserSafes","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `GET /user/accounts`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `GET /user/accounts`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Dashboard"],"operationId":"addUserSafe","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `POST /user/accounts`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `POST /user/accounts`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/safes/deploy":{"post":{"tags":["Dashboard"],"operationId":"deployUserSafe","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `POST /user/accounts/deploy`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `POST /user/accounts/deploy`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/safes/{id}":{"put":{"tags":["Dashboard"],"operationId":"renameUserSafe","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `PUT /user/accounts/{accountId}`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `PUT /user/accounts/{accountId}`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["Dashboard"],"operationId":"unlinkUserSafe","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `DELETE /user/accounts/{accountId}`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `DELETE /user/accounts/{accountId}`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/safes/{id}/default":{"put":{"tags":["Dashboard"],"operationId":"setDefaultUserSafe","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `PUT /user/accounts/{accountId}/default`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `PUT /user/accounts/{accountId}/default`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/safes/{id}/funding":{"get":{"tags":["Dashboard"],"operationId":"getUserSafeFunding","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `GET /user/accounts/{accountId}/funding`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `GET /user/accounts/{accountId}/funding`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/safe":{"put":{"tags":["Dashboard"],"operationId":"updateUserSafe","deprecated":true,"summary":"RETIRED (#2914) — always answers 410.","description":"The Safe-vocabulary paths are retired: Haven accounts are addressed as accounts. Use `PUT /user/account`, which takes the same request. **Read the replacement own schema before swapping the path** — the list envelope key moved from safes to accounts, and three of these replacements (POST /user/accounts, POST /user/accounts/deploy, PUT /user/account) are themselves 410 from the Safe-rail inflow closure (#1984), so the refusal body names the live alternative instead. The body carries replacement as a field so a client can route on it without parsing prose, and authMiddleware still runs FIRST — an anonymous caller gets 401 and never learns which paths this deployment used to serve.","security":[{"DashboardJwt":[]}],"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The body names `PUT /user/account`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/accounts":{"get":{"tags":["Dashboard"],"operationId":"listUserAccounts","summary":"List the accounts linked to the caller's account, oldest first.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Linked accounts ordered by created_at ASC.","content":{"application/json":{"schema":{"type":"object","required":["accounts"],"properties":{"accounts":{"type":"array","items":{"type":"object","required":["id","account_address","chain_id","name","is_default","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"name":{"type":"string","description":"Display label; defaults to 'My account' when none is given."},"is_default":{"type":"boolean","description":"The first account a user links becomes the default."},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Dashboard"],"operationId":"addUserAccount","summary":"RETIRED — always answers 410. Importing an account is closed.","description":"**RETIRED (#1984, epic #1440) — always answers 410 and writes nothing.** Importing was one of the four ways an account could enter Haven on the Safe rail; all four are closed. The refusal is a route preHandler, so it precedes every read and write. Kept as a compatibility tombstone rather than removed — a 410 tells an old client the flow is permanently gone, where a 404 reads as a transient routing error and invites retries (the #834 session-rail / #1328 mpp_demo pattern). Create a Haven account on the delegation rail instead (POST /accounts/hybrid). Existing linked accounts are unaffected: GET /user/accounts, rename, re-default, unlink and every read path behave exactly as before.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_address"],"properties":{"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer","description":"Defaults to DEFAULT_CHAIN_ID (Base); must be a supported chain."},"name":{"type":"string","description":"Trimmed; blank or absent becomes 'My account'."}}}}}},"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The Safe rail is retired; the message names POST /accounts/hybrid.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/accounts/deploy":{"post":{"tags":["Dashboard"],"operationId":"deployUserAccount","summary":"RETIRED — always answers 410. Haven no longer deploys Safes.","description":"**RETIRED (#1984, epic #1440) — always answers 410 and spends no relayer gas.** The refusal is a route preHandler, so it precedes the relayer entirely. Historically the relayer sponsored the deployment and returned the deployed address plus the transaction hash, and owner_address was NOT checked against the caller — an unbounded-by-ownership relayer-gas surface that this retirement closes as a side effect. Create a Haven account on the delegation rail instead (POST /accounts/hybrid).","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_address"],"properties":{"owner_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer","description":"Defaults to DEFAULT_CHAIN_ID (Base); must be a supported chain."}}}}}},"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The Safe rail is retired; the message names POST /accounts/hybrid.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/accounts/{accountId}":{"put":{"tags":["Dashboard"],"operationId":"renameUserAccount","summary":"Rename a linked account.","description":"Display metadata only — the name exists nowhere on-chain.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Linked-account id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"description":"Trimmed; blank after trimming is a 400."}}}}}},"responses":{"200":{"description":"The renamed account.","content":{"application/json":{"schema":{"type":"object","required":["id","account_address","chain_id","name","is_default","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"name":{"type":"string","description":"Display label; defaults to 'My account' when none is given."},"is_default":{"type":"boolean","description":"The first account a user links becomes the default."},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["Dashboard"],"operationId":"unlinkUserAccount","summary":"Unlink an account from the Haven account.","description":"Removes the link and its Haven-side metadata. **The account itself is untouched on-chain** — the user still owns it and can re-link it later. Unlinking the default account promotes another one. Unlinking is refused while an agent has a live budget delegation (pending, active or replaced), an in-flight recovery, or an in-flight re-key.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Linked-account id."}],"responses":{"200":{"description":"Account unlinked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"The account remains linked while a delegation, recovery, or re-key is in progress.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/accounts/{accountId}/default":{"put":{"tags":["Dashboard"],"operationId":"setDefaultUserAccount","summary":"Make a linked account the default.","description":"Exactly one account is default per user; setting one clears the previous.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Linked-account id."}],"responses":{"200":{"description":"Default updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/accounts/{accountId}/funding":{"get":{"tags":["Dashboard"],"operationId":"getAccountFunding","summary":"Machine-readable funding facts for one account: what to fund, with what, where, and how much.","description":"Read-only facts a human acts on (#2534). Funding is a human step — a transfer from the user's own wallet or exchange — and this is the single source an agent (or the dashboard's empty-state funding card) reads to hand that instruction over: the account address, the chain and its explorer, each token's balance and its documented `minimum_useful_human` constant, and whether the account already counts as funded (`funded`: any KNOWN token balance ≥ its minimum — a failed balance read serves the last-known figure marked stale (#3317), and a token never successfully read counts as unknown, never as unfunded). `native.needed` is always false: gas is relay-sponsored (UserOps), so no ETH is requested. `faucet_url` is present ONLY on testnets, taken from the chain registry — a link for the human; Haven never calls a faucet. Accepts the `owner_cli` device-code session in addition to the dashboard JWT. Constructs no transfer and grants no authority.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Linked-account id (the delegation-rail account)."}],"responses":{"200":{"description":"The funding picture for the linked account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FundingResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/profile":{"put":{"tags":["Dashboard"],"operationId":"updateUserProfile","summary":"Rename the caller's own account.","description":"Returns the FULL profile row (including currency_preference and created_at) — a wider shape than the wallet and safe writes below.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"Trimmed; blank or over 80 characters is a 400."}}}}}},"responses":{"200":{"description":"The updated profile.","content":{"application/json":{"schema":{"type":"object","required":["id","name","email","wallet_address","account_address","currency_preference","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":"string"},"wallet_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$"},"account_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$"},"currency_preference":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The account was deleted while a valid token for it was still in flight.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/wallet":{"put":{"tags":["Dashboard"],"operationId":"updateUserWallet","summary":"Record the caller's connected wallet address.","description":"Bookkeeping only: recording an address grants Haven nothing and moves nothing. Returns the narrower five-field identity projection, not the full profile.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["wallet_address"],"properties":{"wallet_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}}}}}},"responses":{"200":{"description":"The updated identity projection.","content":{"application/json":{"schema":{"type":"object","required":["id","name","email","wallet_address","account_address"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":"string"},"wallet_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$"},"account_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/account":{"put":{"tags":["Dashboard"],"operationId":"updateUserAccount","summary":"RETIRED — always answers 410. This link is an import.","description":"**RETIRED (#1984, epic #1440) — always answers 410 and writes nothing.** This route linked an account as the user's default, emitting the `safe_imported` funnel event: it is an IMPORT, so it retires with the rail. Kept as a compatibility tombstone; create a Haven account on the delegation rail instead (POST /accounts/hybrid).","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_address"],"properties":{"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer","description":"Defaults to DEFAULT_CHAIN_ID (Base)."}}}}}},"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The Safe rail is retired; the message names POST /accounts/hybrid.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/signers":{"get":{"tags":["Dashboard"],"operationId":"listUserSigners","summary":"List the caller's signers across all of their accounts, each once.","description":"Every signer of the caller's live Hybrid DeleGator accounts on every chain, deduplicated: a passkey by `key_id` (one passkey sits on every chain's account), an owner wallet by address (always lowercase). Each lists the accounts it approves. Passkeys come first, ordered by `created_at` ascending (unknown last, then `key_id`); wallets follow, ordered by address. Each `accounts` list is ordered by chain then address. `created_at` is the earliest enrollment date known for the key, null when none is recorded. Public-key material only; an account whose signer configuration cannot be resolved is skipped.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The deduplicated signer list.","content":{"application/json":{"schema":{"type":"object","required":["signers"],"properties":{"signers":{"type":"array","items":{"oneOf":[{"type":"object","required":["kind","key_id","created_at","accounts"],"properties":{"kind":{"type":"string","enum":["passkey"]},"key_id":{"type":"string"},"created_at":{"type":"string","format":"date-time","nullable":true},"accounts":{"type":"array","items":{"type":"object","required":["account_id","account_address","account_name","chain_id"],"properties":{"account_id":{"type":"string"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"account_name":{"type":"string","nullable":true},"chain_id":{"type":"integer"}}}}}},{"type":"object","required":["kind","address","accounts"],"properties":{"kind":{"type":"string","enum":["wallet"]},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"accounts":{"type":"array","items":{"type":"object","required":["account_id","account_address","account_name","chain_id"],"properties":{"account_id":{"type":"string"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"account_name":{"type":"string","nullable":true},"chain_id":{"type":"integer"}}}}}}]}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/preferences":{"get":{"tags":["Dashboard"],"operationId":"getUserPreferences","summary":"Read the caller's display-currency preference.","description":"Defaults to 'SEK' when the account has never set one — a value, not an absence.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The current preference.","content":{"application/json":{"schema":{"type":"object","required":["currency_preference"],"properties":{"currency_preference":{"type":"string"}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"put":{"tags":["Dashboard"],"operationId":"updateUserPreferences","summary":"Set the display-currency preference.","description":"Names the currency a transaction’s converted amount (`convertedAmount`) is struck in. Display only — it changes no balance, no price and no settlement asset.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["currency_preference"],"properties":{"currency_preference":{"type":"string","enum":["SEK","USD","EUR"]}}}}}},"responses":{"200":{"description":"The stored preference.","content":{"application/json":{"schema":{"type":"object","required":["currency_preference"],"properties":{"currency_preference":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/user/attention-dismissals":{"get":{"tags":["Dashboard"],"operationId":"listAttentionDismissals","summary":"The caller’s saved \"Needs you\" dismissals.","description":"Server-saved dismissals for the recurring \"Needs you\" items (#3813), stored per account (backup) and per agent (needs setup) — permanent, and visible from every device of the same user. Presentation-only: nothing here is on the payment path, and there is no undismiss — the backup recommendation stays visible on the account page and a budget-less agent on the agent page.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Every dismissal the caller has made, oldest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttentionDismissalsResponse"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Dashboard"],"operationId":"createAttentionDismissal","summary":"Dismiss one recurring \"Needs you\" item.","description":"Idempotent: re-dismissing answers the same 201 as the first write. `item_kind` decides which id the body must carry — `no-backup` requires `account_id` (the backup item is per account), `needs-setup` requires `agent_id` (per agent within its account). A foreign or unknown id is a 404, never a 403 — the route does not confirm that an id exists.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttentionDismissalInput"}}}},"responses":{"201":{"description":"Dismissed. Returns the stored dismissal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttentionDismissal"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The account or agent does not exist or belongs to another user.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/export":{"get":{"tags":["Dashboard"],"operationId":"exportAccounting","summary":"Legacy SIE export — GATED OFF by default.","description":"Superseded by the non-asserting accounting feed (#491): agent spend now syncs into the accounting tool as draft transactions instead of being exported as an asserting SIE file. **410 is the normal answer on a default deployment**; the route only serves when the legacy flag is on. Responds with a FILE, not JSON — Content-Disposition attachment, plus the custom headers X-Export-Entry-Count and X-Export-Skipped reporting how many entries were written and how many could not be.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["sie"]},"description":"Defaults to 'sie'; anything else is a 400."},{"name":"from","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}"},"description":"ISO date (YYYY-MM-DD…)."},{"name":"to","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}"},"description":"ISO date (YYYY-MM-DD…)."},{"name":"company","in":"query","schema":{"type":"string"},"description":"Company name in the file header; defaults to 'Haven'."}],"responses":{"200":{"description":"The SIE file.","headers":{"X-Export-Entry-Count":{"schema":{"type":"string"},"description":"Entries written."},"X-Export-Skipped":{"schema":{"type":"string"},"description":"Entries that could not be written."}},"content":{"application/octet-stream":{"schema":{"type":"string"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"The default: SIE export is retired in favour of the accounting feed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/reconcile":{"get":{"tags":["Dashboard"],"operationId":"reconcileAccounting","summary":"Surface the entries that cannot book cleanly over a period.","description":"Read-only diagnosis, never a fix: it classifies each entry and counts the classes, so a user can see WHY a period will not balance before trying to book it. Note the camelCase byStatus/paymentId/txHash/settledAt fields — this report comes from the accounting module, not from SQL rows.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}"},"description":"ISO date."},{"name":"to","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}"},"description":"ISO date."}],"responses":{"200":{"description":"The reconciliation report.","content":{"application/json":{"schema":{"type":"object","required":["total","ok","issues","byStatus","items"],"properties":{"total":{"type":"integer"},"ok":{"type":"integer"},"issues":{"type":"integer"},"byStatus":{"type":"object","required":["ok","missing_fx","missing_tx","unbalanced"],"properties":{"ok":{"type":"integer"},"missing_fx":{"type":"integer","description":"No SEK amount — the FX rate was unavailable."},"missing_tx":{"type":"integer","description":"No transaction hash to anchor the entry."},"unbalanced":{"type":"integer","description":"Debits and credits disagree."}}},"items":{"type":"array","description":"ONLY the entries that need attention — an entry classified ok is counted in byStatus but never listed here, so items.length is issues, not total.","items":{"type":"object","required":["paymentId","txHash","settledAt","status"],"properties":{"paymentId":{"type":"string"},"txHash":{"type":"string"},"settledAt":{"type":"string"},"status":{"type":"string","enum":["ok","missing_fx","missing_tx","unbalanced"]}}}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/categories":{"get":{"tags":["Dashboard"],"operationId":"listAccountOverrides","summary":"The caller's per-merchant BAS account overrides.","description":"NOTE THE CASING: this read returns the SQL rows as-is (resource_url/bas_account, snake_case), while the write below echoes its own request fields (resourceUrl/account, camelCase). Same path, two conventions — documented rather than normalised, because a generated client must match what the routes actually emit.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Overrides ordered by resource_url.","content":{"application/json":{"schema":{"type":"object","required":["overrides"],"properties":{"overrides":{"type":"array","items":{"type":"object","required":["resource_url","bas_account"],"properties":{"resource_url":{"type":"string"},"bas_account":{"type":"string"}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"put":{"tags":["Dashboard"],"operationId":"setAccountOverride","summary":"Map a merchant to a BAS account.","description":"Idempotent upsert keyed on (user, resourceUrl). A category is a bookkeeping label — it changes no payment and moves nothing. The response echoes the request fields in camelCase, unlike the snake_case read above.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resourceUrl","account"],"properties":{"resourceUrl":{"type":"string","minLength":1},"account":{"type":"string","pattern":"^[0-9]{3,6}$","description":"A BAS account number."}}}}}},"responses":{"200":{"description":"The stored override, echoed.","content":{"application/json":{"schema":{"type":"object","required":["resourceUrl","account"],"properties":{"resourceUrl":{"type":"string"},"account":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["Dashboard"],"operationId":"clearAccountOverride","summary":"Clear a merchant's BAS account override.","description":"Answers **204 No Content**, not a success envelope. Idempotent: clearing an override that was never set still succeeds.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"resourceUrl","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Override cleared (or was never set)."},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/feed/status":{"get":{"tags":["Dashboard"],"operationId":"getAccountingFeedStatus","summary":"Whether the accounting feed is available, connected, and live — plus recent syncs.","description":"Deliberately NOT gated, unlike the actions below: the page must be able to tell whether to render the full UI, an upsell, or nothing at all, and a 404 here would make \"not entitled\" indistinguishable from \"broken\". `liveSyncReady` false means sync is a preview that delivers nowhere — the provider adapter is not configured on this deployment. When the feed is unavailable the answer is a complete, honest shape with available:false and an empty syncs list, not an error. #2869: it answers 200 in every off state — `hosted:false` is \"not available on self-hosted\", `hosted:true, enabled:false` is \"Coming soon\" (visible in production by owner decision), and `enabled:true, entitled:false` is the add-on state; the gated actions 404 in all three.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Feed status.","content":{"application/json":{"schema":{"type":"object","required":["hosted","enabled","flagEnabled","liveSyncReady","entitled","entitlementMode","available","connected","companyName","destination","missingScopes","syncs","counts"],"properties":{"hosted":{"type":"boolean","description":"This deployment is the hosted Haven (`HAVEN_HOSTED`). False on a self-hosted box, where the feed is not available and never \"coming soon\" (#2869)."},"enabled":{"type":"boolean","description":"#2869: the `HAVEN_ACCOUNTING_ENABLED` flag. `hosted && !enabled` is the \"Coming soon\" state the dashboard shows in production."},"flagEnabled":{"type":"boolean","deprecated":true,"description":"Deprecated — same value as `enabled`; removed one release after #2869."},"liveSyncReady":{"type":"boolean","description":"A real provider adapter is registered."},"entitled":{"type":"boolean","description":"#2861: whether THIS account passes the entitlement check — in mode `granted` it holds the row, in mode `all` every account does. Always false when `hosted` or `enabled` is false, so the UI can tell \"feature off\" from \"not entitled\" without a second call."},"entitlementMode":{"type":"string","enum":["granted","all"],"description":"How entitlement is decided on this deployment (`HAVEN_ACCOUNTING_ENTITLEMENT_MODE`). `all` is the dev setting; production runs `granted`."},"available":{"type":"boolean","description":"hosted AND enabled AND entitled — the one field a caller needs to decide whether to render the feed."},"connected":{"type":"boolean","description":"The caller has a live provider connection."},"companyName":{"type":["string","null"],"description":"The company the ACTIVE connection points at, as the provider reported it (#2864) — \"Connected to <Company AB>\". Null when not connected, when the grant could not read it (`scope_missing`), or when the feed is unavailable."},"destination":{"type":["object","null"],"required":["provider","displayName","status","companyName","lastPushAt"],"description":"#2869: the connection flagged as the feed destination WHATEVER its status — the page summary line (\"Feeding Fortnox · Company AB · last push …\") and the sidebar attention badge read it. A `needs_reauthorisation` / `scope_missing` / `revoked_at_provider` destination is the attention state. Metadata only, never secrets. Null when there is no destination row or the feed is unavailable.","properties":{"provider":{"type":"string","examples":["fortnox"]},"displayName":{"type":"string"},"status":{"type":"string","enum":["connected","needs_reauthorisation","revoked_at_provider","scope_missing","needs_attention","disconnected"]},"companyName":{"type":["string","null"]},"lastPushAt":{"type":["string","null"],"format":"date-time"}}},"missingScopes":{"type":"array","items":{"type":"string"},"description":"#2865: the scopes the DESTINATION connection lacks (the row flagged as destination, whatever its status — a `scope_missing` destination reports `connected:false` and names them here), so the UI can say which scope a reconnect adds. Empty when nothing is missing or there is no destination; always present."},"syncs":{"type":"array","items":{"type":"object","required":["id","user_id","provider","payment_id","external_ref","status","error","attempts","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"provider":{"type":"string","examples":["fortnox"]},"payment_id":{"type":"string"},"external_ref":{"type":["string","null"],"description":"The provider-side reference once pushed."},"status":{"type":"string","enum":["pending","pushed","failed","skipped"]},"error":{"type":["string","null"],"description":"The provider message on a failed row, the reason on a skipped row, a non-fatal note on a pushed row. A `failed` row the retry sweep (#2866) has given up on starts with `exhausted:`."},"attempts":{"type":"integer","description":"Claims so far; the background sweep stops at 8."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"counts":{"type":"object","required":["pending","failed","exhausted"],"description":"#2866: sync rows by retry state, over ALL of the caller's rows (the `syncs` list is capped). `pending` is in flight (or a stale claim the sweep will release), `failed` is retryable — the background sweep re-feeds it with backoff — and `exhausted` is a `failed` row at the attempt cap (8) that the sweep has given up on; its `error` starts with `exhausted:` until a manual \"Sync now\" re-claims it (a later failure then carries the plain reason while `attempts` keeps it in this count). \"Sync now\" still retries exhausted rows. Zeros when the feed is unavailable.","properties":{"pending":{"type":"integer"},"failed":{"type":"integer"},"exhausted":{"type":"integer"}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/feed/sync":{"post":{"tags":["Dashboard"],"operationId":"syncAccountingFeed","summary":"Backfill and retry the feed for the caller.","description":"Pushes what has not been pushed and retries what failed. Gated: **404 when the feed is unavailable**, which is how an unentitled caller sees it. `fed` counts the payments this call actually pushed (#2915) — a candidate whose FX was not ready, that was skipped, or that failed is not counted even though it was enumerated; `total` is how many were enumerated. 0 fed is a normal answer, not a failure.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Payments pushed, and how many candidates the call enumerated.","content":{"application/json":{"schema":{"type":"object","required":["fed","total"],"properties":{"fed":{"type":"integer","description":"Payments this call pushed (at most 200)."},"total":{"type":"integer","description":"Payments this call enumerated as candidates — the ones not in `fed` were skipped, failed, or were not feedable (no book-time SEK, below the floor)."}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The accounting feed is not available for this caller.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/feed/verify/{paymentId}":{"get":{"tags":["Dashboard"],"operationId":"verifyAccountingFeedInvoice","summary":"Read back a pushed invoice from the provider's own records.","description":"Strictly read-only (#1362): it confirms whether the supplier invoice still exists and whether a human has booked it, and asserts nothing — the non-asserting principle is untouched. A payment that was never pushed, a disconnected provider, or a sync row with no invoice reference all answer 409 with a machine-readable error_code, because none of them is a verification result.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Haven payment id (a uuid, as every other `paymentId` path parameter says — #3030)."}],"responses":{"200":{"description":"The provider's verdict.","content":{"application/json":{"schema":{"type":"object","required":["registered","missing","booked","cancelled","invoice_number","document_ref","voucher","invoice_date","total","checked_at"],"properties":{"registered":{"type":"boolean","description":"The invoice exists in Fortnox under our external_ref."},"missing":{"type":["string","null"],"enum":["deleted","foreign_invoice",null],"description":"Why registered is false. 'deleted' = the number 404s; 'foreign_invoice' = an invoice exists at that number but carries someone else's ExternalInvoiceNumber (a company-switch collision). Null when registered. Both mean OUR record was never delivered under our ref — but an audit trail must not say 'no longer exists' about an invoice that does."},"booked":{"type":["boolean","null"],"description":"A human has booked it. Null when not registered (or not knowable)."},"cancelled":{"type":["boolean","null"],"description":"Registered but struck. Null when not registered."},"invoice_number":{"type":["integer","null"],"description":"The provider's own number for the record (what its UI shows). Null when the delivered object carries no number (#3018: an Accounted document) — document_ref names it instead."},"document_ref":{"type":["string","null"],"description":"The provider's id for the delivered document, when the record IS a document (Accounted: accounted:document:<id>). Null for invoice-shaped records (Fortnox)."},"voucher":{"type":["string","null"],"description":"`<series><number> <year>` once booked, e.g. \"A123 2026\". Null until then."},"invoice_date":{"type":["string","null"]},"total":{"type":["number","null"]},"checked_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The accounting feed is not available for this caller.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Not verifiable — not pushed, not connected, or no invoice reference.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["not_pushed","not_connected","no_invoice_ref"]},"status":{"type":["string","null"]}}}}}}}}},"/accounting/feed/reopen/{paymentId}":{"post":{"tags":["Dashboard"],"operationId":"reopenAccountingFeedPush","summary":"Reopen a pushed row for retry — only when the provider confirms the invoice is gone.","description":"The ONLY path that flips a pushed row back to retryable, and it is conditional on the PROVIDER, not on the caller's say-so (#1365): the server re-runs the read-back and reopens only when the invoice is confirmed gone, or when a number collision proves the invoice at that number is not ours. **An invoice that still exists refuses with 409 and writes nothing** — that is the double-post guard, and reopening against a live invoice would duplicate it. A row that moved between the check and the flip (raced by a concurrent sync) also refuses rather than pretending. After a successful reopen, the next sync re-claims and re-pushes through the normal retry path.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Haven payment id (a uuid, as every other `paymentId` path parameter says — #3030)."}],"responses":{"200":{"description":"Row reopened for retry.","content":{"application/json":{"schema":{"type":"object","required":["reopened","payment_id","next"],"properties":{"reopened":{"type":"boolean","enum":[true]},"payment_id":{"type":"string"},"next":{"type":"string"}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The accounting feed is not available for this caller.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Refused, nothing written — the invoice still exists, the row is not pushed, it moved under us, or (#2864, `previous_company`) the row was delivered into the company the connection pointed at BEFORE its latest company switch: its record lives in that company, \"missing\" in the current one is the correct verdict, and reopening would re-feed the previous company's history into the new one.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["not_pushed","not_connected","no_invoice_ref","invoice_exists","previous_company"]},"invoice_number":{"type":"integer"},"switched_at":{"type":"string","format":"date-time","description":"With `previous_company`: when the connection switched company."}}}}}}}}},"/accounting/webhooks/accounted/{token}":{"post":{"tags":["Webhooks"],"operationId":"accountedWebhookCallback","summary":"PUBLIC Accounted webhook callback — authenticated by the capability token + the HMAC signature, not by a session.","description":"Hit by Accounted's dispatcher (#3019). `<token>` is the per-connection capability token (32 random bytes base64url, generated at connect — the URL's first credential); the body is HMAC-verified against the connection's stored subscription secret (`X-Gnubok-Signature`, `t=<unix>,v1=<hex>`, over `${t}.${rawBody}` — the second credential, checked BEFORE any JSON parse on the raw bytes; `t` older than 5 minutes is refused). **400** on a bad signature or stale timestamp, **404** on an unknown token, **200** for everything else: feature off, `webhook.test`, unknown event types, duplicates (the `(provider, delivery_id)` row is written before the 2xx), and success — **never 410 and never any 3xx** (either would auto-disable the subscription or hand the provider a URL it did not register). Rate-limited at 600/min keyed per IP (a webhook carries no credential header).","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Acknowledged (or refused-with-a-counter — see the body). The provider stops retrying on 2xx.","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string"},"counted":{"type":"string"},"deliveryId":{"type":"string"}}}}}},"400":{"description":"Bad or malformed signature, or a stale timestamp."},"404":{"description":"No connection carries this token."}}}},"/accounting/webhooks/accounted/{token}/":{"post":{"tags":["Webhooks"],"operationId":"accountedWebhookCallbackTrailingSlash","summary":"PUBLIC Accounted webhook callback (trailing-slash twin — the same handler; never a redirect).","description":"Registered EXPLICITLY so the provider never meets Fastify's trailing-slash redirect: a 3xx would hand it a URL the subscription was not registered with, and a 410 would auto-disable the subscription without replay (#3019). Identical contract to `accountedWebhookCallback`.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Acknowledged (or refused-with-a-counter — see the body). The provider stops retrying on 2xx.","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string"},"counted":{"type":"string"},"deliveryId":{"type":"string"}}}}}},"400":{"description":"Bad or malformed signature, or a stale timestamp."},"404":{"description":"No connection carries this token."}}}},"/accounting/providers":{"get":{"tags":["Dashboard"],"operationId":"listAccountingProviders","summary":"The accounting providers Haven knows about, live or coming soon.","description":"Four today: Fortnox and Accounted (`live` — Fortnox over OAuth2, Accounted over a pasted API key) and Light, Igdrasil (`coming_soon` — listed by product decision before any code exists for them). Only a `live` provider accepts a connect. `configured` says whether THIS deployment can connect it.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The registry.","content":{"application/json":{"schema":{"type":"object","required":["providers"],"properties":{"providers":{"type":"array","items":{"type":"object","required":["id","displayName","authKind","capabilities","availability","requiredScopes","configured"],"properties":{"id":{"type":"string","examples":["fortnox"]},"displayName":{"type":"string","examples":["Fortnox"]},"authKind":{"type":"string","enum":["oauth2","api_key"]},"capabilities":{"type":"object","required":["attachments","verify","revoke","companyInfo"],"properties":{"attachments":{"type":"boolean"},"verify":{"type":"boolean"},"revoke":{"type":"boolean"},"companyInfo":{"type":"boolean"}}},"availability":{"type":"string","enum":["live","coming_soon"],"description":"Only a `live` provider accepts a connect; `coming_soon` ones are listed so the dashboard can show them."},"requiredScopes":{"type":"array","items":{"type":"string"}},"configured":{"type":"boolean","description":"Whether THIS deployment can connect the provider (its connector is registered). A live provider without credentials configured is listed but refuses connect with 503."}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/connections":{"get":{"tags":["Dashboard"],"operationId":"listAccountingConnections","summary":"The caller's accounting connections — metadata only, never secrets.","description":"One entry per provider the caller has ever connected; a disconnected one stays as `status: disconnected` (history stays, secrets cleared). Exactly one carries `isActiveDestination: true` while any is connected.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Connections.","content":{"application/json":{"schema":{"type":"object","required":["connections"],"properties":{"connections":{"type":"array","items":{"type":"object","required":["provider","displayName","authKind","status","statusReason","isActiveDestination","feedFrom","grantedScope","missingScopes","tokenExpiresAt","externalCompanyId","externalCompanyName","baseCurrency","lastPushAt","lastError","connectedAt","updatedAt","settings"],"properties":{"provider":{"type":"string","examples":["fortnox"]},"displayName":{"type":"string"},"authKind":{"type":"string","enum":["oauth2","api_key"]},"status":{"type":"string","enum":["connected","needs_reauthorisation","revoked_at_provider","scope_missing","needs_attention","disconnected"],"description":"Disconnect keeps the row as `disconnected` (history stays); `scope_missing` is set by a post-push attachment failure that needs a re-consent, by a connect whose company read was refused for scope (#2864), by a callback whose granted scope falls short of the provider's required scopes, or by a push whose create call was refused for scope (#2865). `needs_attention` (#3019) is the webhook half failing independently of the feed — the key validates and pushes work, but the subscriptions could not be created (a `webhook subscription failed` registration) or the deployment states no public API origin (`no public API origin configured`); a reconnect after fixing the deployment state resolves it. A re-consent — the same connect-url + callback on the existing connection — restores `connected` and keeps settings, feedFrom, the active flag and the sync history."},"statusReason":{"type":["string","null"]},"isActiveDestination":{"type":"boolean","description":"Exactly one connection per user is where settled payments go."},"feedFrom":{"type":["string","null"],"format":"date-time","description":"Nothing settled before this is fed. Set to now by activate (#2862) and by a company switch (#2864); moved EARLIER only by the backfill (#2867). Null on a pre-#2862 row that feeds everything."},"grantedScope":{"type":["string","null"]},"missingScopes":{"type":"array","items":{"type":"string"},"description":"#2865: the provider's required scopes the grant does not carry — derived from `grantedScope` against the descriptor's `requiredScopes`, plus the scopes a push-time refusal named while the row is `scope_missing`. Empty when nothing is missing. Non-empty on a `connected` row means the grant predates a scope widening and will degrade at the first call that needs it; a re-consent clears it."},"tokenExpiresAt":{"type":["string","null"],"format":"date-time","description":"Access-token expiry (OAuth2 providers). Null for API-key providers."},"externalCompanyId":{"type":["string","null"],"description":"The provider's own tenant id (Fortnox: `DatabaseNumber`), read at connect (#2864). A reconnect that comes back with a different id is a company switch: the row is kept, the company fields are replaced, `feedFrom` moves to now and `statusReason` names the switch. Null until a grant with the company scope read it."},"externalCompanyName":{"type":["string","null"],"description":"The company the connection points at, for \"Connected to <Company AB>\" (#2864)."},"baseCurrency":{"type":["string","null"],"description":"ISO-4217 as the provider reported it at connect, and the currency the feed pushes in (#2877). A ledger outside the supported list — SEK, EUR, USD, DKK, NOK, GBP — is refused at connect with \"Haven feeds SEK, EUR, USD, DKK, NOK and GBP ledgers\" (#2864). Null when the provider cannot say; such a connection books in SEK."},"lastPushAt":{"type":["string","null"],"format":"date-time"},"lastError":{"type":["string","null"]},"connectedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"settings":{"type":"object","required":["suggestedAccount","autoFeed"],"properties":{"suggestedAccount":{"type":["string","null"],"description":"A hint for the accountant, carried on every pushed document ONLY in the connector's non-asserting hint field (Fortnox: `YourReference: \"suggested account 6540\"`) — never as an account field; the payload guard still bans `Account`. A per-merchant override, when one exists, wins over this. Fortnox: a four-digit BAS account. Null = no hint."},"autoFeed":{"type":"boolean","description":"Default true. False = \"manual only\": the settlement hook and the background retry sweep leave this user alone; `POST /accounting/feed/sync` and the backfill still push."}}}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The deployment is not hosted, or the accounting feature flag is off.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/connections/{provider}/connect-url":{"post":{"tags":["Dashboard"],"operationId":"getAccountingConnectUrl","summary":"Get the consent URL for a live OAuth2 provider, as JSON.","description":"A single-page app cannot carry its Bearer token through a plain browser navigation, so it fetches the URL here and navigates itself. The URL embeds a signed `state` that expires in 10 minutes, carries a purpose claim `authMiddleware` rejects, is bound to the provider, and is SINGLE-USE (a `jti` the callback consumes) — see the callback.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"},"description":"Provider id from /accounting/providers."}],"responses":{"200":{"description":"The consent URL. Carries no token material.","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string"}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Unknown provider, or the accounting feature is off (#2918, same body shape as `requireAccountingFeed`).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Refused: the provider is not live (`PROVIDER_NOT_LIVE`) or the flow does not match its auth kind (`WRONG_AUTH_KIND`).","content":{"application/json":{"schema":{"type":"object","required":["error","error_code"],"properties":{"error":{"type":"string"},"error_code":{"type":"string"}}}}}},"503":{"description":"The provider is live but not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/connections/{provider}/callback":{"get":{"tags":["Dashboard"],"operationId":"accountingOAuthCallback","summary":"PUBLIC OAuth callback — authenticated by the signed state, not by a session.","description":"Hit by a browser redirect from the provider, which carries no JWT. The caller is authenticated by the `state` this flow issued: it must verify, carry the accounting_oauth PURPOSE claim (an ordinary session token is rejected), name THIS provider, and its `jti` must not have been seen before — the state is consumed before the code is exchanged, so a replay never reaches the provider. **Every outcome is a redirect to the accounting page, never JSON**, and every failure collapses to the same `connect=error` regardless of cause: a bad or replayed state, a failed code exchange, a missing secrets key and a failed save are indistinguishable to the browser by design. Two outcomes are named because the user can act on them: a user-declined consent is `connect=denied` (their own action, not a failure to hide), and a company that books in a currency Haven does not feed is `connect=error&reason=unsupported_currency` (#2864, widened by #2877: \"Haven feeds SEK, EUR, USD, DKK, NOK and GBP ledgers\" — nothing was stored; an existing connection is left as it was, and the user can pick another company). #2918: this route is NEVER the bare 404 the other connection routes answer when the feature is off — a consent can be mid-flight when the flag is flipped, so the off-path still redirects, named `connect=error&reason=feature_off`, and the `state`'s `jti` is consumed either way (a state cannot be banked while the flag is off and replayed after it comes back on).","security":[],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}},{"name":"code","in":"query","schema":{"type":"string"},"description":"Authorization code from the provider."},{"name":"state","in":"query","schema":{"type":"string"},"description":"The signed, purpose-scoped, single-use state this flow issued."},{"name":"error","in":"query","schema":{"type":"string"},"description":"Present when the user declined consent."}],"responses":{"302":{"description":"Always a redirect to `/accounting?provider=<id>&connect=connected|denied|error`, with `&reason=unsupported_currency` on the one named refusal, or `&reason=feature_off` (#2918) when the deployment is not hosted or the flag is off."}}}},"/accounting/connections/{provider}/api-key":{"post":{"tags":["Dashboard"],"operationId":"connectAccountingApiKey","summary":"Connect a live API-key provider: validate the key at the provider, then store it encrypted.","description":"The key is validated by asking the provider who it belongs to; a key the provider rejects never lands (400). A company that books in a currency outside the supported list is refused (409 `UNSUPPORTED_BASE_CURRENCY`, \"Haven feeds SEK, EUR, USD, DKK, NOK and GBP ledgers\") BEFORE the key is stored — nothing lands, an existing connection is left as it was. A VALID key that can see MORE THAN ONE company is refused too (#3017: 409 `MULTI_COMPANY_KEY` — the feed has no per-push company choice, so create a key scoped to one company). Accounted is live today; Light is listed `coming_soon`. The key is never echoed.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["apiKey"],"properties":{"apiKey":{"type":"string"}}}}}},"responses":{"201":{"description":"Connected. Metadata only.","content":{"application/json":{"schema":{"type":"object","required":["connection"],"properties":{"connection":{"type":"object","required":["provider","displayName","authKind","status","statusReason","isActiveDestination","feedFrom","grantedScope","missingScopes","tokenExpiresAt","externalCompanyId","externalCompanyName","baseCurrency","lastPushAt","lastError","connectedAt","updatedAt","settings"],"properties":{"provider":{"type":"string","examples":["fortnox"]},"displayName":{"type":"string"},"authKind":{"type":"string","enum":["oauth2","api_key"]},"status":{"type":"string","enum":["connected","needs_reauthorisation","revoked_at_provider","scope_missing","needs_attention","disconnected"],"description":"Disconnect keeps the row as `disconnected` (history stays); `scope_missing` is set by a post-push attachment failure that needs a re-consent, by a connect whose company read was refused for scope (#2864), by a callback whose granted scope falls short of the provider's required scopes, or by a push whose create call was refused for scope (#2865). `needs_attention` (#3019) is the webhook half failing independently of the feed — the key validates and pushes work, but the subscriptions could not be created (a `webhook subscription failed` registration) or the deployment states no public API origin (`no public API origin configured`); a reconnect after fixing the deployment state resolves it. A re-consent — the same connect-url + callback on the existing connection — restores `connected` and keeps settings, feedFrom, the active flag and the sync history."},"statusReason":{"type":["string","null"]},"isActiveDestination":{"type":"boolean","description":"Exactly one connection per user is where settled payments go."},"feedFrom":{"type":["string","null"],"format":"date-time","description":"Nothing settled before this is fed. Set to now by activate (#2862) and by a company switch (#2864); moved EARLIER only by the backfill (#2867). Null on a pre-#2862 row that feeds everything."},"grantedScope":{"type":["string","null"]},"missingScopes":{"type":"array","items":{"type":"string"},"description":"#2865: the provider's required scopes the grant does not carry — derived from `grantedScope` against the descriptor's `requiredScopes`, plus the scopes a push-time refusal named while the row is `scope_missing`. Empty when nothing is missing. Non-empty on a `connected` row means the grant predates a scope widening and will degrade at the first call that needs it; a re-consent clears it."},"tokenExpiresAt":{"type":["string","null"],"format":"date-time","description":"Access-token expiry (OAuth2 providers). Null for API-key providers."},"externalCompanyId":{"type":["string","null"],"description":"The provider's own tenant id (Fortnox: `DatabaseNumber`), read at connect (#2864). A reconnect that comes back with a different id is a company switch: the row is kept, the company fields are replaced, `feedFrom` moves to now and `statusReason` names the switch. Null until a grant with the company scope read it."},"externalCompanyName":{"type":["string","null"],"description":"The company the connection points at, for \"Connected to <Company AB>\" (#2864)."},"baseCurrency":{"type":["string","null"],"description":"ISO-4217 as the provider reported it at connect, and the currency the feed pushes in (#2877). A ledger outside the supported list — SEK, EUR, USD, DKK, NOK, GBP — is refused at connect with \"Haven feeds SEK, EUR, USD, DKK, NOK and GBP ledgers\" (#2864). Null when the provider cannot say; such a connection books in SEK."},"lastPushAt":{"type":["string","null"],"format":"date-time"},"lastError":{"type":["string","null"]},"connectedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"settings":{"type":"object","required":["suggestedAccount","autoFeed"],"properties":{"suggestedAccount":{"type":["string","null"],"description":"A hint for the accountant, carried on every pushed document ONLY in the connector's non-asserting hint field (Fortnox: `YourReference: \"suggested account 6540\"`) — never as an account field; the payload guard still bans `Account`. A per-merchant override, when one exists, wins over this. Fortnox: a four-digit BAS account. Null = no hint."},"autoFeed":{"type":"boolean","description":"Default true. False = \"manual only\": the settlement hook and the background retry sweep leave this user alone; `POST /accounting/feed/sync` and the backfill still push."}}}}}}}}}},"400":{"description":"Missing key, or the provider rejected it.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Unknown provider, or the accounting feature is off (#2918, same body shape as `requireAccountingFeed`).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Refused: the provider is not live (`PROVIDER_NOT_LIVE`), the flow does not match its auth kind (`WRONG_AUTH_KIND`), the company books in a currency outside the supported list (`UNSUPPORTED_BASE_CURRENCY`, #2864/#2877 — nothing stored), or the key can see more than one company (`MULTI_COMPANY_KEY`, #3017 — nothing stored; create a key scoped to one company).","content":{"application/json":{"schema":{"type":"object","required":["error","error_code"],"properties":{"error":{"type":"string"},"error_code":{"type":"string"}}}}}},"503":{"description":"The provider is live but not configured on this deployment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/connections/{provider}":{"delete":{"tags":["Dashboard"],"operationId":"disconnectAccountingProvider","summary":"Disconnect a provider for the caller.","description":"Clears the stored secrets and marks the connection `disconnected` — the row stays because sync history references it (owner decision). When the provider declares the `revoke` capability the grant is revoked at the provider first. Answers **204 No Content** and returns no token material. Idempotent — disconnecting when nothing is connected still succeeds.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Disconnected (or was never connected)."},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The deployment is not hosted, or the accounting feature flag is off.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/connections/{provider}/activate":{"post":{"tags":["Dashboard"],"operationId":"activateAccountingConnection","summary":"Make this connection the feed destination; feed_from = now.","description":"Exactly one connection is where settled payments go. Activating another sets its `feedFrom` to now, so switching destination never re-feeds history into the new ledger — the epic's feed-from rule. A user who wants history chooses a backfill (`POST /accounting/connections/{provider}/backfill`, #2867). Only a `connected` connection can be activated.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The now-active connection.","content":{"application/json":{"schema":{"type":"object","required":["connection"],"properties":{"connection":{"type":"object","required":["provider","displayName","authKind","status","statusReason","isActiveDestination","feedFrom","grantedScope","missingScopes","tokenExpiresAt","externalCompanyId","externalCompanyName","baseCurrency","lastPushAt","lastError","connectedAt","updatedAt","settings"],"properties":{"provider":{"type":"string","examples":["fortnox"]},"displayName":{"type":"string"},"authKind":{"type":"string","enum":["oauth2","api_key"]},"status":{"type":"string","enum":["connected","needs_reauthorisation","revoked_at_provider","scope_missing","needs_attention","disconnected"],"description":"Disconnect keeps the row as `disconnected` (history stays); `scope_missing` is set by a post-push attachment failure that needs a re-consent, by a connect whose company read was refused for scope (#2864), by a callback whose granted scope falls short of the provider's required scopes, or by a push whose create call was refused for scope (#2865). `needs_attention` (#3019) is the webhook half failing independently of the feed — the key validates and pushes work, but the subscriptions could not be created (a `webhook subscription failed` registration) or the deployment states no public API origin (`no public API origin configured`); a reconnect after fixing the deployment state resolves it. A re-consent — the same connect-url + callback on the existing connection — restores `connected` and keeps settings, feedFrom, the active flag and the sync history."},"statusReason":{"type":["string","null"]},"isActiveDestination":{"type":"boolean","description":"Exactly one connection per user is where settled payments go."},"feedFrom":{"type":["string","null"],"format":"date-time","description":"Nothing settled before this is fed. Set to now by activate (#2862) and by a company switch (#2864); moved EARLIER only by the backfill (#2867). Null on a pre-#2862 row that feeds everything."},"grantedScope":{"type":["string","null"]},"missingScopes":{"type":"array","items":{"type":"string"},"description":"#2865: the provider's required scopes the grant does not carry — derived from `grantedScope` against the descriptor's `requiredScopes`, plus the scopes a push-time refusal named while the row is `scope_missing`. Empty when nothing is missing. Non-empty on a `connected` row means the grant predates a scope widening and will degrade at the first call that needs it; a re-consent clears it."},"tokenExpiresAt":{"type":["string","null"],"format":"date-time","description":"Access-token expiry (OAuth2 providers). Null for API-key providers."},"externalCompanyId":{"type":["string","null"],"description":"The provider's own tenant id (Fortnox: `DatabaseNumber`), read at connect (#2864). A reconnect that comes back with a different id is a company switch: the row is kept, the company fields are replaced, `feedFrom` moves to now and `statusReason` names the switch. Null until a grant with the company scope read it."},"externalCompanyName":{"type":["string","null"],"description":"The company the connection points at, for \"Connected to <Company AB>\" (#2864)."},"baseCurrency":{"type":["string","null"],"description":"ISO-4217 as the provider reported it at connect, and the currency the feed pushes in (#2877). A ledger outside the supported list — SEK, EUR, USD, DKK, NOK, GBP — is refused at connect with \"Haven feeds SEK, EUR, USD, DKK, NOK and GBP ledgers\" (#2864). Null when the provider cannot say; such a connection books in SEK."},"lastPushAt":{"type":["string","null"],"format":"date-time"},"lastError":{"type":["string","null"]},"connectedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"settings":{"type":"object","required":["suggestedAccount","autoFeed"],"properties":{"suggestedAccount":{"type":["string","null"],"description":"A hint for the accountant, carried on every pushed document ONLY in the connector's non-asserting hint field (Fortnox: `YourReference: \"suggested account 6540\"`) — never as an account field; the payload guard still bans `Account`. A per-merchant override, when one exists, wins over this. Fortnox: a four-digit BAS account. Null = no hint."},"autoFeed":{"type":"boolean","description":"Default true. False = \"manual only\": the settlement hook and the background retry sweep leave this user alone; `POST /accounting/feed/sync` and the backfill still push."}}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No connection for this provider, or the accounting feature is off (#2918, same body shape as `requireAccountingFeed`).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"The connection is not in the `connected` state.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/connections/{provider}/backfill":{"post":{"tags":["Dashboard"],"operationId":"backfillAccountingConnection","summary":"Include history: move feed_from EARLIER to `since` and run one bounded sync.","description":"The user's explicit choice to feed payments settled before the connection became the destination (#2867). Every connection gets `feedFrom = now` at connect and at activate, so nothing is re-fed unasked; this is the ONE call that moves it earlier. `since` must be an ISO date in the past and not before 2020-01-01 (400 `SINCE_INVALID`), and EARLIER than the current `feedFrom` (400 `SINCE_NOT_EARLIER` — a backfill only ever includes more history; moving the floor forward is activate's job; a connection with no floor already feeds everything and is refused the same way). Only the active, `connected` destination can be backfilled (409 `NOT_ACTIVE`). The choice is recorded on the connection and one sync runs, bounded to 200 payments and resumable — press Sync now for the rest. Ignores `autoFeed: false`: this is a manual action.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["since"],"properties":{"since":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}","description":"ISO date or date-time; the new feed-from floor. (#3030: was declared `format: date-time`, which refused the plain date the description — and the dashboard — send; the pattern states the prefix and the handler decides parseability, the 2020 floor and the future bound.)","examples":["2026-01-01"]}}}}}},"responses":{"200":{"description":"The new floor, how many payments this call pushed, and how many it enumerated (0 fed is normal when the history is already pushed or empty).","content":{"application/json":{"schema":{"type":"object","required":["feedFrom","fed","total"],"properties":{"feedFrom":{"type":"string","format":"date-time","description":"The connection's feed-from after the move — `since`, normalised."},"fed":{"type":"integer","description":"Payments this call pushed (at most 200)."},"total":{"type":"integer","description":"Payments this call enumerated — the ones not in `fed` were skipped, failed, or were not feedable (no book-time SEK, below the floor)."}}}}}},"400":{"description":"`SINCE_INVALID` (not a date, in the future, before 2020-01-01) or `SINCE_NOT_EARLIER` (not earlier than the current feed-from, or the connection has no floor).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No connection for this provider, or the accounting feature is off (#2918, same body shape as `requireAccountingFeed`).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"The connection is not the active `connected` destination (`NOT_ACTIVE`).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/connections/{provider}/settings":{"patch":{"tags":["Dashboard"],"operationId":"updateAccountingConnectionSettings","summary":"Per-connection settings: suggested account and auto-feed.","description":"Exactly two keys, each optional (#2867). `suggested_account`: for Fortnox a four-digit BAS account (`^[1-8]\\d{3}$`), for other providers a non-empty string of at most 32 characters, `null` to clear; it reaches the ledger only as the connector's non-asserting hint, never as an account field. `auto_feed`: `false` makes the settlement hook and the retry sweep skip this user while Sync now and the backfill still push; absent = `true`. Any other key, or an invalid value, is a 400 that names the key. Other stored connection state (the company-switch log, the backfill record) is preserved — the write is a merge. Supplier strategy is not a setting (one supplier per merchant, fixed).","security":[{"DashboardJwt":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"suggested_account":{"type":["string","null"],"description":"Fortnox: a four-digit BAS account. Null clears.","examples":["6540"]},"auto_feed":{"type":"boolean","description":"False = manual only."}}}}}},"responses":{"200":{"description":"The connection with its settings after the merge.","content":{"application/json":{"schema":{"type":"object","required":["connection"],"properties":{"connection":{"type":"object","required":["provider","displayName","authKind","status","statusReason","isActiveDestination","feedFrom","grantedScope","missingScopes","tokenExpiresAt","externalCompanyId","externalCompanyName","baseCurrency","lastPushAt","lastError","connectedAt","updatedAt","settings"],"properties":{"provider":{"type":"string","examples":["fortnox"]},"displayName":{"type":"string"},"authKind":{"type":"string","enum":["oauth2","api_key"]},"status":{"type":"string","enum":["connected","needs_reauthorisation","revoked_at_provider","scope_missing","needs_attention","disconnected"],"description":"Disconnect keeps the row as `disconnected` (history stays); `scope_missing` is set by a post-push attachment failure that needs a re-consent, by a connect whose company read was refused for scope (#2864), by a callback whose granted scope falls short of the provider's required scopes, or by a push whose create call was refused for scope (#2865). `needs_attention` (#3019) is the webhook half failing independently of the feed — the key validates and pushes work, but the subscriptions could not be created (a `webhook subscription failed` registration) or the deployment states no public API origin (`no public API origin configured`); a reconnect after fixing the deployment state resolves it. A re-consent — the same connect-url + callback on the existing connection — restores `connected` and keeps settings, feedFrom, the active flag and the sync history."},"statusReason":{"type":["string","null"]},"isActiveDestination":{"type":"boolean","description":"Exactly one connection per user is where settled payments go."},"feedFrom":{"type":["string","null"],"format":"date-time","description":"Nothing settled before this is fed. Set to now by activate (#2862) and by a company switch (#2864); moved EARLIER only by the backfill (#2867). Null on a pre-#2862 row that feeds everything."},"grantedScope":{"type":["string","null"]},"missingScopes":{"type":"array","items":{"type":"string"},"description":"#2865: the provider's required scopes the grant does not carry — derived from `grantedScope` against the descriptor's `requiredScopes`, plus the scopes a push-time refusal named while the row is `scope_missing`. Empty when nothing is missing. Non-empty on a `connected` row means the grant predates a scope widening and will degrade at the first call that needs it; a re-consent clears it."},"tokenExpiresAt":{"type":["string","null"],"format":"date-time","description":"Access-token expiry (OAuth2 providers). Null for API-key providers."},"externalCompanyId":{"type":["string","null"],"description":"The provider's own tenant id (Fortnox: `DatabaseNumber`), read at connect (#2864). A reconnect that comes back with a different id is a company switch: the row is kept, the company fields are replaced, `feedFrom` moves to now and `statusReason` names the switch. Null until a grant with the company scope read it."},"externalCompanyName":{"type":["string","null"],"description":"The company the connection points at, for \"Connected to <Company AB>\" (#2864)."},"baseCurrency":{"type":["string","null"],"description":"ISO-4217 as the provider reported it at connect, and the currency the feed pushes in (#2877). A ledger outside the supported list — SEK, EUR, USD, DKK, NOK, GBP — is refused at connect with \"Haven feeds SEK, EUR, USD, DKK, NOK and GBP ledgers\" (#2864). Null when the provider cannot say; such a connection books in SEK."},"lastPushAt":{"type":["string","null"],"format":"date-time"},"lastError":{"type":["string","null"]},"connectedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"settings":{"type":"object","required":["suggestedAccount","autoFeed"],"properties":{"suggestedAccount":{"type":["string","null"],"description":"A hint for the accountant, carried on every pushed document ONLY in the connector's non-asserting hint field (Fortnox: `YourReference: \"suggested account 6540\"`) — never as an account field; the payload guard still bans `Account`. A per-merchant override, when one exists, wins over this. Fortnox: a four-digit BAS account. Null = no hint."},"autoFeed":{"type":"boolean","description":"Default true. False = \"manual only\": the settlement hook and the background retry sweep leave this user alone; `POST /accounting/feed/sync` and the backfill still push."}}}}}}}}}},"400":{"description":"An unknown key or an invalid value; `key` names it.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code","key"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["INVALID_SETTING"]},"key":{"type":["string","null"],"description":"The offending setting, or null when the body itself is not an object."}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No connection for this provider, or the accounting feature is off (#2918, same body shape as `requireAccountingFeed`).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounting/fortnox/push":{"post":{"tags":["Dashboard"],"operationId":"pushFortnoxVouchers","summary":"Legacy asserting voucher push — GATED OFF by default.","description":"The one Fortnox-shaped path left after #2862 replaced `/accounting/fortnox/*` with the provider-generic connections above: it is provider-specific by nature. The asserting counterpart to the accounting feed: it pushes FINISHED vouchers rather than drafts, which is exactly what #491/#492 moved away from. **410 is the normal answer on a default deployment.** When enabled, it reports per-entry outcomes rather than failing the batch: an entry with no book-time SEK amount is unbookable and counted as skipped, and a provider error is collected into failures with its payment id — so a partial push is visible as a partial push instead of an exception.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}"},"description":"ISO date."},{"name":"to","in":"query","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}"},"description":"ISO date."}],"responses":{"200":{"description":"Per-entry outcome of the push.","content":{"application/json":{"schema":{"type":"object","required":["pushed","skipped","failed","failures"],"properties":{"pushed":{"type":"integer"},"skipped":{"type":"integer","description":"Entries with no book-time SEK amount — unbookable, not errors."},"failed":{"type":"integer"},"failures":{"type":"array","items":{"type":"object","required":["paymentId","error"],"properties":{"paymentId":{"type":"string"},"error":{"type":"string"}}}}}}}}},"400":{"description":"Malformed date, or Fortnox is not connected.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"The default: pushing finished vouchers is retired in favour of the draft feed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounts/hybrid":{"post":{"tags":["Dashboard"],"operationId":"createHybridAccount","summary":"Provision a counterfactual Hybrid DeleGator account.","description":"Computes the deterministic account address for an owner configuration (an EOA, P256 passkeys, or both) and records the row. **No transaction happens here** — the address is derived, not deployed, and `deployed: false` says so; the first sponsored operation (the budget grant) deploys the code. Refusals guard the derivation's own traps: the zero address is rejected because it derives the SAME account as the pure-passkey configuration while counting as a second signer, and duplicate passkey key_ids are rejected because they collapse to one on-chain key. A single-signer account is permitted (#1153) — the backup recommendation reaches the user after funding instead of walling the first minute.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"At least one owner is required: owner_address, passkeys, or both.","properties":{"chain_id":{"type":"integer","description":"Defaults to Base Sepolia while delegation onboarding is dark-launched."},"name":{"type":"string","description":"Display label; defaults to 'My account'."},"owner_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"EOA owner. Must not be the zero address."},"passkeys":{"type":"array","items":{"type":"object","required":["key_id","x","y"],"properties":{"key_id":{"type":"string"},"x":{"type":"string","pattern":"^0x[0-9a-fA-F]{1,64}$","description":"P256 public-key x coordinate."},"y":{"type":"string","pattern":"^0x[0-9a-fA-F]{1,64}$","description":"P256 public-key y coordinate."}}}},"single_signer_waiver":{"type":"object","description":"Accepted and recorded as history, but REQUIRED FOR NOTHING (#1153) — sending it changes no outcome and omitting it changes no outcome. Kept on the request shape so existing clients keep working.","properties":{"acknowledged":{"type":"boolean"}}}}}}}},"responses":{"201":{"description":"Account row recorded. Counterfactual — nothing was deployed.","content":{"application/json":{"schema":{"type":"object","required":["id","account_address","chain_id","account_type","deployed","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"account_type":{"type":"string","enum":["delegator_hybrid"]},"deployed":{"type":"boolean","enum":[false],"description":"Always false here — deployment rides the first sponsored operation."},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad owner configuration, or a chain the delegation rail does not serve.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"This account is already registered for the caller.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"The account address could not be derived.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounts/hybrid/{address}/signers":{"get":{"tags":["Dashboard"],"operationId":"getHybridAccountSigners","summary":"Read an account's signer set (public key material only).","description":"The account-scoped twin of the agent-scoped read, and it exists for two cases the agent route cannot serve: resolving a signer at LOGIN, before any agent exists, and giving account-level recovery a data source for an account with zero agents. Owner-scoped; nothing secret — an address and P256 public-key coordinates. The exact configuration the address was derived FROM, so a client can rebuild the same signer.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"The hybrid account address."},{"name":"chain_id","in":"query","schema":{"type":"string"},"description":"Required — the (address, chain) pair identifies the account."}],"responses":{"200":{"description":"The signer set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HybridAccountSigners"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"The account signer configuration is unknown.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounts/hybrid/{address}/signers/prepare":{"post":{"tags":["Dashboard"],"operationId":"prepareHybridSignerChange","summary":"Prepare a signer-set change for an EXISTING signer to sign.","description":"Enroll a backup passkey or EOA, or remove one. Haven prepares; an existing signer signs — Haven never signs a signer change. The chain's own last-signer rule is mirrored as a clear 409 rather than an opaque revert. The response branches on the signature scheme the device can satisfy.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"The hybrid account address."},{"name":"chain_id","in":"query","schema":{"type":"string"},"description":"Required — the (address, chain) pair identifies the account."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["add_passkey","remove_passkey","add_owner","remove_owner"]},"passkey":{"type":"object","description":"Required for add_passkey/remove_passkey.","properties":{"key_id":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"}}},"owner_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Required for add_owner."},"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"]}}}}}},"responses":{"200":{"description":"Prepared change, shaped by the signature scheme. Carries no treasury_address.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["signature_scheme","signing_payload","user_operation","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"signing_payload":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true},"instructions":{"type":"string"}}},{"type":"object","required":["signature_scheme","user_op_hash","user_operation","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["webauthn_userop"]},"user_op_hash":{"type":"string"},"user_operation":{"type":"object","additionalProperties":true},"instructions":{"type":"string"}}}]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounts/hybrid/{address}/signers/submit":{"post":{"tags":["Dashboard"],"operationId":"submitHybridSignerChange","summary":"Submit the signed signer-set change.","description":"Envelope first, account second — a malformed body is a 400 regardless of account state. Storage syncs only after the on-chain operation succeeds, and the sync is pinned to the SIGNED calldata: a user_operation that does not match the signed action is refused rather than trusted.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"The hybrid account address."},{"name":"chain_id","in":"query","schema":{"type":"string"},"description":"Required — the (address, chain) pair identifies the account."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature","user_operation"],"properties":{"action":{"type":"string","enum":["add_passkey","remove_passkey","add_owner","remove_owner"]},"passkey":{"type":"object","description":"Required for add_passkey/remove_passkey.","properties":{"key_id":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"}}},"owner_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Required for add_owner."},"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"]},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"},"user_operation":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"Signer set updated on-chain and in storage.","content":{"application/json":{"schema":{"type":"object","required":["updated","tx_hash"],"properties":{"updated":{"type":"boolean"},"tx_hash":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounts/hybrid/{address}/transfers/prepare":{"post":{"tags":["Dashboard"],"operationId":"prepareHybridTransfer","summary":"MONEY PATH: prepare an owner-signed ERC-20 transfer from the account.","description":"The rail's Send. The treasury executes the transfer as a sponsored account operation **the OWNER signs** — the same prepare/submit and calldata-pinning discipline as a signer change, and the same device-decides scheme. Haven constructs and relays; it never signs, so it cannot move these funds on its own. Works on a counterfactual account: deploy and transfer ride one sponsored operation. Rate-limited on the money-path limiter.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"The hybrid account address."},{"name":"chain_id","in":"query","schema":{"type":"string"},"description":"Required — the (address, chain) pair identifies the account."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token_address","to","amount_atomic"],"properties":{"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Recipient."},"amount_atomic":{"type":"string","pattern":"^[0-9]+$","description":"Atomic units."},"signature_scheme":{"type":"string","enum":["eip712_userop","webauthn_userop"]}}}}}},"responses":{"200":{"description":"Prepared transfer, shaped by the signature scheme.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["signature_scheme","signing_payload","user_operation","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"signing_payload":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true},"instructions":{"type":"string"}}},{"type":"object","required":["signature_scheme","user_op_hash","user_operation","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["webauthn_userop"]},"user_op_hash":{"type":"string"},"user_operation":{"type":"object","additionalProperties":true},"instructions":{"type":"string"}}}]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/accounts/hybrid/{address}/transfers/submit":{"post":{"tags":["Dashboard"],"operationId":"submitHybridTransfer","summary":"MONEY PATH: submit the owner-signed transfer.","description":"Relays the signed operation. The calldata is pinned to what was signed, so the submitted transfer is the one the owner approved — a different recipient or amount is refused, not relayed. Rate-limited on the money-path limiter.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"The hybrid account address."},{"name":"chain_id","in":"query","schema":{"type":"string"},"description":"Required — the (address, chain) pair identifies the account."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature","user_operation"],"properties":{"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"amount_atomic":{"type":"string","pattern":"^[0-9]+$"},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"},"user_operation":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"Transfer submitted on-chain.","content":{"application/json":{"schema":{"type":"object","required":["submitted","tx_hash"],"properties":{"submitted":{"type":"boolean"},"tx_hash":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/auth/signup":{"post":{"tags":["Dashboard"],"operationId":"signup","summary":"Create an account and return a session token.","description":"The email is NORMALISED before the uniqueness check, deliberately: an exact match on the raw input would let `ADA@Example.com` register alongside a stored `ada@example.com`, giving one person two accounts and two treasuries. Password bounds are 8-128 characters. The returned user is a fixed new-account shape — no wallet, no Safe, USD, an empty safes list — because none of those exist yet. **The 409 for a taken address is a deliberate, ACCEPTED disclosure (#1654):** one unauthenticated request tells the caller whether an email has a Haven account, which is stronger than the oracle #1646 closed on login. It stands because telling a returning user \"you already have an account — sign in instead\" is real product value, and because the standard mitigation is structurally unavailable here: a 201 carries the SESSION TOKEN, so answering 201 for a taken address would either mint a token for someone else's account or return a token-less 201 that is an equally strong oracle — genuine hardening needs an email-verification channel this API does not have. Bulk probing is additionally rate limited (#1670) — 10 requests/minute per client address, in deployments where TRUST_PROXY_HOPS is set so per-IP means the CLIENT rather than one shared proxy bucket; the tier deliberately disarms itself otherwise, since a shared-bucket limit on the front door is a denial-of-service, not a protection. Throttled is not closed: one request against one address still answers definitively, which is what the acceptance above is about. If an email channel is ever added, revisit this tradeoff in the same change. Timing is not equalised on this route because the status already discloses account existence — the clock has nothing to add beyond what the 409 already says.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email","password"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"Trimmed; control characters are rejected."},"email":{"type":"string","maxLength":255},"password":{"type":"string","minLength":8,"maxLength":128},"via":{"type":"string","description":"Agent hand-off marker (#2522): the dashboard sends `agent` when the signup came from an agent-initiated link. Sanitised server-side to `agent` or nothing; any other value is ignored. Declared in #3030 — the dashboard had been sending it undeclared."}}}}}},"responses":{"201":{"description":"Account created; the token is valid for 7 days.","content":{"application/json":{"schema":{"type":"object","required":["token","user"],"properties":{"token":{"type":"string"},"user":{"type":"object","required":["id","name","email","wallet_address","account_address","currency_preference","accounts"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":"string"},"wallet_address":{"type":["string","null"]},"account_address":{"type":["string","null"]},"currency_preference":{"type":"string"},"accounts":{"type":"array","items":{"type":"object","required":["id","account_address","chain_id","name","is_default","created_at","account_type","value_bearing_chain","needs_backup_recommendation"],"properties":{"id":{"type":"string","format":"uuid"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"name":{"type":"string"},"is_default":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"account_type":{"type":["string","null"]},"value_bearing_chain":{"type":"boolean","description":"Derived from the chain — is real value at stake here."},"needs_backup_recommendation":{"type":["boolean","null"],"description":"Delegation-rail accounts only (null otherwise): whether to RECOMMEND a backup signer. A recommendation, never a gate (#1153) — nothing refuses a single-signer account."}}}}}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"An account with this email already exists. A deliberate, documented enumeration disclosure — see this operation's description (#1654).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Rate limited (10/min per client address, #1670) — only in deployments with a trusted proxy.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/auth/login":{"post":{"tags":["Dashboard"],"operationId":"login","summary":"Exchange credentials for a session token.","description":"**An unknown email and a wrong password return the SAME 401**, deliberately: distinguishing them would turn this endpoint into an account-enumeration oracle, letting anyone discover which addresses have Haven accounts. Do not make the error message more helpful. The protection covers timing as well as the status and body (#1646): the password comparison runs on BOTH paths — against an absent-user hash when there is no account — so the answer costs materially the same either way (the remaining difference is the sub-millisecond indexed lookup, far below bcrypt cost). The token is valid for 7 days, and the response carries the user's Safes so a client needs no second call to render a session.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"Session established.","content":{"application/json":{"schema":{"type":"object","required":["token","user"],"properties":{"token":{"type":"string"},"user":{"type":"object","required":["id","name","email","wallet_address","account_address","currency_preference","accounts"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":"string"},"wallet_address":{"type":["string","null"]},"account_address":{"type":["string","null"]},"currency_preference":{"type":"string"},"accounts":{"type":"array","items":{"type":"object","required":["id","account_address","chain_id","name","is_default","created_at","account_type","value_bearing_chain","needs_backup_recommendation"],"properties":{"id":{"type":"string","format":"uuid"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"name":{"type":"string"},"is_default":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"account_type":{"type":["string","null"]},"value_bearing_chain":{"type":"boolean","description":"Derived from the chain — is real value at stake here."},"needs_backup_recommendation":{"type":["boolean","null"],"description":"Delegation-rail accounts only (null otherwise): whether to RECOMMEND a backup signer. A recommendation, never a gate (#1153) — nothing refuses a single-signer account."}}}}}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Invalid email or password — one answer for both, on purpose.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Rate limited (30/min per client address, #1670) — only in deployments with a trusted proxy.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/auth/me":{"get":{"tags":["Dashboard"],"operationId":"getSession","summary":"Read the authenticated session: profile plus Safes.","description":"Both reads are scoped to the JWT subject, never to a client-supplied id. Returns the FULL profile row (including created_at, unlike the login and signup user object) plus the same Safe list. A 404 here means the account was deleted while a valid token for it was still in flight.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"The session.","content":{"application/json":{"schema":{"type":"object","required":["id","name","email","wallet_address","account_address","currency_preference","created_at","accounts"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":"string"},"wallet_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$"},"account_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$"},"currency_preference":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"accounts":{"type":"array","items":{"type":"object","required":["id","account_address","chain_id","name","is_default","created_at","account_type","value_bearing_chain","needs_backup_recommendation"],"properties":{"id":{"type":"string","format":"uuid"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"name":{"type":"string"},"is_default":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"account_type":{"type":["string","null"]},"value_bearing_chain":{"type":"boolean","description":"Derived from the chain — is real value at stake here."},"needs_backup_recommendation":{"type":["boolean","null"],"description":"Delegation-rail accounts only (null otherwise): whether to RECOMMEND a backup signer. A recommendation, never a gate (#1153) — nothing refuses a single-signer account."}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"The account no longer exists.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/passkeys":{"get":{"tags":["Dashboard"],"operationId":"listPasskeys","summary":"List the caller's enrolled passkeys.","description":"Wider than the create response: it also carries the bound Safe address and the enrollment time. Still no key material.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Enrolled passkeys.","content":{"application/json":{"schema":{"type":"object","required":["passkeys"],"properties":{"passkeys":{"type":"array","items":{"type":"object","required":["id","credential_id","signer_address","chain_id","account_address","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"credential_id":{"type":"string"},"signer_address":{"type":"string"},"chain_id":{"type":"integer"},"account_address":{"type":["string","null"],"description":"Null until the passkey is bound to an account."},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-activity/{id}/activity":{"get":{"tags":["Dashboard"],"operationId":"getAgentActivity","summary":"One agent's payments and tool calls, newest first.","description":"A heterogeneous list discriminated by `type`: payment or mcp_tool_call. (#2262: the third branch, `approval`, is gone — #2055 dropped `approval_requests` and this handler merges `payment_intents` and the MCP tool-call audit log, with no third source.) **Read the pagination carefully — it is approximate by construction.** `limit` is applied to EACH of the two sources separately and the results are then merged and sorted, so this route can return up to twice `limit` entries, and `offset` walks each source independently rather than the merged sequence. (The combined feed below merges the same way but then truncates to `limit`, so the two routes do NOT paginate identically.) Treat the list as a recent-activity window, not as a stable paged sequence.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Agent id."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":30},"description":"Capped at 100."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":9007199254740991,"default":0}}],"responses":{"200":{"description":"Merged activity, newest first.","content":{"application/json":{"schema":{"type":"object","required":["activity"],"properties":{"activity":{"type":"array","items":{"oneOf":[{"type":"object","required":["type","id","token","amount","to","status","created_at"],"properties":{"type":{"type":"string","enum":["payment"]},"id":{"type":"string"},"token":{"type":["string","null"]},"amount_raw":{"type":["string","null"]},"amount":{"type":["string","null"]},"approx_amount":{"type":["string","null"],"description":"#3824: the token amount at today’s rate in `approx_currency`; null when no usable price exists — never \"0\"."},"approx_currency":{"type":"string","enum":["SEK","USD","EUR"],"description":"#3824: the currency `approx_amount` is denominated in (the user’s `currency_preference`)."},"to":{"type":["string","null"]},"status":{"type":["string","null"]},"tx_hash":{"type":["string","null"],"description":"The FUNDING transaction on the eip3009 bridge — Haven’s sponsored UserOp (account → delegate)."},"funding_tx_hash":{"type":["string","null"],"description":"#3763: the EIP-3009 funding leg, named — same value as `tx_hash`."},"settlement_tx_hash":{"type":["string","null"],"description":"#3763: the merchant’s settlement transaction (delegate → merchant) when an agent reported one or the backend’s settlement sweep detected it on-chain (#3888), and it verified on-chain (#3475, `machine_metadata.merchant_settlement_tx_hash`). Null is \"not recorded\", never \"failed\" — the SDK’s default evidence post reports the funding hash, which the writer refuses, so many eip3009 payments legitimately never get one."},"settlement_scheme":{"type":["string","null"],"description":"#3763: which settlement branch moved the money — `eip3009` or `erc7710` — read from the intent’s `machine_metadata`; null when none was recorded."},"payment_id":{"type":"string"},"payment_proof_status":{"type":["string","null"]},"payment_flow_status":{"type":["string","null"],"description":"Derived from the payment lifecycle."},"payment_attention_reason":{"type":["string","null"],"description":"Derived: why this payment needs a human look, if it does."},"source":{"type":"string","description":"Falls back to 'direct'."},"x402_resource_url":{"type":["string","null"]},"x402_merchant_address":{"type":["string","null"]},"delivery_reference":{"type":["string","null"],"description":"#3778: the non-secret delivery pointer reported with an accepted x402 outcome (merchant, product, value, order id); null when none was reported. Never a redemption code or other credential — those are refused at write."},"chain_id":{"type":["integer","null"]},"token_address":{"type":["string","null"]},"account_id":{"type":["string","null"]},"account_address":{"type":["string","null"]},"account_name":{"type":["string","null"]},"explorer_url":{"type":["string","null"],"description":"#3763: the merchant settlement transaction when one is recorded, else the funding transaction; null exactly when the payment has no transaction at all (`tx_hash` null and nothing recorded)."},"execution_rail":{"type":["string","null"],"description":"Which on-chain mechanism moved the money (#799)."},"delegation_hash":{"type":["string","null"],"description":"Which delegation authorized a delegation-rail payment (#829)."},"confirmed_at":{"type":["string","null"]},"created_at":{"type":"string"},"agent_id":{"type":"string","description":"Feed only — the per-agent list already knows the agent."},"agent_name":{"type":"string","description":"Feed only; 'Unknown' when the agent row is gone."}}},{"type":"object","required":["type","id","tool_name","created_at"],"properties":{"type":{"type":"string","enum":["mcp_tool_call"]},"id":{"type":"string"},"tool_name":{"type":"string"},"payment_id":{"type":["string","null"],"description":"Set when the call created or advanced a payment."},"result_status":{"type":["string","null"]},"next_action":{"type":["string","null"]},"error_code":{"type":["string","null"]},"status_code":{"type":["integer","null"]},"created_at":{"type":"string"},"agent_id":{"type":"string","description":"Feed only."},"agent_name":{"type":"string","description":"Feed only."}}}]}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No such agent for this caller.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-activity/{id}/stats":{"get":{"tags":["Dashboard"],"operationId":"getAgentStats","summary":"One agent's spend totals per token, plus its pending-approval count.","description":"Totals count CONFIRMED spend only, so an in-flight payment does not inflate them. Each window (all time, today, this week) is a separate per-token list rather than one list with three numbers, because a token can appear in one window and not another.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Agent id."}],"responses":{"200":{"description":"Spend totals and the pending count.","content":{"application/json":{"schema":{"type":"object","required":["all_time","today","this_week","pending_approvals"],"properties":{"all_time":{"type":"array","items":{"type":"object","required":["token","total_spent","tx_count"],"properties":{"token":{"type":["string","null"]},"total_spent":{"type":["string","null"]},"tx_count":{"type":"integer"}}}},"today":{"type":"array","items":{"type":"object","required":["token","total_spent","tx_count"],"properties":{"token":{"type":["string","null"]},"total_spent":{"type":["string","null"]},"tx_count":{"type":"integer"}}}},"this_week":{"type":"array","items":{"type":"object","required":["token","total_spent","tx_count"],"properties":{"token":{"type":["string","null"]},"total_spent":{"type":["string","null"]},"tx_count":{"type":"integer"}}}},"pending_approvals":{"type":"integer","description":"Always 0 since #2055 — the approval queue died with the Safe rail; kept for wire compatibility."}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No such agent for this caller.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-activity/feed":{"get":{"tags":["Dashboard"],"operationId":"getActivityFeed","summary":"Combined activity across every agent the caller owns.","description":"The same two entry types as the per-agent list, each additionally carrying agent_id and agent_name so the feed can attribute a row without a second lookup ('Unknown' when the agent row is gone — the activity stays visible). Unlike the per-agent route, the merged list IS truncated to `limit`. A caller with no agents gets an empty list and a zero count rather than an error. `pending_approvals` is always 0 since #2055 (the approval queue died with the Safe rail); the field survives for wire compatibility.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":30},"description":"Capped at 100."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":9007199254740991,"default":0}}],"responses":{"200":{"description":"Merged cross-agent activity plus the actionable-approval count.","content":{"application/json":{"schema":{"type":"object","required":["activity","pending_approvals"],"properties":{"activity":{"type":"array","items":{"oneOf":[{"type":"object","required":["type","id","token","amount","to","status","created_at"],"properties":{"type":{"type":"string","enum":["payment"]},"id":{"type":"string"},"token":{"type":["string","null"]},"amount_raw":{"type":["string","null"]},"amount":{"type":["string","null"]},"approx_amount":{"type":["string","null"],"description":"#3824: the token amount at today’s rate in `approx_currency`; null when no usable price exists — never \"0\"."},"approx_currency":{"type":"string","enum":["SEK","USD","EUR"],"description":"#3824: the currency `approx_amount` is denominated in (the user’s `currency_preference`)."},"to":{"type":["string","null"]},"status":{"type":["string","null"]},"tx_hash":{"type":["string","null"],"description":"The FUNDING transaction on the eip3009 bridge — Haven’s sponsored UserOp (account → delegate)."},"funding_tx_hash":{"type":["string","null"],"description":"#3763: the EIP-3009 funding leg, named — same value as `tx_hash`."},"settlement_tx_hash":{"type":["string","null"],"description":"#3763: the merchant’s settlement transaction (delegate → merchant) when an agent reported one or the backend’s settlement sweep detected it on-chain (#3888), and it verified on-chain (#3475, `machine_metadata.merchant_settlement_tx_hash`). Null is \"not recorded\", never \"failed\" — the SDK’s default evidence post reports the funding hash, which the writer refuses, so many eip3009 payments legitimately never get one."},"settlement_scheme":{"type":["string","null"],"description":"#3763: which settlement branch moved the money — `eip3009` or `erc7710` — read from the intent’s `machine_metadata`; null when none was recorded."},"payment_id":{"type":"string"},"payment_proof_status":{"type":["string","null"]},"payment_flow_status":{"type":["string","null"],"description":"Derived from the payment lifecycle."},"payment_attention_reason":{"type":["string","null"],"description":"Derived: why this payment needs a human look, if it does."},"source":{"type":"string","description":"Falls back to 'direct'."},"x402_resource_url":{"type":["string","null"]},"x402_merchant_address":{"type":["string","null"]},"delivery_reference":{"type":["string","null"],"description":"#3778: the non-secret delivery pointer reported with an accepted x402 outcome (merchant, product, value, order id); null when none was reported. Never a redemption code or other credential — those are refused at write."},"chain_id":{"type":["integer","null"]},"token_address":{"type":["string","null"]},"account_id":{"type":["string","null"]},"account_address":{"type":["string","null"]},"account_name":{"type":["string","null"]},"explorer_url":{"type":["string","null"],"description":"#3763: the merchant settlement transaction when one is recorded, else the funding transaction; null exactly when the payment has no transaction at all (`tx_hash` null and nothing recorded)."},"execution_rail":{"type":["string","null"],"description":"Which on-chain mechanism moved the money (#799)."},"delegation_hash":{"type":["string","null"],"description":"Which delegation authorized a delegation-rail payment (#829)."},"confirmed_at":{"type":["string","null"]},"created_at":{"type":"string"},"agent_id":{"type":"string","description":"Feed only — the per-agent list already knows the agent."},"agent_name":{"type":"string","description":"Feed only; 'Unknown' when the agent row is gone."}}},{"type":"object","required":["type","id","tool_name","created_at"],"properties":{"type":{"type":"string","enum":["mcp_tool_call"]},"id":{"type":"string"},"tool_name":{"type":"string"},"payment_id":{"type":["string","null"],"description":"Set when the call created or advanced a payment."},"result_status":{"type":["string","null"]},"next_action":{"type":["string","null"]},"error_code":{"type":["string","null"]},"status_code":{"type":["integer","null"]},"created_at":{"type":"string"},"agent_id":{"type":"string","description":"Feed only."},"agent_name":{"type":"string","description":"Feed only."}}}]}},"pending_approvals":{"type":"integer","description":"Always 0 since #2055 — the approval queue died with the Safe rail; kept for wire compatibility."}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/analytics/funnel":{"get":{"tags":["Dashboard"],"operationId":"getOnboardingFunnel","summary":"Onboarding step conversion and median time-to-first-payment.","description":"Counts DISTINCT users per onboarding step over a date range, with the conversion from the previous step, plus the median time from signup to first settled payment. Defaults to the last 30 days. The window is echoed back as resolved ISO timestamps so a caller can tell exactly which range produced the numbers rather than re-deriving the default.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string"},"description":"Date; defaults to 30 days before `to`."},{"name":"to","in":"query","schema":{"type":"string"},"description":"Date; defaults to now."},{"name":"segment","in":"query","schema":{"type":"string","enum":["via","run_mode"]},"description":"Split the same steps by one dimension (#2529), added as `segments` alongside the unsegmented `steps`. `via` is the agent hand-off marker — it reads the funnel metadata key `handoff_via`, NOT the key literally named `via`, which predates it and records which CODE PATH created the record ('connection_setup' from the connect flow). Segmenting on that one would answer 'connection_setup' for every connect-modal agent and look like a working metric. `run_mode` is how the connector was invoked ('json' | 'prose', #2528). Attribution is resolved once PER USER from the earliest event in the window carrying the key, then carried across every step: only `signed_up` and `agent_created` write these keys, so a per-event split would report zero agent-driven first payments and read as 'agents never convert'. A user with no value for the key lands in the `unattributed` group — for `run_mode` that means a connector predating #2528, which is NOT the same fact as a `prose` run; the same definition is repeated on `segments.value` so a consumer reading only one of the two fields still learns it. Omitting the parameter returns the unsegmented body unchanged; an unrecognised value is a 400 rather than a silent fall-back to unsegmented."}],"responses":{"200":{"description":"Funnel counts and median TTFP.","content":{"application/json":{"schema":{"type":"object","required":["steps","medianTtfpMs","from","to"],"properties":{"steps":{"type":"array","items":{"type":"object","required":["event","users","conversionFromPrev"],"properties":{"event":{"type":"string","enum":["signed_up","safe_deployed","safe_imported","agent_created","allowance_granted","safe_funded","first_payment_settled"],"description":"Retained for wire compatibility with historical windows. THREE of these steps are permanently zero for any window after their retirement and a funnel built from them will show three dead stages: 'safe_deployed' and 'safe_imported' (410 since #1984 — Safe inflow is retired) and 'allowance_granted' (#2020 — the AllowanceModule rail no longer grants). Historical rows before those dates still count."},"users":{"type":"integer","description":"DISTINCT users who reached this step."},"conversionFromPrev":{"type":["number","null"],"description":"Null when there is nothing to convert from: the first step, or any step whose predecessor counted zero users."}}}},"medianTtfpMs":{"type":["integer","null"],"description":"Median signup→first-settled-payment in ms. Null when nobody has completed it in the window."},"from":{"type":"string","format":"date-time","description":"The resolved window start."},"to":{"type":"string","format":"date-time","description":"The resolved window end."},"segment":{"type":"string","enum":["via","run_mode"],"description":"Echoed back only when the request asked for a split. Absent otherwise, together with `segments`."},"segments":{"type":"array","description":"Present only when `segment` was requested. One entry per distinct value of that dimension, each carrying the SAME seven steps as the unsegmented `steps`, so a group reads exactly like a funnel. Sorted alphabetically with `unattributed` last — that bucket means the user has no value for the key at all, which for `run_mode` is a connector predating #2528 and is NOT the same fact as 'prose'. Summing a step across groups equals the unsegmented count for that step.","items":{"type":"object","required":["value","steps"],"properties":{"value":{"type":"string","description":"The dimension's value for this group, or 'unattributed' when the user carries none."},"steps":{"type":"array","items":{"type":"object","required":["event","users","conversionFromPrev"],"properties":{"event":{"type":"string","enum":["signed_up","safe_deployed","safe_imported","agent_created","allowance_granted","safe_funded","first_payment_settled"]},"users":{"type":"integer","description":"DISTINCT users in this group who reached this step."},"conversionFromPrev":{"type":["number","null"],"description":"Null when the predecessor step counted zero users in THIS group — which includes the three permanently-zero retired stages, so the step after each of them is null by construction rather than by absence of data."}}}}}}}}}}}},"400":{"description":"Unparseable dates, or from is not before to.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/analytics/overview":{"get":{"tags":["Dashboard"],"operationId":"getAnalyticsOverview","summary":"One range-scoped aggregate: spend, refusals, fees, gas, budgets and balance.","description":"Everything the `/analytics` page renders in one round trip, so the page has one loading state and one \"based on N payments\" basis (#2946, epic #2944 slice B). Sums are over `payment_intents` rows with `status = 'confirmed'` ONLY — fiat values are booked by the confirm UPDATE, so `pending_signature`/`submitted`/`failed`/`expired` rows carry NULL and never count. Spend figures are additionally NET of `delegate_sweeps` clawbacks (#3755): per (agent, token, chain) group, gross × (1 − min(1, swept_atomic/confirmed_atomic)) over sweeps that actually returned funds (`status = 'submitted'` with a `tx_hash`, anchored on `submitted_at` in the same window) — a merchant-refused funding leg whose stranded amount was swept back to the treasury does not count as spent. `payments`/`payments_counted` and `last_payment_at` stay gross: they count funding legs made, not money kept. `basis.unsettled_submitted` separately counts `submitted` rows in range so the page can say how many payments are awaiting settlement evidence. Fees are Haven's own fee (`payment_fees.fee_amount_atomic`), valued with the intent's booked fiat, `0` honestly while the flag is off. Gas is a sponsored-operation COUNT on value-bearing chains only — never a fiat figure. Budget-used is read from the chain per active delegation, never summed from intents. `tz` (default UTC) buckets `by_day` server-side, using the same zone Postgres and this validator agree on (an IANA name only — `tz` rejects UTC offsets and fixed abbreviations, which Postgres and JavaScript can interpret with opposite sign conventions); `range.from`/`to` are UTC instants regardless of `tz`. Because `range.from`/`to` are fixed UTC instants, `by_day`'s FIRST and LAST buckets can be PARTIAL under a non-UTC `tz` (they cover less than a full local day) — this is expected, not a bug, and the page should treat the edge buckets as partial. `balance_by_day` is unaffected: `user_daily_portfolio_snapshots` is a UTC-dated daily snapshot, produced once per day regardless of the caller's `tz` — except under `currency=sek`, where days snapshotted before the `total_sek` column existed (#3127, migration 090) carry no SEK figure and are OMITTED from the series rather than zeroed. The same honesty applies to the SEK SUMS under `currency=sek` (#3127, round-2 review): the totals, per-agent, top-merchant and fees figures sum `sek_value`, which is NULL for a confirmed row whose book-time SEK could not be captured or backfilled — those rows are still counted in `basis.payments_counted` but contribute nothing to any SEK sum, so a SEK total can sit below the basis it is computed over. USD and EUR are unaffected (their booking predates the capture gate). Delegation-rail accounts only.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"range","in":"query","required":true,"schema":{"type":"string","enum":["7d","30d","90d"]},"description":"Window length ending now."},{"name":"currency","in":"query","schema":{"type":"string","enum":["usd","eur","sek"],"default":"usd"},"description":"Display currency — a sum of already-booked values, never re-converted. SEK reads the `sek_value` column booked beside usd/eur by the same confirm UPDATE (#3127); days snapshotted before that column existed carry no SEK figure in `balance_by_day` and are omitted rather than zeroed."},{"name":"tz","in":"query","schema":{"type":"string","default":"UTC"},"description":"IANA time zone used to bucket `by_day`. Defaults to UTC; an unrecognized zone is a 400."}],"responses":{"200":{"description":"The full analytics-overview aggregate for the requested window.","content":{"application/json":{"schema":{"type":"object","required":["range","currency","basis","totals","by_day","agents","merchants","balance_by_day"],"properties":{"range":{"type":"object","required":["from","to","days","previous_from","previous_to"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"days":{"type":"integer","enum":[7,30,90]},"previous_from":{"type":"string","format":"date-time"},"previous_to":{"type":"string","format":"date-time"}}},"currency":{"type":"string","enum":["usd","eur","sek"]},"basis":{"type":"object","required":["payments_counted","unsettled_submitted","refusals_counted","refusal_attempts","fee_rows","gas_sponsored_ops","snapshot_days","tz","refusals_recorded_from"],"properties":{"payments_counted":{"type":"integer","description":"CONFIRMED payments summed into `totals.spent`."},"unsettled_submitted":{"type":"integer","description":"`submitted` rows in range, not counted as spend."},"refusals_counted":{"type":"integer","description":"Distinct `payment_refusals` rows (the dedupe makes rows != attempts)."},"refusal_attempts":{"type":"integer"},"fee_rows":{"type":"integer"},"gas_sponsored_ops":{"type":"integer"},"snapshot_days":{"type":"integer"},"tz":{"type":"string","description":"The zone actually used to bucket `by_day` — UTC when the request gave none."},"refusals_recorded_from":{"type":["string","null"],"description":"The ledger floor (#3013): the earliest `payment_refusals` day (UTC `YYYY-MM-DD`) with rows, inside NO window bound — a property of the ledger, not of the requested range. `null` only when the ledger has no rows at all, so a window behind the floor reads a coverage caveat (\"nothing was recorded\"), not a clean zero."}}},"totals":{"type":"object","required":["spent","spent_previous","refused_count","refused_attempts","refused_amount","refused_previous_count","budget_bands","fees","gas_sponsored_ops"],"properties":{"spent":{"type":"string","description":"Booked fiat of CONFIRMED payments, NET of `delegate_sweeps` clawbacks (#3755): per (agent, token, chain) group, gross × (1 − min(1, swept_atomic/confirmed_atomic)) over submitted sweeps in the window — clawed-back funding legs do not count as spent."},"spent_previous":{"type":"string","description":"Same net-of-sweeps basis as `spent`, over the previous window."},"refused_count":{"type":"integer"},"refused_attempts":{"type":"integer"},"refused_amount":{"type":"string","description":"Attempted amount — never \"saved\". A numeric string like every other money field on this response."},"refused_previous_count":{"type":"integer"},"budget_bands":{"type":"object","required":["above_75","above_50","agents_with_budget"],"properties":{"above_75":{"type":"integer","description":"Agents whose worst active delegation's used/budget ratio exceeds 75%."},"above_50":{"type":"integer"},"agents_with_budget":{"type":"integer"}}},"fees":{"type":"object","required":["amount","previous","flag_on"],"properties":{"amount":{"type":"string","description":"\"0\" while the fee flag is off — honest, not a placeholder."},"previous":{"type":"string"},"flag_on":{"type":"boolean"}}},"gas_sponsored_ops":{"type":"integer","description":"A COUNT on value-bearing chains only — never a fiat figure."}}},"by_day":{"type":"array","items":{"type":"object","required":["date","spent_by_agent","refusals"],"properties":{"date":{"type":"string","description":"YYYY-MM-DD in the `tz` zone."},"spent_by_agent":{"type":"object","additionalProperties":{"type":"string"}},"refusals":{"type":"integer"}}}},"agents":{"type":"array","description":"The tenant's delegation-rail agents, unordered. An agent is listed when it is not revoked (even with no activity in range), when it had a confirmed payment or a refusal in range, or when it is revoked but still holds an active budget delegation (revoking flips only the agent's status). A revoked agent with none of these is omitted (#3540).","items":{"type":"object","required":["id","name","status","spent","share","payments","refusals","refusal_attempts","budgets","top_merchant","last_payment_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string"},"spent":{"type":"string","description":"Net of `delegate_sweeps` clawbacks (#3755), same basis as `totals.spent`; `payments` and `last_payment_at` stay gross."},"share":{"type":"number","description":"This agent’s share of total spend across delegation-rail agents, in [0, 1]."},"payments":{"type":"integer"},"refusals":{"type":"integer"},"refusal_attempts":{"type":"integer"},"budgets":{"type":"array","items":{"type":"object","required":["token","recipient","used_atomic","budget_atomic","remaining_from_chain","period_start","period_end"],"properties":{"token":{"type":"string"},"recipient":{"type":["string","null"]},"used_atomic":{"type":"string"},"budget_atomic":{"type":"string"},"remaining_from_chain":{"type":"boolean","description":"False when the on-chain read fell back to the configured budget."},"period_start":{"type":"string","format":"date-time"},"period_end":{"type":"string","format":"date-time"}}}},"top_merchant":{"type":["object","null"],"required":["label","address"],"properties":{"label":{"type":"string"},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}}},"last_payment_at":{"type":["string","null"],"format":"date-time"}}}},"merchants":{"type":"array","description":"Top 10 by spend.","items":{"type":"object","required":["label","address","spent","payments","agent_ids","first_seen","last_seen"],"properties":{"label":{"type":"string","description":"The user’s contact name where the address matches, else the receipt’s merchant name, else the address."},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"spent":{"type":"string"},"payments":{"type":"integer"},"agent_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"first_seen":{"type":"string","format":"date-time"},"last_seen":{"type":"string","format":"date-time"}}}},"balance_by_day":{"type":"array","items":{"type":"object","required":["date","value"],"properties":{"date":{"type":"string"},"value":{"type":"string"}}}}}}}}},"400":{"description":"range/currency outside the enum, or tz is not a recognized IANA zone.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/chains":{"get":{"tags":["Health"],"operationId":"getChains","summary":"PUBLIC: which chains this deployment serves.","description":"Two different lists, and the difference matters: `supported` is every chain Haven runs on (Base and Base Sepolia; Gnosis is history-only and not listed), while `deployable` is the subset this environment will actually provision accounts on (#679). Onboarding pickers must offer `deployable`, not `supported`, or they will offer a chain the deployment refuses. No authentication — it is configuration, not data.","security":[],"responses":{"200":{"description":"The chain registry for this deployment.","content":{"application/json":{"schema":{"type":"object","required":["deployable","supported"],"properties":{"deployable":{"type":"array","items":{"type":"integer"},"description":"Chains this environment will provision on."},"supported":{"type":"array","items":{"type":"integer"},"description":"Chains Haven runs on (Base and Base Sepolia). History-only chains such as Gnosis are not listed."}}}}}}}}},"/x402/{id}/settle":{"post":{"tags":["x402"],"operationId":"settleX402Payment","summary":"MONEY PATH: settle a delegation-rail x402 payment with the delegate signature.","description":"The delegation rail's settlement step, and the reason the rail has no funding leg: the agent signs the settlement child delegation, Haven assembles the merchant X-PAYMENT header, and the merchant redeems the chain directly from the budget delegation — **money moves account→merchant, never through a delegate hot balance**. Retry the merchant with the returned `payment_header`; it is a signed, single-use, amount-and-merchant-bound authorization, not a key. Refusals are specific on purpose: a payment on the wrong rail is a 409 rather than a confusing 400; a stored 402 challenge that advertises no unique matching erc7710 option is also a 409 telling you to re-authorize, because that refusal is deterministic and retrying it can never succeed; and a lost settlement context is a 502 telling you to re-authorize rather than a silent failure. Agent-authenticated and rate-limited on the money-path limiter.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Payment intent id from authorize."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$","description":"The delegate EIP-712 signature over the settlement child."}}}}}},"responses":{"200":{"description":"Settled; retry the merchant with payment_header.","content":{"application/json":{"schema":{"type":"object","required":["payment_id","status","payment_header"],"properties":{"payment_id":{"type":"string"},"status":{"type":"string","enum":["submitted"]},"payment_header":{"type":"string","description":"The merchant X-PAYMENT header. Single-use and bound to this amount and merchant."},"resource_url":{"type":["string","null"]},"passport":{"description":"Optional passport reference for the paying agent."}}}}}},"400":{"description":"A delegate signature is required.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Payment not found.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Not a delegation-rail settlement, not awaiting a signature, or the stored 402 challenge advertises no unique erc7710 option matching this authorization — re-authorize. Exception (#3423): an erc7710 payment that already settled answers code \"payment_already_settled\" with payment_id and tx_hash; nothing is signed, written or charged again.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Settlement state was lost — re-authorize.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/pause":{"post":{"tags":["Agents"],"operationId":"pauseAgent","summary":"Pause an agent — reversible, unlike revoke.","description":"Pausing stops Haven serving the agent while leaving its on-chain authority intact; revoking is the terminal action. A paused agent's standing reports as suspended rather than revoked.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Paused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No such agent for this caller, or it cannot be paused from its current state.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/resume":{"post":{"tags":["Agents"],"operationId":"resumeAgent","summary":"Resume a paused agent.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"Resumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No such agent for this caller, or it cannot be resumed from its current state.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/rotate-key":{"post":{"tags":["Agents"],"operationId":"rotateAgentKey","summary":"Issue a new API key for an active agent.","description":"**The plaintext key is returned ONCE, here, and is never retrievable again** — Haven stores only its hash. Rotation invalidates the previous key immediately, so a running agent must be given the new one before its next call. Only an ACTIVE agent can rotate: a revoked agent stays revoked (409), because handing out a fresh credential for it would undo the revocation.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"responses":{"200":{"description":"The new key. Store it now.","content":{"application/json":{"schema":{"type":"object","required":["api_key","api_key_prefix"],"properties":{"api_key":{"type":"string","description":"Plaintext, shown once."},"api_key_prefix":{"type":"string","description":"The prefix Haven keeps for display."}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"The agent is not active.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/allowances":{"post":{"tags":["Agents"],"operationId":"setAgentAllowance","summary":"RETIRED (410): per-token allowances died with the Safe rail.","description":"Retired with the Safe rail (#1440/#2020). Always answers 410 and writes nothing — spend authority on the delegation rail is a signed budget delegation (`POST /agents/{id}/delegations/prepare` → activate), never a per-token allowance row. The typed operation stays as a tombstone so older clients get a stable, explicit answer rather than a 404.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"Ignored — the endpoint refuses before reading the body."}}}},"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always. The Safe rail is retired; grant a budget delegation instead.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agents/{id}/allowances/{tokenAddress}":{"delete":{"tags":["Agents"],"operationId":"deleteAgentAllowance","summary":"RETIRED (410): per-token allowances died with the Safe rail.","description":"Retired with the Safe rail (#1440/#2020). Always answers 410 and deletes nothing (a malformed token address still gets its 400). Revoke or change the agent’s budget delegation instead.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"name":"tokenAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}}],"responses":{"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Always, for a well-formed token address.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/transactions/payment-intents/{paymentId}/evidence":{"get":{"tags":["Transactions"],"operationId":"getPaymentEvidence","summary":"Read the stored evidence for one payment or approval.","description":"The audit view behind a transaction row. `payment_id` is resolved for the caller — it carries whichever of the two source ids exists, so a client does not branch on payment-versus-approval to find its own identifier.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The evidence record.","content":{"application/json":{"schema":{"type":"object","required":["evidence"],"properties":{"evidence":{"type":"object","additionalProperties":true,"required":["payment_id"],"properties":{"payment_id":{"type":["string","null"],"description":"Resolved from the payment intent id, else the approval request id."}}}}}}}},"400":{"description":"Malformed paymentId.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"No evidence for this payment.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups":{"post":{"tags":["Connect Agent 2"],"operationId":"createAgentConnectionSetup","summary":"Create a pending Connect Agent 2 setup.","description":"Creates setup metadata and a short-lived setup token before any agent signing address exists. Haven stores only a setup-token hash and never receives an agent private key.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentConnectionSetupRequest"}}}},"responses":{"201":{"description":"Pending setup created. The setup_token is returned once and should be passed to the local connector.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentConnectionSetupResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/resolve":{"post":{"tags":["Connect Agent 2"],"operationId":"resolveAgentConnectionSetup","summary":"Resolve setup details for the local connector.","description":"Uses the setup token from the request body to return public setup context and an exact challenge message. The response contains no API key or private key material.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveAgentConnectionSetupRequest"}}}},"responses":{"200":{"description":"Public setup details and proof-of-possession challenge.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveAgentConnectionSetupResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/register":{"post":{"tags":["Connect Agent 2"],"operationId":"registerAgentConnectionSetup","summary":"Register a locally generated public signing address.","description":"The local connector signs the Haven challenge with its locally generated key and sends only the public signing address, proof, and locally generated API-key hash. Haven creates a non-active pending agent and never receives the private key or plaintext API key.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentConnectionSetupRequest"}}}},"responses":{"201":{"description":"Public signing address registered. Payment tools remain unavailable until wallet approval activates the agent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAgentConnectionSetupResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/{setupId}":{"get":{"tags":["Connect Agent 2"],"operationId":"getAgentConnectionSetup","summary":"Read pending setup status for the signed-in user.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/SetupId"}],"responses":{"200":{"description":"Recoverable setup status for the Haven UI.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentConnectionSetupStatus"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/{setupId}/install-status":{"post":{"tags":["Connect Agent 2"],"operationId":"updateAgentConnectionInstallStatus","summary":"Report local connector install readiness.","description":"Updates best-effort local install/probe metadata only. A setup token may be used only before registration and before expiry; after registration the connector uses the pending agent API key. This endpoint cannot change signing address, wallet, allowances, approval status, or payment authority.","security":[{"AgentApiKey":[]},{"SetupToken":[]}],"parameters":[{"$ref":"#/components/parameters/SetupId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConnectorInstallStatusRequest"}}}},"responses":{"200":{"description":"Install status updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConnectorInstallStatusResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/{setupId}/retired-agents":{"post":{"tags":["Connect Agent 2"],"operationId":"recordRetiredAgents","summary":"Report agents this connector retired on replacement (#3772).","description":"Called by `@haven_ai/connect --replace` after it retires old agent directories on a machine: the NEW agent, authenticated with its own API key, names the retired agent ids. Each id that belongs to the same user is stamped superseded-by-this-agent, and that agent's later identity and allowances reads carry `superseded_by`. WARNING ONLY: nothing is granted, revoked or paused, and no payment path consults it. Agent API key only — a setup token is not accepted — and accepted only within 30 minutes of the setup's registration (409 after). Ids owned by another user, the caller itself, or unknown are silently omitted from `recorded_agent_ids`; a later replace overwrites an earlier stamp.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/SetupId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordRetiredAgentsRequest"}}}},"responses":{"200":{"description":"Retired agents recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordRetiredAgentsResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/{setupId}/connector-status":{"get":{"tags":["Connect Agent 2"],"operationId":"getAgentConnectionConnectorStatus","summary":"Narrow post-register status read for the local connector.","description":"Lets the connector wait for the user's budget approval after registering (#1377). Authenticated with the pending agent API key — deliberately usable while the key is still setup_pending-scoped. Read-only and narrow by design: it reveals only the setup status plus, once active, the approved budget summary; it grants no payment authority and returns 404 for setups not owned by the presented key.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/SetupId"}],"responses":{"200":{"description":"Current setup status for the polling connector.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentConnectionConnectorStatus"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/{setupId}/budget-approval":{"post":{"tags":["Connect Agent 2"],"operationId":"recordAgentConnectionBudgetApproval","summary":"Complete a delegation-rail Connect Agent 2 setup.","description":"Activates the pending agent only after Haven confirms that every budget this setup promised exists as an active, owner-signed budget on the agent — the caller asserts nothing, so the request body is empty and the call is safe to retry. Rejected with 409 on a retired Safe / AllowanceModule account, which has no approval path in Haven since #2259 deleted the wallet-approval route.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/SetupId"}],"responses":{"200":{"description":"The budget was confirmed and the setup status was returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentConnectionSetupStatus"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/agent-connection-setups/{setupId}/cancel":{"post":{"tags":["Connect Agent 2"],"operationId":"cancelAgentConnectionSetup","summary":"Cancel a pending Connect Agent 2 setup.","description":"Cancels setup state and revokes the pending agent API key when no on-chain authority has been activated. An approved agent is stopped by ending its budgets instead (POST /agents/{id}/delegations/revoke-all, owner-signed; in the dashboard, Stop budget or Remove agent…); pausing only blocks payments through Haven.","security":[{"DashboardJwt":[]}],"parameters":[{"$ref":"#/components/parameters/SetupId"}],"responses":{"200":{"description":"Setup cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/task-budgets":{"post":{"tags":["TaskBudgets"],"operationId":"openTaskBudget","summary":"Open a task budget, step 1: build the budget to sign (nothing signed yet).","description":"Carves an unsigned, self-delegated child from the agent's active budget delegation for (token, recipient|open) — chain [taskChild, budget], delegate = the agent's own delegate account, never ANY_BENEFICIARY. Stored pending; the agent signs sign_data.typed_data and POSTs it to /task-budgets/{id}/submit to open it. Pre-sign refusal (409 task_budget_exceeds_remaining) when max_amount_atomic plus this agent's other OPEN task budgets under the same parent would exceed the parent's on-chain remaining budget — a convenience, never the real control: the enforcers still rule at redemption.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["max_amount_atomic","ttl_seconds"],"properties":{"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Defaults to the chain's USDC."},"max_amount_atomic":{"type":"string","pattern":"^[0-9]+$","description":"Positive atomic amount; must fit uint96."},"ttl_seconds":{"type":"integer","minimum":60,"maximum":86400,"description":"This task budget's lifetime, 60 s to 24 h."},"recipient_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Optional recipient pin."},"label":{"type":"string","maxLength":120}}}}}},"responses":{"201":{"description":"Pending task budget stored; the agent signs sign_data.typed_data next.","content":{"application/json":{"schema":{"type":"object","required":["task_budget","sign_data","next_action","instructions"],"properties":{"task_budget":{"$ref":"#/components/schemas/TaskBudget"},"sign_data":{"type":"object","required":["signature_scheme","typed_data"],"properties":{"signature_scheme":{"type":"string","enum":["eip712_delegation"]},"typed_data":{"type":"object","additionalProperties":true,"description":"EIP-712 typed data (primaryType 'Delegation') the agent signs verbatim."}}},"next_action":{"type":"string","enum":["sign_then_submit"]},"instructions":{"type":"string"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"no_delegation_for_target — no active budget delegation authorizes this token/recipient.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"not_delegation_rail, or task_budget_exceeds_remaining (carries remaining_atomic, reserved_atomic, requested_atomic).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"get":{"tags":["TaskBudgets"],"operationId":"listTaskBudgets","summary":"List task budgets for the authenticated agent.","description":"Default status=open: OPEN and not expired. status=live (#3518): every row the agent can still act on — closing rows always, pending and open rows while not expired; closed and expired rows are omitted, so the read (and its per-row chain reads) is bounded by live work rather than history. status=all: every row regardless of status or expiry. #3501: open rows carry spent_atomic and remaining_atomic, read live from the enforcer's spentMap for the child's delegation hash (the same authority the chain applies at redemption), so the agent can tell BEFORE paying whether its next payment will be refused. A failed on-chain read reports spent/remaining as null with remaining_is_from_chain: false — never the full cap as remaining.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["open","live","all"]}}],"responses":{"200":{"description":"Task budgets.","content":{"application/json":{"schema":{"type":"object","required":["task_budgets"],"properties":{"task_budgets":{"type":"array","items":{"$ref":"#/components/schemas/TaskBudget"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/task-budgets/{id}":{"get":{"tags":["TaskBudgets"],"operationId":"getTaskBudget","summary":"Fetch one task budget.","description":"#3501: an OPEN task budget carries spent_atomic and remaining_atomic, read live from the enforcer's spentMap for the child's delegation hash (the same authority the chain applies at redemption), so the agent can tell BEFORE paying whether its next payment will be refused. A failed on-chain read reports spent/remaining as null with remaining_is_from_chain: false — never the full cap as remaining.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The task budget.","content":{"application/json":{"schema":{"type":"object","required":["task_budget"],"properties":{"task_budget":{"$ref":"#/components/schemas/TaskBudget"}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/task-budgets/{id}/sign-context":{"get":{"tags":["TaskBudgets"],"operationId":"getTaskBudgetSignContext","summary":"Re-servable, byte-free signing handoff for a pending or closing task budget.","description":"purpose='open' (status pending): typed_data is the EIP-712 Delegation payload for the task child. purpose='close' (status closing): typed_data is the userOp typed data for the disableDelegation call, plus user_operation and user_op_hash. Any other status answers 409 sign_context_unavailable.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The sign context for whichever signature is currently pending.","content":{"application/json":{"schema":{"type":"object","required":["task_budget_id","purpose","task_sign_context_version","typed_data","expected"],"properties":{"task_budget_id":{"type":"string","format":"uuid"},"purpose":{"type":"string","enum":["open","close"]},"task_sign_context_version":{"type":"integer","enum":[1]},"typed_data":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true,"description":"purpose=close only."},"user_op_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]+$","description":"purpose=close only."},"expected":{"type":"object","additionalProperties":true}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"sign_context_unavailable — the task budget is open, closed, or has no signature currently pending.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/task-budgets/{id}/submit":{"post":{"tags":["TaskBudgets"],"operationId":"submitTaskBudget","summary":"Submit the agent signature — opens a pending child, or relays the signed close operation.","description":"status=pending: verifies signature recovers the agent's delegate key over the stored child typed data, then flips to open. status=closing: relays the stored close operation with the signature and flips to closed; if the operation could not be sent (the account moved on since it was prepared) the answer is 409 close_needs_reprepare — call close again and re-sign; if it was sent but its outcome is unconfirmed, the answer is 200 status='closed' when the chain shows the child disabled, else 502 close_outcome_unconfirmed — call close again later: it reports closed once the disable has finalised, or returns new sign_data if the earlier operation did not land (sign and submit that one). Any other status is 409.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"}}}}}},"responses":{"200":{"description":"Opened (from pending) or closed (from closing).","content":{"application/json":{"schema":{"type":"object","required":["task_budget","status"],"properties":{"task_budget":{"$ref":"#/components/schemas/TaskBudget"},"status":{"type":"string","enum":["open","closed"]},"close_tx_hash":{"type":"string","description":"Present when status=closed."}}}}}},"400":{"description":"signature_mismatch, or a malformed signature.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"The close was sent but its outcome is unconfirmed and the chain does not yet show the child disabled (error_code close_outcome_unconfirmed): call close again later — it reports closed once the disable has finalised, or returns new sign_data if the earlier operation did not land.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/task-budgets/{id}/close":{"post":{"tags":["TaskBudgets"],"operationId":"closeTaskBudget","summary":"Close a task budget — trivially if never signed or already expired, otherwise prepares the revocation.","description":"status=pending, or status=open/closing past its expiry: closes immediately, nothing signed, nothing on-chain (200, status='closed'). status=open and live: prepares disableDelegation(child) from the agent's own delegate account and returns sign_data for the agent to sign, then submit via POST /task-budgets/{id}/submit. status=closing: first checks the chain; if the child is already disabled, answers 200 status='closed' with nothing to sign; otherwise re-prepares a fresh close operation and replaces the stored one, so calling close again is always safe. status=closed: 409.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Closed trivially, or a close signature is now pending.","content":{"application/json":{"schema":{"type":"object","required":["task_budget"],"properties":{"task_budget":{"$ref":"#/components/schemas/TaskBudget"},"status":{"type":"string","enum":["closed"],"description":"Present on a trivial close."},"sign_data":{"type":"object","properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"typed_data":{"type":"object","additionalProperties":true},"user_op_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"}},"description":"Present when a live child needs a revocation signature."},"next_action":{"type":"string","enum":["sign_then_submit"]}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Already closed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/sub-budgets":{"get":{"tags":["SubBudgets"],"operationId":"listSubBudgets","summary":"List sub-budgets this agent holds (it is the sub-agent), or owes a signature on (it is the delegating agent).","description":"Default status=open: OPEN, not expired grants where this agent is the HOLDER (sub-agent B). status=all: every row regardless of status or expiry. status=awaiting_signature (#3506): the DELEGATING side — the rows this agent must still sign, i.e. its own parent-child row and every grant it issued, while pending (an open signature is owed; a pending row past its expiry is omitted) or closing (a close signature is owed), parent-child row first. Each row's id is the sub_budget_id to pass to haven_sign, then haven_submit. The agent's own parent-child narrowings are otherwise read through the delegating side (sign-context/close below) — the default list is what a sub-agent spends through.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["open","all","awaiting_signature"]}}],"responses":{"200":{"description":"Sub-budgets held by this agent, or owed a signature by it.","content":{"application/json":{"schema":{"type":"object","required":["sub_budgets"],"properties":{"sub_budgets":{"type":"array","items":{"$ref":"#/components/schemas/SubBudget"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/sub-budgets/{id}":{"get":{"tags":["SubBudgets"],"operationId":"getSubBudget","summary":"Fetch one sub-budget this agent holds.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The sub-budget.","content":{"application/json":{"schema":{"type":"object","required":["sub_budget"],"properties":{"sub_budget":{"$ref":"#/components/schemas/SubBudget"}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/sub-budgets/{id}/sign-context":{"get":{"tags":["SubBudgets"],"operationId":"getSubBudgetSignContext","summary":"Re-servable, byte-free signing handoff for a pending or closing sub-budget row.","description":"DELEGATOR-scoped: both children of a tree (the parent-child narrowing AND the grant to the sub-agent) are signed by the DELEGATING agent's delegate key, so only the delegating agent authenticates here. purpose='open' (status pending): typed_data is the EIP-712 Delegation payload for that row's child. purpose='close' (status closing): typed_data is the userOp typed data for the disableDelegation call, plus user_operation and user_op_hash. Any other status answers 409 sign_context_unavailable.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The sign context for whichever signature is currently pending.","content":{"application/json":{"schema":{"type":"object","required":["sub_budget_id","purpose","sub_budget_sign_context_version","typed_data","expected"],"properties":{"sub_budget_id":{"type":"string","format":"uuid"},"purpose":{"type":"string","enum":["open","close"]},"sub_budget_sign_context_version":{"type":"integer","enum":[1]},"typed_data":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true,"description":"purpose=close only."},"user_op_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]+$","description":"purpose=close only."},"expected":{"type":"object","additionalProperties":true}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"sign_context_unavailable — the sub-budget has no signature currently pending.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/sub-budgets/{id}/submit":{"post":{"tags":["SubBudgets"],"operationId":"submitSubBudget","summary":"Submit the delegating agent signature — opens a pending child, or relays the signed close operation.","description":"status=pending: verifies the signature recovers the DELEGATING agent's delegate key over the stored child typed data, then flips to open. status=closing: relays the stored close operation with the signature and flips to closed. Any other status is 409. A pending grant whose receiving agent was revoked, archived or is pending_approval (retired after issuance, #3553) is refused 409 sub_agent_retired and stays pending; close submits are never gated.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"}}}}}},"responses":{"200":{"description":"Opened (from pending) or closed (from closing).","content":{"application/json":{"schema":{"type":"object","required":["sub_budget","status"],"properties":{"sub_budget":{"$ref":"#/components/schemas/SubBudget"},"status":{"type":"string","enum":["open","closed"]},"close_tx_hash":{"type":"string","description":"Present when status=closed."}}}}}},"400":{"description":"signature_mismatch, or a malformed signature.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/sub-budgets/{id}/close":{"post":{"tags":["SubBudgets"],"operationId":"closeSubBudget","summary":"Close a sub-budget row — trivially if never signed or already expired, otherwise prepares the revocation.","description":"DELEGATOR-scoped (the delegating agent closes either row of its tree). status=pending, or status=open/closing past its expiry: closes immediately, nothing signed, nothing on-chain (200, status='closed'). status=open and live: prepares disableDelegation(child) from the delegating agent's own delegate account and returns sign_data to sign, then submit via POST /sub-budgets/{id}/submit. status=closing: first checks the chain; if the child is already disabled, answers 200 status='closed'; otherwise re-prepares a fresh close operation (idempotent in EFFECT, never in bytes). Closing the parent-child row strands every grant under it; closing a grant row leaves the delegating agent intact. status=closed: 409.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Closed trivially, or a close signature is now pending.","content":{"application/json":{"schema":{"type":"object","required":["sub_budget"],"properties":{"sub_budget":{"$ref":"#/components/schemas/SubBudget"},"status":{"type":"string","enum":["closed"],"description":"Present on a trivial close."},"sign_data":{"type":"object","properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"typed_data":{"type":"object","additionalProperties":true},"user_op_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"}},"description":"Present when a live child needs a revocation signature."},"next_action":{"type":"string","enum":["sign_then_submit"]}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Already closed.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/payments":{"get":{"tags":["Payments"],"operationId":"listAgentPayments","summary":"List recent payment intents for the authenticated agent.","security":[{"AgentApiKey":[]}],"responses":{"200":{"description":"Recent payment intents.","content":{"application/json":{"schema":{"type":"object","required":["payments"],"properties":{"payments":{"type":"array","items":{"$ref":"#/components/schemas/PaymentListItem"}}},"additionalProperties":false}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}}}},"post":{"tags":["Payments"],"operationId":"createPaymentIntent","summary":"Create a direct Haven payment intent.","description":"Creates a signable payment intent on the delegation rail. The agent must sign the returned sign_data with its delegate key before Haven can relay execution; the budget delegation's caveat enforcers authorize it on-chain at redemption. #2105: there is no over-budget approval branch — the approval queue died with the Safe rail (#2055). Both retired rails are refused with 410 before anything is written. #3503: a payment the budget delegation's live remaining PERIOD budget cannot cover is refused with 403 error_code \"delegation_budget_exceeded\" (phase \"insufficient_funds\", next_action \"fund_account_or_raise_allowance\", remaining/remaining_atomic, shortfall/shortfall_atomic, amount/amount_atomic) before any UserOp is built — the same error_code and budget fields the x402 legs refuse with. It reads the delegation the payment redeems (a task budget's parent, else the (token, recipient) grant); for a sub-budget, every link of its three-link chain, the smallest remaining deciding. It is a fail-fast convenience, not the gate: an unreadable read fails OPEN and the ERC20PeriodTransferEnforcer still reverts on-chain; a period revert that a fresh read confirms gets the same 403. #3500: with task_budget_id, a payment the task budget's own cap cannot cover is refused with 403 error_code \"task_budget_exceeded\" (task_budget_id, remaining_atomic, max_atomic, amount_atomic) before anything is built, read from the task child's ERC20TransferAmountEnforcer; the enforcer still reverts on-chain when that read is unavailable.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePaymentRequest"}}}},"parameters":[{"$ref":"#/components/parameters/HavenClient"}],"responses":{"200":{"description":"Idempotent replay of a request whose intent has already progressed to a terminal, reportable state. Body is an `AgentPaymentStatus` plus `idempotent_replay: true`. Deliberately NOT a strict $ref: `AgentPaymentStatus` is `additionalProperties: false`, so the replay marker is not a member of it — the permissive shape is the accurate one here, and narrowing it would be a claim the route does not honour.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"201":{"description":"Payment intent requires the agent signature.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignablePaymentIntent"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"EITHER the agent-auth refusal (`agent_pending_approval` / `agent_paused`, #1130) OR spend authority the agent does not have: no active budget delegation for this token and recipient, or (#3503) error_code \"delegation_budget_exceeded\" — the period budget cannot cover the amount — or (#3500) error_code \"task_budget_exceeded\" — the task budget's cap cannot. Both budget refusals are decided before any UserOp is built.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Idempotency conflict: the key already belongs to a payment with a different token, recipient, amount or task budget (#3392), or it replays an intent that is mid-flight (pending_signature / submitted). Only `payment_intents` carry the key — the approval-queue replay fallback is gone with the table (#2055).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"EITHER a retired rail — the Safe / AllowanceModule rail (#1986) or the session rail (#834), refused before anything is written and before any chain read, with a message naming POST /accounts/hybrid — OR an idempotent replay of a request whose intent has since expired. The two are distinguished by `idempotent_replay` on the body; only the first is a rail refusal.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"426":{"description":"Client outdated (#3303): the `X-Haven-Client` package is below the minimum version this deployment accepts here. Nothing was written or signed. Run `client_update.upgrade_command` (for a connector-installed package it diagnoses the install and prints the exact repair line to run next), then retry the same request.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code","client_update","next_action","next_tool_omitted_reason"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["client_outdated"]},"client_update":{"$ref":"#/components/schemas/ClientUpdate"},"next_action":{"type":"string","enum":["stop_and_tell_user"]},"next_tool_omitted_reason":{"type":"string"}},"additionalProperties":false}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"EITHER #3609: the prepare simulation failed. `error_code: \"prepare_reverted\"` — the redemption REVERTED in execution (a decoded reason, a named caveat-enforcer error, the timestamp caveat's text, or a gas-estimation execution revert): nothing was signed or moved, and the same payment reverts again on every retry; `refusal_reason` and `revert_reason` name it, and the refusal is booked in the ledger. #3731: when the revert was the token's own insufficient-balance error the body also carries `revert_cause: \"insufficient_balance\"` and a funding-first `message` — the account needs funds, not a caveat change. `error_code: \"prepare_failed\"` — anything else: a bundler, RPC or transport failure (nothing booked); an ERC-4337 validation failure the bundler words as a revert (an AA code such as AA25 or AA31), which may clear on its own; or a revert with no nameable execution cause. A classified one is booked as the classifier says (usually onchain_revert), as before #3609. Both carry bounded, redacted `details` (for a viem error, its short message and cause, never the request dump). A task-budget revert a fresh read confirms is answered by the typed 403 instead on both routes (#3500), and on POST /payments a confirmed period-budget revert too (#3503); the funding leg has no period re-read. OR an idempotent replay of a request whose payment has failed (no `error_code`).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":["string","null"],"description":"The underlying bundler/viem failure text, scrubbed of vendor secrets and capped at 300 characters plus an ellipsis — never the full dump."},"error_code":{"type":"string","enum":["prepare_reverted","prepare_failed"]},"refusal_reason":{"type":"string","enum":["delegation_expired","delegation_budget_exceeded","onchain_revert"],"description":"Present on `prepare_reverted` only: the refusal ledger's classification of the revert."},"revert_reason":{"type":["string","null"],"description":"Present on `prepare_reverted` only: the short reason the revert named — a decoded enforcer error (e.g. \"ERC20PeriodTransferEnforcer:transfer-amount-exceeded\") — printable ASCII, at most 120 characters plus an ellipsis; `null` when it named none. Chain text: display it, never act on it."},"revert_cause":{"type":"string","enum":["insufficient_balance"],"description":"#3731: present on `prepare_reverted` only, and only when the revert was the token's own insufficient-balance error (\"ERC20: transfer amount exceeds balance\" — matched on the same value `revert_reason` reports, so the two cannot disagree): the account does not hold enough of the token. The remedy is FUNDING the account, not a caveat change — the wallet owner adds funds in Haven, then the payment can be re-made. Any other revert carries no `revert_cause`. The refusal ledger still books this as `onchain_revert`; nothing about enforcement changes."},"message":{"type":"string","description":"Present on `prepare_reverted` only: the remedy."}},"additionalProperties":true}}}},"503":{"description":"This deployment has no delegation-rail bundler credential for the agent's chain (#3416). The body carries error_code \"rail_unavailable_for_chain\" and chain_id. Not transient: a retry gets the same answer until an operator provisions the chain. Nothing was signed, written or charged.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/payments/{id}":{"get":{"tags":["Payments"],"operationId":"getPaymentIntent","summary":"Fetch direct payment intent status.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/PaymentId"}],"responses":{"200":{"description":"Payment intent status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentIntentStatus"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/payments/{id}/sign-context":{"get":{"tags":["Payments"],"operationId":"getDirectPaymentSignContext","summary":"Fetch the exact signing payload for a pending DIRECT delegation-rail payment.","description":"Read-only byte-free signing handoff (#3271, the direct sibling of GET /x402/{id}/sign-context from #1263): re-serves the stored delegation-rail sign_data.typed_data for a plain POST /payments intent, rebuilt from the stored UserOperation exactly as the idempotent replay of the create rebuilds it, so a LOCAL SIGNER can fetch exact bytes by payment_id instead of an agent re-emitting a multi-KB EIP-712 payload. Constructs and signs nothing new. An x402/MPP intent id is refused here (fetch GET /x402/{id}/sign-context instead), and a direct intent id is refused there — each surface serves only its own rail's shape.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/PaymentId"},{"$ref":"#/components/parameters/HavenClient"}],"responses":{"200":{"description":"The rebuilt direct sign_data — identical to what the idempotent replay serves.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectSignContext"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"The intent is pinned to a retired rail — the AllowanceModule rail (#1986) or the session rail (#834) — or it has expired (with the same lazy-expire GET /x402/{id}/sign-context performs). A retired-rail intent is refused whatever its status.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"426":{"description":"Client outdated (#3303): the `X-Haven-Client` package is below the minimum version this deployment accepts here. Nothing was written or signed. Run `client_update.upgrade_command` (for a connector-installed package it diagnoses the install and prints the exact repair line to run next), then retry the same request.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code","client_update","next_action","next_tool_omitted_reason"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["client_outdated"]},"client_update":{"$ref":"#/components/schemas/ClientUpdate"},"next_action":{"type":"string","enum":["stop_and_tell_user"]},"next_tool_omitted_reason":{"type":"string"}},"additionalProperties":false}}}}}}},"/payments/{id}/sign":{"post":{"tags":["Payments"],"operationId":"submitPaymentSignature","summary":"Submit a delegate signature and relay a payment intent.","description":"The signature must be produced outside Haven by the agent-held delegate key. #2105 (found by review): Haven does NOT verify it against the delegate address or an on-chain allowance, as this said — that was the retired AllowanceModule scheme (`sign_hash` + raw-ECDSA `recoverSigner`), which died with the rail (#1986). Haven now applies a SHAPE check only and relays; the real validator is the account itself (EIP-1271 / the 4337 signature check on the typed data) and the budget delegation's caveat enforcers on redemption. An intent pinned to a retired rail is refused 410 here rather than relayed.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/PaymentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]{100,}$"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Payment execution result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentExecutionResult"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"The intent is pinned to a retired rail — the AllowanceModule rail (#1986) or the session rail (#834) — or it has expired. A retired-rail intent is refused before the expiry flip, so nothing is written.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"On-chain submission failed or its outcome is not yet known. TWO mutually exclusive shapes: `error_code: \"submission_outcome_unknown\"` — the receipt-unconfirmed submit (#3564): the intent is NOT failed, it stays submitted with the user_op_hash recorded and Haven reconciles it from the chain; do not create a new payment — haven_get_payment_status answers the outcome-pending state (next_action check_status_later) and then the real terminal state. Otherwise the intent is already `failed` (known cause): `signature_rejected` (AA24 only — update the signer, then pay again), `account_validation_failed` (a different AA2x code, not a signer cause — pay again), `task_budget_exceeded` / `delegation_budget_exceeded` (the same body shape the create-time 403 answers — `asset` on `delegation_budget_exceeded` only — neither carries a `message` field), or `onchain_execution_failed` (including a submitted UserOp that executed and reverted — a confirmed, no-funds-moved outcome — pay again). The non-budget codes carry bounded, redacted `details` (300 characters plus an ellipsis if longer, or `null`); two known-failure codes and the outcome-pending body carry a fixed remedy `message`, while `onchain_execution_failed`'s `message` carries the bounded text itself (or the literal fallback \"On-chain execution failed\").","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":["string","null"]},"user_op_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"Present only on `error_code: \"submission_outcome_unknown\"`."}},"additionalProperties":true}}}}}}},"/payments/{id}/receipt":{"get":{"tags":["Payments"],"operationId":"getPaymentReceipt","summary":"Fetch a verifiable receipt for a settled payment.","description":"Returns a self-contained proof bundle (payment facts, the delegate authorization signature, and the on-chain tx) plus a self-verification. verification checks the delegate signature over the hash named by its verifiedOver field: on erc7710 payments the EIP-712 delegation digest (verifiedOver delegation_digest), on retired-rail history the raw sign_hash. verification is NOT proof the payment settled and does not bind the payment block: amount and recipient are Haven-asserted, and settlement is proven only by the on-chain transaction (settlementTxHash / onChain.txHash, check on an explorer). Direct-payment and eip3009-funding bundles carry authorization.signatureScheme eip712_userop and their verification returns not_verifiable_offline — the signed user operation is not part of the bundle.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/PaymentId"}],"responses":{"200":{"description":"Receipt bundle and verification result.","content":{"application/json":{"schema":{"type":"object","properties":{"receipt":{"type":"object","additionalProperties":true},"verification":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/payments/{id}/resume_state":{"get":{"tags":["Payments"],"operationId":"getPaymentResumeState","summary":"Rehydrate x402 or MPP resume state for a payment id.","description":"Returns stored protocol context only. This endpoint does not sign, execute, relay, or authorize a payment. The agent still signs locally when it resumes the x402 or MPP flow.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/PaymentId"}],"responses":{"200":{"description":"Serializable x402 or MPP resume state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentResumeState"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/x402/authorize":{"post":{"tags":["x402"],"operationId":"authorizeX402Payment","summary":"Authorize an x402 funding payment.","description":"Creates or executes the Haven side of an x402 merchant request. Haven relays only independently signed payloads; it does not sign on behalf of the agent. The scheme follows the payTo shape: a merchant payTo builds an erc7710 settlement child delegation and settles account→merchant directly with NO funding leg; the EIP-3009 bridge (agent-EOA payTo + merchantPayTo) is the fallback and is the only shape that still funds anything. #2105: there is no approval branch — spend authority is the agent's budget delegation, refused up front with 403 when the amount exceeds the live remaining budget (#2082, extended to this endpoint's EIP-3009 funding leg by #2706) and enforced on-chain by the caveat enforcers at redemption. Preserve the original merchant session and the x402 details. The client performs the merchant retry itself; nothing mid-flow waits for a resume signal. #2145: if the process dies after the funding leg confirms, a later GET /machine-payments/:id/status reports next_action retry_original_x402_request (funding confirmed, no merchant response ever recorded) — resume that payment instead of authorizing a new one.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402AuthorizeRequest"}}}},"parameters":[{"$ref":"#/components/parameters/HavenClient"}],"responses":{"200":{"description":"Existing or resumed x402 state.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/X402SignablePayment"},{"$ref":"#/components/schemas/X402ConfirmedPayment"},{"$ref":"#/components/schemas/AgentPaymentStatus"}]}}}},"201":{"description":"Signable or confirmed x402 funding payment.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/X402SignablePayment"},{"$ref":"#/components/schemas/X402ConfirmedPayment"},{"$ref":"#/components/schemas/AgentPaymentStatus"}]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Spend authority the agent does not have. Either it holds no active budget delegation for this token/merchant, or (#2082, #2706) the amount exceeds that delegation's live remaining period budget — on BOTH settlement schemes now: the erc7710 direct-settlement branch and, since #2706, the EIP-3009 funding leg too. The over-budget refusal is PRE-FUNDING and PRE-PREPARE — no funding redemption is prepared, no settlement child is built, no intent row is written, no delegate account is deployed — and carries error_code \"delegation_budget_exceeded\", phase \"insufficient_funds\", next_action \"fund_account_or_raise_allowance\", plus remaining/remaining_atomic, amount/amount_atomic and shortfall/shortfall_atomic. On the funding leg merchant_address names merchantPayTo (the real merchant), while payTo was the funding target. It is a fail-fast convenience, not the gate: the budget delegation's ERC20PeriodTransferEnforcer still refuses an over-budget redemption on-chain, and a degraded budget read fails OPEN (the payment proceeds to prepare, where the enforcer rules). #3500: with taskBudgetId, a payment the task budget's own cap cannot cover is the same 403 with error_code \"task_budget_exceeded\" (task_budget_id, remaining_atomic, max_atomic, amount_atomic), decided from its ERC20TransferAmountEnforcer before anything is built (or, on the funding leg, after a transfer-cap revert the same read confirms).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"A retired rail: the Safe / AllowanceModule rail (#1986) or the session rail (#834). Fail-closed — nothing is written and no chain read is made. The message names POST /accounts/hybrid.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"426":{"description":"Client outdated (#3303): the `X-Haven-Client` package is below the minimum version this deployment accepts here. Nothing was written or signed. Run `client_update.upgrade_command` (for a connector-installed package it diagnoses the install and prints the exact repair line to run next), then retry the same request.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code","client_update","next_action","next_tool_omitted_reason"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["client_outdated"]},"client_update":{"$ref":"#/components/schemas/ClientUpdate"},"next_action":{"type":"string","enum":["stop_and_tell_user"]},"next_tool_omitted_reason":{"type":"string"}},"additionalProperties":false}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"On the EIP-3009 funding leg: #3609: the prepare simulation failed. `error_code: \"prepare_reverted\"` — the redemption REVERTED in execution (a decoded reason, a named caveat-enforcer error, the timestamp caveat's text, or a gas-estimation execution revert): nothing was signed or moved, and the same payment reverts again on every retry; `refusal_reason` and `revert_reason` name it, and the refusal is booked in the ledger. #3731: when the revert was the token's own insufficient-balance error the body also carries `revert_cause: \"insufficient_balance\"` and a funding-first `message` — the account needs funds, not a caveat change. `error_code: \"prepare_failed\"` — anything else: a bundler, RPC or transport failure (nothing booked); an ERC-4337 validation failure the bundler words as a revert (an AA code such as AA25 or AA31), which may clear on its own; or a revert with no nameable execution cause. A classified one is booked as the classifier says (usually onchain_revert), as before #3609. Both carry bounded, redacted `details` (for a viem error, its short message and cause, never the request dump). A task-budget revert a fresh read confirms is answered by the typed 403 instead on both routes (#3500), and on POST /payments a confirmed period-budget revert too (#3503); the funding leg has no period re-read. On erc7710: the settlement delegation could not be built, or the delegate account could not be deployed — infrastructure, no `error_code`, bounded `details`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":["string","null"],"description":"The underlying bundler/viem failure text, scrubbed of vendor secrets and capped at 300 characters plus an ellipsis — never the full dump."},"error_code":{"type":"string","enum":["prepare_reverted","prepare_failed"]},"refusal_reason":{"type":"string","enum":["delegation_expired","delegation_budget_exceeded","onchain_revert"],"description":"Present on `prepare_reverted` only: the refusal ledger's classification of the revert."},"revert_reason":{"type":["string","null"],"description":"Present on `prepare_reverted` only: the short reason the revert named — a decoded enforcer error (e.g. \"ERC20PeriodTransferEnforcer:transfer-amount-exceeded\") — printable ASCII, at most 120 characters plus an ellipsis; `null` when it named none. Chain text: display it, never act on it."},"revert_cause":{"type":"string","enum":["insufficient_balance"],"description":"#3731: present on `prepare_reverted` only, and only when the revert was the token's own insufficient-balance error (\"ERC20: transfer amount exceeds balance\" — matched on the same value `revert_reason` reports, so the two cannot disagree): the account does not hold enough of the token. The remedy is FUNDING the account, not a caveat change — the wallet owner adds funds in Haven, then the payment can be re-made. Any other revert carries no `revert_cause`. The refusal ledger still books this as `onchain_revert`; nothing about enforcement changes."},"message":{"type":"string","description":"Present on `prepare_reverted` only: the remedy."}},"additionalProperties":true}}}},"503":{"description":"This deployment has no delegation-rail bundler credential for the agent's chain (#3416). The body carries error_code \"rail_unavailable_for_chain\" and chain_id. Not transient: a retry gets the same answer until an operator provisions the chain. Nothing was signed, written or charged.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/x402/by-idempotency-key/{key}":{"get":{"tags":["x402"],"operationId":"getX402IntentByIdempotencyKey","summary":"Look up the caller's x402 payment intent by idempotency key.","description":"Read-only, agent-scoped lookup (#3739). The hosted pay tool's request mode checks this BEFORE re-probing a merchant, so a replayed call returns the existing intent's state without depending on the merchant still answering. Writes nothing: a stale pending_signature row is reported with window_open false, never lazily expired here. Failed intents are not returned, and an expired one ranks below a live one with the same key. Not-found and not-yours are the same 404. The key is URL-encoded by the client.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":128},"description":"The idempotency key the intent was created with (URL-encoded)."}],"responses":{"200":{"description":"The intent's current state.","content":{"application/json":{"schema":{"type":"object","required":["payment_id","status","settlement_scheme","resource_url","expires_at","window_open","task_budget_id","amount_atomic","asset","network"],"properties":{"payment_id":{"type":"string"},"status":{"type":"string"},"settlement_scheme":{"type":["string","null"],"enum":["erc7710","eip3009",null]},"resource_url":{"type":["string","null"]},"expires_at":{"type":["string","null"],"format":"date-time"},"window_open":{"type":"boolean","description":"True only when status is pending_signature and expires_at is in the future."},"task_budget_id":{"type":["string","null"]},"amount_atomic":{"type":"string","pattern":"^[0-9]+$"},"asset":{"type":["string","null"],"description":"Token contract of the stored amount, so a caller can convert a whole-token cap."},"network":{"type":"string"}},"additionalProperties":false}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/x402/{id}/sign-context":{"get":{"tags":["x402"],"operationId":"getX402SignContext","summary":"Fetch the exact signing payload for a pending delegation-rail x402 intent.","description":"Read-only byte-free signing handoff (#1263): re-serves the stored delegation-rail signing payload (sign_data.typed_data) plus a freshly Haven-signed expected context committing to its digest, so a LOCAL SIGNER can fetch exact bytes by payment_id instead of an agent re-emitting them. Constructs and signs nothing new; the signer re-derives the digest and verifies the binding exactly as against an authorize response.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Payment intent id from the quote/authorize response."},{"$ref":"#/components/parameters/HavenClient"}],"responses":{"200":{"description":"The rebuilt sign_data + complete snake_case x402_expected context (field-for-field as signed).","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/X402SignablePayment"},{"$ref":"#/components/schemas/X402ConfirmedPayment"},{"$ref":"#/components/schemas/AgentPaymentStatus"}]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"426":{"description":"Client outdated (#3303): the `X-Haven-Client` package is below the minimum version this deployment accepts here. Nothing was written or signed. Run `client_update.upgrade_command` (for a connector-installed package it diagnoses the install and prints the exact repair line to run next), then retry the same request.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code","client_update","next_action","next_tool_omitted_reason"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["client_outdated"]},"client_update":{"$ref":"#/components/schemas/ClientUpdate"},"next_action":{"type":"string","enum":["stop_and_tell_user"]},"next_tool_omitted_reason":{"type":"string"}},"additionalProperties":false}}}}}}},"/x402/{id}/merchant-call-context":{"get":{"tags":["x402"],"operationId":"getX402MerchantCallContext","summary":"Fetch the stored MCP merchant-call context for an x402 intent.","description":"Read-only settle-leg handoff (#1307): re-serves the merchant_url, tool_name, arguments, and mcp_transport recorded on the intent at quote time (haven_pay_mcp_tool), so haven_settle_mcp_tool / haven_complete_mcp_tool can omit them and let Haven rehydrate by payment_id instead of the caller re-threading them. Convenience metadata for retrying the MERCHANT's own call — never payment authority.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Payment intent id from the quote/authorize response."}],"responses":{"200":{"description":"The stored merchant call context.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402MerchantCallContext"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/x402":{"post":{"tags":["x402"],"operationId":"authorizeX402PaymentLegacy","summary":"Legacy alias for POST /x402/authorize.","deprecated":true,"security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402AuthorizeRequest"}}}},"parameters":[{"$ref":"#/components/parameters/HavenClient"}],"responses":{"200":{"description":"Same response as POST /x402/authorize.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/X402SignablePayment"},{"$ref":"#/components/schemas/X402ConfirmedPayment"},{"$ref":"#/components/schemas/AgentPaymentStatus"}]}}}},"201":{"description":"Same response as POST /x402/authorize.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/X402SignablePayment"},{"$ref":"#/components/schemas/X402ConfirmedPayment"},{"$ref":"#/components/schemas/AgentPaymentStatus"}]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"Same as POST /x402/authorize: a retired rail — Safe / AllowanceModule (#1986) or session (#834). Fail-closed, nothing written.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"426":{"description":"Client outdated (#3303): the `X-Haven-Client` package is below the minimum version this deployment accepts here. Nothing was written or signed. Run `client_update.upgrade_command` (for a connector-installed package it diagnoses the install and prints the exact repair line to run next), then retry the same request.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code","client_update","next_action","next_tool_omitted_reason"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["client_outdated"]},"client_update":{"$ref":"#/components/schemas/ClientUpdate"},"next_action":{"type":"string","enum":["stop_and_tell_user"]},"next_tool_omitted_reason":{"type":"string"}},"additionalProperties":false}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"This deployment has no delegation-rail bundler credential for the agent's chain (#3416). The body carries error_code \"rail_unavailable_for_chain\" and chain_id. Not transient: a retry gets the same answer until an operator provisions the chain. Nothing was signed, written or charged.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/agent":{"get":{"tags":["Machine payments"],"operationId":"getMachinePaymentAgent","summary":"Fetch the authenticated agent identity.","security":[{"AgentApiKey":[]}],"responses":{"200":{"description":"Agent identity for machine-payment tools.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MachinePaymentAgent"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}}}}},"/machine-payments/allowances":{"get":{"tags":["Machine payments"],"operationId":"getMachinePaymentAllowances","summary":"Fetch live spend-authority state for the authenticated agent.","description":"Rail-aware (#1135): on the delegation rail the response carries the agent's active, unexpired budget delegations (remaining = the period budget; AllowanceModule-only fields are zeroed placeholders). BOTH retired rails answer 410 — the session rail (#993) and, since #2020 reversed #1986’s left-readable decision, the Safe/AllowanceModule rail too. Reporting only — enforcement stays on-chain.","security":[{"AgentApiKey":[]}],"responses":{"200":{"description":"Configured and remaining spend authority for the account's rail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllowanceSummary"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"410":{"description":"The account is on a RETIRED rail — session (#993) or Safe/AllowanceModule (#2020, reversing #1986’s left-readable decision on the recorded owner call: the accounts are emptied and unsupported, so no state is read). Fail-closed; nothing is read or written.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/budget-precheck":{"post":{"tags":["Machine payments"],"operationId":"precheckMachinePaymentBudget","summary":"Decide server-side whether a quote amount fits the agent’s remaining budget.","description":"#3054: the guided prepare's budget compare moved server-side so an over-budget refusal is DECIDED by Haven — and reaches the payment_refusals ledger with source \"hosted_prepare\" through the refuse() choke point — instead of being computed in the agent's runtime where the ledger never saw it. The body carries the merchant quote facts (chainId/token/amountAtomic plus advisory merchantTo and the bought resourceUrl — the refusal dedupe window's discriminating column, never this endpoint's own URL); nothing about the caller's claim is trusted beyond which quote it asks about. Sufficiency answers { sufficient: true, remaining_atomic }. Insufficiency refuses 403 delegation_budget_exceeded with the same taxonomy body the x402 legs refuse with (phase, next_action, remaining/shortfall atomic+human). BOTH retired rails answer 410 like every rail-aware surface. Reporting-and-refusal only — enforcement stays on-chain: the budget delegation's ERC20PeriodTransferEnforcer still refuses an over-budget redemption. #3492/#3527: when the body also carries idempotencyKey and it resolves to an already-SETTLED erc7710 OR eip3009 payment for this exact quote, the answer is { sufficient: true, remaining_atomic, replay: true } instead — no refusal, no ledger write, because the money already moved. See BudgetPrecheckRequest.idempotencyKey for the exact match rule and scope.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetPrecheckRequest"}}}},"responses":{"200":{"description":"The amount fits the remaining budget.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetPrecheckResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"The amount exceeds the agent's remaining delegation budget — decided here and recorded in the payment_refusals ledger (source \"hosted_prepare\"). Carries error_code \"delegation_budget_exceeded\", phase \"insufficient_funds\", next_action \"fund_account_or_raise_allowance\", plus remaining/remaining_atomic, amount/amount_atomic and shortfall/shortfall_atomic, and resource_url / merchant_address when the request carried them.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"#3518: the request named no merchantTo, the agent has no open budget for the token, and it holds live merchant-locked budgets for it. Not a refusal — nothing is recorded in payment_refusals. Carries error_code \"budget_requires_recipient\", next_action \"retry_with_explicit_context\" and budget_recipient_addresses (the pins, lowercase); repeat the check with merchantTo set to the payee.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"410":{"description":"The account is on a RETIRED rail — session (#993) or Safe/AllowanceModule (#2020). Fail-closed; there is no budget concept left to pre-check.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/balance-coverage":{"get":{"tags":["Machine payments"],"operationId":"getMachinePaymentBalanceCoverage","summary":"Report whether the account actually holds the amount behind the agent's budget (#3126).","description":"Answers the question no spend-authority read answers: is `amount_atomic` of `token` actually HELD on the authenticated agent's own account right now. The response is a sufficiency signal, never a balance: `covered` is true/false/null and the account's balance itself is deliberately not returned — a constrained agent has no business reading the treasury total, and the boolean answers the only decision an agent has (attempt the payment, or tell the user funds are missing). `covered: null` means the chain read FAILED — treat it as unverifiable, not as absence. Keep the two concepts apart: `budget_remaining_atomic` is spend AUTHORITY (the same figure GET /machine-payments/allowances reports as onchain.remaining); `covered` is about HELD funds. Rail-aware like every read: both retired rails answer 410. Reporting only — grants no authority, moves nothing; enforcement stays on-chain.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"The ERC-20 contract address to check holdings of."},{"name":"amount_atomic","in":"query","required":true,"schema":{"type":"string","pattern":"^[0-9]+$"},"description":"The amount the coverage question is asked about, in ATOMIC units, as a decimal string. Zero passes the schema and is refused by the handler (a sufficiency question about nothing has no honest answer)."}],"responses":{"200":{"description":"The coverage answer for the requested token and amount.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalanceCoverageResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"410":{"description":"The account is on a RETIRED rail — session (#993) or Safe/AllowanceModule (#2020). Fail-closed; there is no account this surface can describe.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/authorize":{"post":{"tags":["Machine payments"],"operationId":"authorizeMachinePayment","summary":"Retired: the legacy MPP demo machine-payment authorize flow (#1328).","description":"The internal mpp_demo flow is retired outright — this endpoint now refuses unconditionally with HTTP 410, fail-closed, before the body is inspected (mirrors the #834 session-rail retirement pattern). DELIBERATE EXCEPTION (review decision on #1339): the route is retained as a compatibility tombstone rather than removed — a 410 tells an old client the flow is permanently gone, where a 404 reads as a transient routing error and invites retries. No new mpp_demo challenge can be authorized. Use the x402 merchant flow instead (POST /x402/authorize). Existing mpp_demo payment/receipt/evidence/status records remain readable through the other /machine-payments/* endpoints.","security":[{"AgentApiKey":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MachinePaymentAuthorizeRequest"}}}},"responses":{"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"410":{"description":"The mpp_demo flow is retired (#1328) — no new legacy MPP demo challenge can be authorized.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/{id}/status":{"get":{"tags":["Machine payments"],"operationId":"getMachinePaymentStatus","summary":"Fetch x402 or MPP payment/approval state.","description":"Branch on next_action, never on message prose. #2145: a confirmed x402 EIP-3009 payment whose merchant leg was never reported (the agent died between the funding confirmation and the merchant retry) answers phase funded_but_unsettled with next_action retry_original_x402_request — resume that payment rather than starting a new one. A client-reported merchant rejection answers sweep_stranded_funds instead.","security":[{"AgentApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/PaymentId"}],"responses":{"200":{"description":"Agent payment status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentPaymentStatus"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/send":{"post":{"tags":["Machine payments"],"operationId":"sendTransfer","summary":"RETIRED: plain transfer. Validates the body, then always refuses.","description":"RETIRED (#1987, epic #1440). This route belonged to the Safe / AllowanceModule rail and no longer has a success path: `modules/mpp/send.ts` is three refusals and nothing else. After body validation (400) the account's rail decides which refusal you get — **410** on either retired rail (Safe / AllowanceModule, #1986; session, #834) and **422** `rail_not_supported` on the delegation rail, which MPP never supported (#1251). Those three cases exhaust the HANDLER, so 2xx is unreachable here; the route in front of it can still answer 400, 401, 403 or 429. Fail-closed: no intent row, no approval row, no sign_data, no chain read. To send from a delegation-rail account use POST /payments, which redeems the agent's budget delegation directly. The operation stays documented rather than deleted because it is still registered and still answers — an integrator needs the refusal contract, not a 404.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["asset","recipient","amount"],"properties":{"asset":{"type":"string","enum":["ETH","USDC"],"description":"Asset to send."},"recipient":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","description":"Recipient address (checksummed or lowercase)."},"amount":{"type":"string","description":"Human-readable amount, e.g. \"1.5\"."},"idempotency_key":{"type":"string","minLength":1,"maxLength":128,"description":"Validated (1–128 characters) and then IGNORED — nothing is deduplicated, because every call refuses. Accepted only so an existing client is refused by the rail rather than by a body error (#2105)."}},"additionalProperties":false}}}},"parameters":[{"$ref":"#/components/parameters/HavenClient"}],"responses":{"400":{"description":"Body validation, in `routes/machine-payments.ts` and therefore BEFORE the rail refusal: unsupported asset, malformed recipient, non-positive amount, or an idempotency_key outside 1–128 characters.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"410":{"description":"The account is on a retired rail — Safe / AllowanceModule (#1986) or session (#834). Fail-closed: nothing is written and no chain read is made. The message names POST /accounts/hybrid.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"422":{"description":"The account is on the delegation rail, which this MPP route never supported (#1251). `error_code` is `rail_not_supported` and the message names POST /payments and the x402 purchase flow. With both retired rails answering 410 above, this is the response every remaining account gets.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"426":{"description":"Client outdated (#3303): the `X-Haven-Client` package is below the minimum version this deployment accepts here. Nothing was written or signed. Run `client_update.upgrade_command` (for a connector-installed package it diagnoses the install and prints the exact repair line to run next), then retry the same request.","content":{"application/json":{"schema":{"type":"object","required":["error","error_code","client_update","next_action","next_tool_omitted_reason"],"properties":{"error":{"type":"string"},"error_code":{"type":"string","enum":["client_outdated"]},"client_update":{"$ref":"#/components/schemas/ClientUpdate"},"next_action":{"type":"string","enum":["stop_and_tell_user"]},"next_tool_omitted_reason":{"type":"string"}},"additionalProperties":false}}}},"429":{"description":"Money-path rate limit.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/receipts":{"get":{"tags":["Machine payments"],"operationId":"listMachinePaymentReceipts","summary":"List stored machine-payment receipts for the authenticated agent.","description":"#3128: a page, newest first. `total` is how many receipts Haven holds for the agent, so an empty `receipts` with `total: 0` means none exist — there is no indexing delay behind this list. `has_more` says the page was cut at `limit`; pass `next_cursor` back as `cursor` for the next page.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` of the previous page (a receipt id). Omit for the first page. A value that is not a uuid, or that names no receipt of this agent, is refused with 400.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"One page of machine-payment receipts.","content":{"application/json":{"schema":{"type":"object","required":["receipts","total","has_more","next_cursor"],"properties":{"receipts":{"type":"array","items":{"$ref":"#/components/schemas/MachinePaymentReceipt"}},"total":{"type":"integer","minimum":0,"description":"Receipts Haven holds for this agent, across all pages."},"has_more":{"type":"boolean","description":"True when receipts beyond this page exist."},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last page."}},"additionalProperties":false}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}}}}},"/machine-payments/evidence":{"post":{"tags":["Machine payments"],"operationId":"attachMachinePaymentEvidence","summary":"Attach merchant proof evidence for a settled machine payment.","description":"Records proof-loop evidence for a settled x402 or MPP payment owned by the authenticated agent. This does not authorize or execute payment. On most schemes the payment is already confirmed and this only attaches merchant/protocol evidence. On erc7710 direct settlement the merchant redeems the delegation chain and Haven submits nothing, so this call is also what COMPLETES the payment: it verifies the reported txHash on-chain against the intent (token, payer, merchant, exact amount, and the settlement window) and confirms the intent before recording evidence. On eip3009, a funded payment reported with a txHash other than its own funding transaction is the merchant's settlement: it is verified on-chain as a delegate → merchant transfer of the exact amount, mined after funding confirmed, and recorded on the payment (receipts then show it as settlement_tx_hash); the funding hash and the proof status do not change. It fails closed — 409 when the transaction does not settle this payment, cannot be attributed to it unambiguously, or is already recorded (on eip3009 the body carries a reason), 503 when the chain could not be read or the transaction is not mined yet; neither confirms or records anything.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MachinePaymentEvidenceRequest"}}}},"responses":{"202":{"description":"Evidence accepted.","content":{"application/json":{"schema":{"type":"object","required":["evidence"],"properties":{"evidence":{"$ref":"#/components/schemas/MachinePaymentReceipt"}},"additionalProperties":false}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"503":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/{id}/delivery-quality":{"post":{"tags":["Machine payments"],"operationId":"reportDeliveryQuality","summary":"Record the agent’s evidence-only delivery verdict for a settled payment.","description":"#3770 — lets the agent that made a payment say what the merchant DELIVERED: \"ok\", \"unusable\" or \"partial\", with an optional bounded note (max 2000 characters). Evidence only: it moves no money, never alters any money field, and never changes the payment — `settled` stays the on-chain fact it already was. Only the paying agent can report (another agent’s payment is 404), and only once the payment is settled (409 otherwise). A re-report replaces the same agent’s earlier verdict; it never touches another agent’s report. The receipt reads (`GET /receipts` rows and the signed receipt bundle) carry the verdict beside the payment once recorded.","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The payment id (intent) the verdict is about."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MachinePaymentDeliveryQualityRequest"}}}},"responses":{"200":{"description":"Verdict recorded.","content":{"application/json":{"schema":{"type":"object","required":["payment_id","quality","note","updated_at"],"properties":{"payment_id":{"type":"string","format":"uuid"},"quality":{"type":"string","enum":["ok","unusable","partial"]},"note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}},"additionalProperties":false}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/{id}/merchant-receipt":{"post":{"tags":["Machine payments"],"operationId":"reportMerchantReceipt","summary":"Report the merchant's own receipt for a settled payment.","description":"Captures the receipt document the merchant handed back in the paid response (invoice number, VAT breakdown — facts Haven's own payment evidence cannot assert). The accounting feed attaches it verbatim next to the Haven-generated evidence document. Best-effort and idempotent: absence is the normal case, the first report wins, and nothing here affects the payment itself. Provide either `url` (https, fetched at feed time under strict guards) or `json` (the inline receipt document, max 64KB).","security":[{"AgentApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The payment id (intent or approval) the receipt belongs to."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"https URL to the merchant receipt document (pdf/png/jpg)."},"json":{"type":"object","description":"The inline receipt document as provided by the merchant.","additionalProperties":true}}}}}},"responses":{"200":{"description":"A merchant receipt was already recorded for this payment (first write wins).","content":{"application/json":{"schema":{"type":"object","properties":{"stored":{"type":"boolean"},"message":{"type":"string"}},"required":["stored"]}}}},"201":{"description":"Merchant receipt stored.","content":{"application/json":{"schema":{"type":"object","properties":{"stored":{"type":"boolean"}},"required":["stored"]}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/reconciliation-events":{"post":{"tags":["Machine payments"],"operationId":"recordMachinePaymentReconciliationEvent","summary":"Record a merchant retry reconciliation event.","description":"Records a post-payment reconciliation marker when the merchant/protocol retry rejects or needs follow-up after a confirmed payment. The event is audit context only; it does not move funds. The payment is resolved scoped to the calling agent, so another agent's payment answers 404 with nothing written. #2292: an acceptance is terminal — a merchant_retry_rejected_after_payment on a payment that already carries a client-reported merchant response (machine_payment_evidence proof_status merchant_response_observed or protocol_receipt_attached) answers 409 rather than re-opening a stranded-funds flag on a delivered payment.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MachinePaymentReconciliationEventRequest"}}}},"responses":{"202":{"description":"Reconciliation event recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MachinePaymentReconciliationEventResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/sweep/prepare":{"post":{"tags":["Machine payments"],"operationId":"prepareDelegateSweep","summary":"Prepare a gasless USDC sweep from the delegate wallet to the agent's account (Haven wallet).","description":"Reads the delegate EOA's stranded USDC and returns an EIP-3009 TransferWithAuthorization (delegate → the agent's account (Haven wallet)) plus Haven's authorization binding. The edge signer signs the authorization with haven_sign_sweep_delegate; POST /machine-payments/sweep/submit relays it. The delegate never needs ETH and Haven never holds the key. Returns { nothing_stranded: true } when the delegate is empty.","security":[{"AgentApiKey":[]}],"responses":{"200":{"description":"Nothing stranded — no authorization to sign.","content":{"application/json":{"schema":{"type":"object","properties":{"nothing_stranded":{"type":"boolean","enum":[true]},"asset":{"type":"string","examples":["USDC"]},"chain_id":{"type":"integer","examples":[8453]},"message":{"type":"string"}},"required":["nothing_stranded","chain_id"]}}}},"201":{"description":"Sweep authorization prepared; sign and submit it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SweepPrepareResponse"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"422":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/machine-payments/sweep/submit":{"post":{"tags":["Machine payments"],"operationId":"submitDelegateSweep","summary":"Relay a delegate-signed gasless USDC sweep.","description":"Submits the delegate-signed EIP-3009 authorization from /machine-payments/sweep/prepare. Haven's relayer pays gas; the relayer is never a spender. The authorization is re-derived from server state, the delegate signature is verified, and the balance is re-read before relaying.","security":[{"AgentApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SweepSubmitRequest"}}}},"responses":{"200":{"description":"Sweep relayed (or idempotent replay of a prior relay).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SweepSubmitResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"502":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/transactions":{"get":{"tags":["Transactions"],"operationId":"listTransactions","summary":"List wallet transactions for the signed-in user.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter to one linked account. The retired `safeId` spelling is REFUSED with a 400 naming this parameter (#2914) rather than ignored — an ignored filter would return every row instead of none."},{"name":"agentId","in":"query","schema":{"type":"string","pattern":"^(user|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"description":"An agent id, or the literal `user` for payments the account holder made directly (#3030: the handler always refused anything else; the spec now says so)."},{"name":"tokenKey","in":"query","schema":{"type":"string","pattern":"^[1-9][0-9]*:(native|0x[0-9a-fA-F]{40})$","examples":["8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"]},"description":"`<chainId>:<token address>`, or `<chainId>:native`. Whether Haven serves that chain is checked by the handler."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":9007199254740991,"default":0},"description":"Bounded to a safe integer (#3030: the handler used to cap it; ajv reads `1e400` as an integer)."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"fresh","in":"query","schema":{"type":"string","enum":["1","true"]}}],"responses":{"200":{"description":"Paginated transactions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionsResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/transactions/export.csv":{"get":{"tags":["Transactions"],"operationId":"exportTransactionsCsv","summary":"Download the filtered transaction list as a CSV file.","description":"Applies the same filters as `GET /transactions` over the whole result set rather than one page, and adds `direction` and `chainId`. UTF-8 with a byte-order mark and RFC 4180 quoting. Bounded at 10 000 rows; above that the request is refused with 413 rather than truncated.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter to one linked account. The retired `safeId` spelling is REFUSED with a 400 naming this parameter (#2914) rather than ignored — an ignored filter would return every row instead of none."},{"name":"agentId","in":"query","schema":{"type":"string","pattern":"^(user|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"description":"An agent id, or the literal `user` for payments the account holder made directly (#3030: the handler always refused anything else; the spec now says so)."},{"name":"tokenKey","in":"query","schema":{"type":"string","pattern":"^[1-9][0-9]*:(native|0x[0-9a-fA-F]{40})$","examples":["8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"]},"description":"`<chainId>:<token address>`, or `<chainId>:native`. Whether Haven serves that chain is checked by the handler."},{"name":"direction","in":"query","schema":{"type":"string","enum":["in","out"]}},{"name":"chainId","in":"query","schema":{"type":"integer","minimum":1,"examples":[8453]}},{"name":"fresh","in":"query","schema":{"type":"string","enum":["1","true"]}}],"responses":{"200":{"description":"The CSV file.","headers":{"Content-Disposition":{"schema":{"type":"string"},"description":"attachment; filename=\"haven-transactions-YYYYMMDD.csv\""},"X-Export-Row-Count":{"schema":{"type":"string"},"description":"Rows written, excluding the header."}},"content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"413":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/transactions/filters":{"get":{"tags":["Dashboard"],"operationId":"getTransactionFilterOptions","summary":"Filter metadata (accounts, agents, tokens) for the transactions view.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"fresh","in":"query","schema":{"type":"string","enum":["1","true"]}}],"responses":{"200":{"description":"Available filter options.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionFilterOptionsResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/transactions/{accountAddress}":{"get":{"tags":["Dashboard"],"operationId":"listAccountTransactions","summary":"Page-based transaction list for one account.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","schema":{"type":"integer","minimum":1}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"maximum":9007199254740991,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"fresh","in":"query","schema":{"type":"string","enum":["1","true"]}}],"responses":{"200":{"description":"Paginated per-account transactions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionsPageResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/receive/{accountAddress}":{"get":{"tags":["Transactions"],"operationId":"getReceiveLedger","summary":"The receive ledger for one account: address, matched USDC balance, inbound rows.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"The account receive ledger. `earned: false` rows are unmatched = unearned; nothing is delivered on them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiveLedgerResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/receive/{accountAddress}/off-ramp-destination":{"put":{"tags":["Transactions"],"operationId":"setOffRampDestination","summary":"Save (or replace) the owner off-ramp destination for one account+chain.","description":"Owner-only by topology: the route sits behind the dashboard JWT and no agent route wraps it. One destination per account+chain; a PUT replaces it. This address is the ONLY recipient the off-ramp hand-off can prepare a transfer to.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","required":true,"schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OffRampDestinationInput"}}}},"responses":{"200":{"description":"The saved destination.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OffRampDestination"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/receive/{accountAddress}/off-ramp/prepare":{"post":{"tags":["Transactions"],"operationId":"prepareOffRampHandoff","summary":"Prepare the owner-signed USDC transfer to the saved off-ramp destination.","description":"Builds the UserOperation through the same prepareTransfer the owner send uses (#1083): Haven prepares, the OWNER signs with the account own signer, Haven relays. The token is the chain registry USDC and the recipient is the SAVED destination — neither comes from the request body. Submit goes through `/hybrid/{accountAddress}/transfers/submit`, which re-derives the calldata and refuses a user_operation that does not contain it. Not a sweep: the sweep is the agent-keyed stranded-delegate machinery.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","required":true,"schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OffRampPrepareInput"}}}},"responses":{"200":{"description":"The prepared UserOperation plus the submit instructions the owner signs into.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OffRampPrepareResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/receive/{accountAddress}/receipt-drop":{"post":{"tags":["Transactions"],"operationId":"dropReceiptForTransfer","summary":"A payer-signed receipt document for one inbound transfer (no authentication; signature IS the auth).","description":"A payer has no Haven account, so this route carries no auth middleware. The signer is RECOVERED from the EIP-191 signature over the exact drop payload (`haven:receipt-drop\\ntx:<hash>\\namount_raw:<atomic>`) and the drop is accepted only when the recovered address IS the payer the inbound transfer names, the amount matches the persisted transfer, and the transfer is still unmatched. The stored document is a reference that flips the row earned flag — it grants nothing and moves nothing.","parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","required":true,"schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptDropInput"}}}},"responses":{"200":{"description":"The transfer is matched and now earned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptDropResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/receive/{accountAddress}/ingest":{"post":{"tags":["Transactions"],"operationId":"ingestInboundTransfer","summary":"Record one inbound USDC transfer in the account receive index.","description":"Owner-scoped ingestion hook (tests and the eventual indexer tick; the production indexer feeds the same repository). Idempotent per (chain, tx hash): a re-ingest returns `ingested: false` rather than double-counting.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","required":true,"schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundIngestInput"}}}},"responses":{"200":{"description":"Recorded (`ingested: false` when the hash was already indexed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundIngestResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/dashboard/overview":{"get":{"tags":["Dashboard"],"operationId":"getDashboardOverview","summary":"Aggregated dashboard overview: totals, day change, spend windows, previews.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"tz","in":"query","schema":{"type":"string"},"description":"IANA time-zone name (e.g. `Europe/Stockholm`) the activity groups bucket local days by. Defaults to UTC; an invalid zone is refused with 400."}],"responses":{"200":{"description":"Dashboard overview.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardOverviewResponse"}}}},"400":{"description":"Invalid `tz` — not an IANA zone name.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/dashboard/budget-remaining":{"get":{"tags":["Dashboard"],"operationId":"getDashboardBudgetRemaining","summary":"Cached on-chain budget-remaining for every budget the user can see, with sub-budget spend attribution.","description":"Display-only. The enforcer read is cached for up to 60 s (and never past the period end in force at the read); a failed or timed-out read returns remaining_from_chain false with remaining_atomic/used_atomic null — never \"0\", never the full budget. `read_at` carries the staleness bound. Sub-budget spend counts confirmed payment_intents through the parent budget's sub-budget tree for the parent's current period; it is a floor, not a ledger.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Per-budget remaining amounts with sub-budget spend attribution.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardBudgetRemainingResponse"}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/balances/{accountAddress}":{"get":{"tags":["Dashboard"],"operationId":"getAccountBalances","summary":"Token balances for one account.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","schema":{"type":"integer","minimum":1},"description":"Required when the same address is linked on more than one chain. A chain id is positive (#3030: the handlers always refused 0 and negatives; the spec now says so)."}],"responses":{"200":{"description":"Balances, native token first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalancesResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/portfolio/{accountAddress}":{"get":{"tags":["Dashboard"],"operationId":"getAccountPortfolio","summary":"Fiat-valued portfolio breakdown for one account.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"accountAddress","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},{"name":"chain_id","in":"query","schema":{"type":"integer","minimum":1},"description":"Required when the same address is linked on more than one chain. A chain id is positive (#3030: the handlers always refused 0 and negatives; the spec now says so)."}],"responses":{"200":{"description":"Portfolio totals and per-token breakdown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioResponse"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/contacts":{"get":{"tags":["Contacts"],"operationId":"listContacts","summary":"List the user's saved address-book entries.","description":"Dashboard address book: a name for an address, scoped to the user. Naming an address changes nothing on-chain and grants no authority — it is presentation only, so a transfer to a named contact is exactly as constrained as a transfer to a raw address.","security":[{"DashboardJwt":[]}],"responses":{"200":{"description":"Contacts, alphabetical by name (LIST_CONTACTS_FOR_USER_SQL orders by name ASC).","content":{"application/json":{"schema":{"type":"object","required":["contacts"],"properties":{"contacts":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"post":{"tags":["Contacts"],"operationId":"createContact","summary":"Save a new address-book entry.","description":"The address must be a valid EVM address and unique per user — a second entry for the same address is a 409, not a silent overwrite, so an existing name is never replaced by accident.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","address"],"properties":{"name":{"type":"string","minLength":1,"description":"Trimmed before storage; blank after trimming is a 400."},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}}}}}},"responses":{"201":{"description":"Contact created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"409":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/feedback":{"post":{"tags":["Feedback"],"operationId":"submitFeedback","summary":"Submit feedback or a bug report from the CLI.","description":"`haven feedback submit \"<text>\"` (#3597). User JWT only — no agent API key, no anonymous caller. Retention is 7 days; there is no reader route yet (the founders-only ops console read is #3602). The backend re-runs the CLI's own secret-check layers 1 (labelled secrets), 3 (key-backed-address derivation, against this database) and 4 (recovery phrases) before writing, and refuses with `text_refused` + a `reason` when one matches — the CLI's own check already refuses before sending, so this is the backstop for a caller that bypasses it.","security":[{"DashboardJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","minLength":1,"maxLength":4000}}}}}},"responses":{"201":{"description":"Stored. The response never echoes the text back.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feedback"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/contacts/{id}":{"put":{"tags":["Contacts"],"operationId":"renameContact","summary":"Rename an address-book entry.","description":"Only the name is mutable; an address is never re-pointed under an existing name. To point a name at a different address, delete and re-create.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1}}}}}},"responses":{"200":{"description":"Updated contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}},"delete":{"tags":["Contacts"],"operationId":"deleteContact","summary":"Delete an address-book entry.","description":"Hard delete of a label, not of history: transactions to that address remain, and simply stop rendering a name. A contact belonging to another user is a 404, never a 403 — the route does not confirm that an id exists.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/catalog":{"get":{"tags":["Catalog"],"operationId":"listCatalog","summary":"List curated payable services agents can discover and pay.","description":"Read-only discovery surface. One source of truth consumed by both the dashboard catalog page and the haven_discover_tools MCP tool. Entries are operator-curated and periodically re-verified against the live merchant 402 challenge; category matching is case-insensitive and every whitespace-separated search word must match the product name, description, or category. Blank search is rejected after trimming, non-empty search is capped at 120 characters, and searches with more than 8 words return 400; nothing here creates payments or signatures. **What `active` means, exactly (#1669):** verification exercises the 402 CHALLENGE only, so `active` says the merchant answers — it cannot say the merchant settles. One deliberate consequence is in the catalog on purpose: entries with `category: 'test-fixture'` simulate failure modes (today, a stranded-funds simulator whose funding leg succeeds but which never settles); their name and description say so plainly. Since #3078 every entry carries its `merchant`, and `merchant.is_test_merchant` is the structural signal a pre-filtering client should use (the Haven demo store and the stranded-funds fixture both carry it); the `test-fixture` category remains as data but is no longer the documented signal.","security":[{"AgentApiKey":[]},{"DashboardJwt":[]}],"parameters":[{"name":"category","in":"query","schema":{"type":"string"}},{"name":"search","in":"query","schema":{"type":"string","minLength":1,"maxLength":120},"description":"Whitespace is trimmed/collapsed. Every word must match the product name, description, or category. Blank search after trimming returns 400; searches over 8 words also return 400."},{"name":"rail","in":"query","schema":{"type":"string","enum":["x402","mpp"]}}],"responses":{"200":{"description":"Catalog entries.","content":{"application/json":{"schema":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/CatalogEntry"}}}}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}}}}},"/catalog/submit":{"post":{"tags":["Catalog"],"operationId":"submitCatalogEntry","summary":"Submit a payable (x402/MCP) endpoint for verification and listing.","description":"Public, unauthenticated self-service submission (epic #1717). Writes a queue row only and returns a `verify_token`; the request path makes **no outbound request of any kind** — nothing here probes, signs, or pays. The seller proves domain control (well-known token / DNS TXT) and a leader-locked, SSRF-hardened, read-only probe watches a real 402 challenge before anything is listed; listed means domain-controlled AND verified-payable, and verification exercises the 402 challenge only, never settlement. Submitting a host that already has a pending/active submission is a no-op returning the same id. A flood is bound by a per-IP rate limit and a capped pending queue (429), and the request body is capped at 8 KB (413). `resource_url` must be https and must NOT carry embedded credentials (`https://user:pass@host/`) — those are refused (400) rather than stored, so nothing downstream can replay them. A queued row is inert: it confers no listing and no standing until domain control and a live 402 challenge have both been observed. Money-path: none; no payment, signature, or authority change.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogSubmitRequest"}}}},"responses":{"201":{"description":"Submission accepted (or an existing pending submission for the same host).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogSubmissionAccepted"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"413":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"429":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/catalog/submit/{id}":{"get":{"tags":["Catalog"],"operationId":"getCatalogSubmissionStatus","summary":"Public status of a catalogue submission (#1715).","description":"Coarse current state plus, while the domain-ownership proof is still valid, the exact well-known / DNS-TXT proof instructions. Deliberately minimal: the `verify_token` is never returned (it is a credential minted once at creation), and failures surface only as the coarse `failed` status — the granular SSRF/ownership reasons stay in server logs so this cannot become an internal-DNS oracle. 404 for an unknown id.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Submission status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogSubmissionStatus"}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/catalog/{id}":{"get":{"tags":["Catalog"],"operationId":"getCatalogEntry","summary":"Fetch one catalog entry.","security":[{"AgentApiKey":[]},{"DashboardJwt":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Catalog entry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogEntry"}}}},"400":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/merchants":{"get":{"tags":["Catalog"],"operationId":"listMerchants","summary":"List the marketplace's merchants.","description":"The sell side of the catalog (#3078, epic #3077): every live merchant with at least one non-delisted offer on a chain this deployment lists (HAVEN_MARKETPLACE_CHAIN_IDS, else HAVEN_DEPLOY_CHAIN_IDS, else every chain) or a verified self-submitted offer, ordered real merchants first, then test merchants. Readable without a credential, like `GET /catalog`. `coming_soon` prospects appear only for an authenticated dashboard user when HAVEN_MARKETPLACE_PROSPECTS is on and HAVEN_MARKETPLACE_CHAIN_IDS itself names a testnet chain (epic #3077 decision 14; the HAVEN_DEPLOY_CHAIN_IDS fallback never counts, so prod, whose list is 8453, cannot show them). Read-only; nothing here creates payments or signatures.","security":[{"AgentApiKey":[]},{"DashboardJwt":[]}],"responses":{"200":{"description":"Merchants.","content":{"application/json":{"schema":{"type":"object","required":["merchants"],"properties":{"merchants":{"type":"array","items":{"$ref":"#/components/schemas/Merchant"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}}}}},"/merchants/{slug}":{"get":{"tags":["Catalog"],"operationId":"getMerchant","summary":"One merchant and its offers.","description":"The merchant and its non-delisted offers on the chains this deployment lists (an agent: its own chain), plus its verified self-submitted offers. A credential-less caller gets the offers in the public catalog shape. 404 — never 403 — for an unknown slug, a live merchant with nothing to show on these chains, or a prospect the caller may not see (the URL must not confirm a prospect exists).","security":[{"AgentApiKey":[]},{"DashboardJwt":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"}}],"responses":{"200":{"description":"Merchant and offers.","content":{"application/json":{"schema":{"type":"object","required":["merchant","funding","offers"],"properties":{"merchant":{"$ref":"#/components/schemas/Merchant"},"funding":{"type":"array","items":{"$ref":"#/components/schemas/MerchantFundingTarget"},"description":"Where the merchant is paid on each listed chain that has an active, verified x402 offer (#3331). Empty when none does."},"offers":{"type":"array","items":{"$ref":"#/components/schemas/CatalogEntry"}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Agent authenticated but not authorized to act (#1130): `agent_pending_approval` — the key is valid but the agent awaits its first budget grant in Haven; `agent_paused` — the owner paused API-initiated transactions. `detail` carries the operator action. Contrast 401, which means the key itself is unknown or revoked.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}},"/merchants/{slug}/budgets":{"get":{"tags":["Catalog"],"operationId":"listMerchantBudgets","summary":"The dashboard user's merchant-locked budgets for one merchant.","description":"Every ACTIVE, unexpired budget delegation the user issued for this merchant (#3331) on an agent that is not revoked, with what is left this period — read from the ERC20PeriodTransferEnforcer's storage, the same read GET /allowances makes (remaining_is_from_chain false = that read failed and remaining_atomic is the full budget) — and the pin against the merchant's current verified payTo. Dashboard session only: an agent key gets 403. 404 for an unknown or non-live merchant.","security":[{"DashboardJwt":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$"}}],"responses":{"200":{"description":"Budgets, by agent name.","content":{"application/json":{"schema":{"type":"object","required":["budgets"],"properties":{"budgets":{"type":"array","items":{"type":"object","required":["agent_id","agent_name","chain_id","token_address","recipient_address","delegation_hash","budget_atomic","period_seconds","expires_at","remaining_atomic","remaining_is_from_chain","pin_status"],"properties":{"agent_id":{"type":"string","format":"uuid"},"agent_name":{"type":"string"},"chain_id":{"type":"integer"},"token_address":{"type":"string","pattern":"^0x[0-9a-f]{40}$"},"recipient_address":{"type":"string","pattern":"^0x[0-9a-f]{40}$"},"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"budget_atomic":{"type":"string","pattern":"^[0-9]+$"},"period_seconds":{"type":"integer"},"expires_at":{"type":"string","pattern":"^[0-9]+$","description":"Unix seconds as a string (BIGINT)."},"remaining_atomic":{"type":"string","pattern":"^[0-9]+$"},"remaining_is_from_chain":{"type":"boolean"},"pin_status":{"type":"string","enum":["current","stale","unverified","not_erc7710"],"description":"`stale`: the merchant now names a different payTo (a rotation) — this budget pays only the old address, and payments to the new one use the open budget, if the agent has one. `unverified`: the merchant names no single payTo on that chain now (including a `shared` one). `not_erc7710`: the payTo still matches but not every offer there advertises ERC-7710, and a pinned budget pays only through ERC-7710."}}}}}}}}},"401":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"403":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}},"404":{"description":"Error response","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"statusCode":{"type":"integer"},"details":{"type":"string"}},"additionalProperties":true}}}}}}}},"components":{"securitySchemes":{"AgentApiKey":{"type":"http","scheme":"bearer","bearerFormat":"sk_agent_*","description":"Agent API keys identify the calling Haven agent only. API auth is identity; signature is authority; the on-chain budget delegation — its caveat enforcers, checked by the DelegationManager at redemption — is enforcement. API keys alone cannot move funds or authorize payment execution."},"OpsJwt":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Ops console session token (#3509), issued by the GitHub sign-in to an allowlisted founder. Signed with its own secret and refused by every customer route; it is read authority over the ops console only, never payment authority."},"DashboardJwt":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Dashboard user session token. This authenticates the user for account-management endpoints; it is not agent payment authority."},"SetupToken":{"type":"apiKey","in":"header","name":"X-Haven-Setup-Token","description":"Short-lived setup token used before connector registration. The same token can also be supplied as setup_token in the JSON request body. Setup tokens authenticate setup only and cannot authorize payment."}},"parameters":{"HavenClient":{"name":"X-Haven-Client","in":"header","required":false,"schema":{"type":"string"},"description":"The calling published client and its version, `<package>/<version>` (for example `@haven_ai/mcp/0.4.0-alpha.0`). Every published Haven client sends it (the connector's read-only identity probe excepted). When the client is below the version this deployment recommends, any JSON-object response carries a `client_update` (`ClientUpdate`, `required: false`). Only below a minimum the deployment has explicitly set is it refused — 426 `client_outdated`, nothing written — and only at the payment-initiating routes (the signer: at sign-context). A missing or unparseable value, a package outside the five published ones, or a `0.0.0-dev.*` snapshot is never refused."},"AgentId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"LabelId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"OrganizationId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"PaymentId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"SetupId":{"name":"setupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}},"schemas":{"ClientUpdate":{"type":"object","description":"#3303: the backend's update hint for an outdated published client. `required: true` means the client is below a minimum this deployment set and will be refused at its refusal points; `upgrade_command` is the command that starts the update, on this deployment's channel. For the connector-installed packages (signer, mcp, connect) it is the connector doctor (#3412): it works as pasted on an existing install and prints the exact `--doctor --repair` line to run next — a bare connector re-run is a setup command that stops at \"Missing --setup\" on an already set-up machine. #3799: on those packages the backend may append ` --name <slug>` (normalised from the calling agent's recorded MCP pair — a display aid, never identity), so an agent on a multi-agent machine can run one command. `/discovery`, `/.well-known/haven.json` and `/releases` keep the bare form.","required":["package","current","recommended","min_version","required","upgrade_command","notes_url"],"properties":{"package":{"type":"string","examples":["@haven_ai/mcp"]},"current":{"type":"string","examples":["0.4.0-alpha.0"]},"recommended":{"type":["string","null"]},"min_version":{"type":["string","null"]},"required":{"type":"boolean"},"upgrade_command":{"type":"string","examples":["npx -y @haven_ai/connect@alpha --doctor"]},"notes_url":{"type":["string","null"],"description":"#3304: the public release notes page. Nullable for clients built before it existed."}},"additionalProperties":false},"HybridAccountSigners":{"type":"object","description":"A hybrid account's signer set — the exact configuration the account address was derived from. Public key material plus per-credential enrollment time (#1679); nothing secret.","required":["account_address","chain_id","owner_address","passkeys"],"properties":{"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"chain_id":{"type":"integer"},"owner_address":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{40}$","description":"Null for a pure-passkey account."},"passkeys":{"type":"array","items":{"type":"object","required":["key_id","x","y","created_at"],"properties":{"key_id":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"},"created_at":{"type":["string","null"],"format":"date-time","description":"When this credential was enrolled (#1679) — the UI labels the row \"Passkey · added {date}\". Null only if the stored row is missing; clients fall back to ordinal \"Passkey N\" labels, never a platform name."}}}}}},"Delegation":{"type":"object","description":"One budget-delegation row (#828). start_date and expires_at are unix-second BIGINTs and arrive as digit STRINGS (node-postgres decodes int8 as string). The signed delegation object is never included here — the list is lifecycle metadata only.","required":["id","chain_id","token_address","recipient_address","delegation_hash","version","status","budget_atomic","period_seconds","start_date","expires_at","created_at","merchant_id","merchant_slug","merchant_name"],"properties":{"id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"token_address":{"type":"string","pattern":"^0x[0-9a-f]{40}$","description":"Stored lowercase (table CHECK)."},"recipient_address":{"type":["string","null"],"pattern":"^0x[0-9a-f]{40}$","description":"Lowercase recipient pin, or null for an open budget."},"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"version":{"type":"integer"},"status":{"type":"string","enum":["pending","active","replaced","revoked"]},"budget_atomic":{"type":"string","pattern":"^[0-9]+$"},"period_seconds":{"type":"integer"},"start_date":{"type":"string","pattern":"^[0-9]+$","description":"Unix seconds as a string (BIGINT)."},"expires_at":{"type":"string","pattern":"^[0-9]+$","description":"Unix seconds as a string (BIGINT)."},"created_at":{"type":"string","format":"date-time"},"merchant_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"The merchant a merchant-locked budget was issued for (#3331); null for every other row, and after that merchant is deleted."},"merchant_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"That merchant’s slug, or null."},"merchant_name":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"That merchant’s display name, or null."},"remaining_atomic":{"anyOf":[{"type":"string","pattern":"^[0-9]+$"},{"type":"null"}],"description":"Atomic units the on-chain caveat enforcer will still allow this period, read for ACTIVE rows — present ONLY on GET /agents/{id}/delegations?include=remaining (#3693); the key is absent entirely without the parameter. When remaining_from_chain is false the read failed and this is the full budget. null for non-active rows when included."},"remaining_from_chain":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Present ONLY on GET /agents/{id}/delegations?include=remaining (#3693); the key is absent entirely without the parameter. False means the on-chain read failed and remaining_atomic is the budget, exactly as readRemainingBudget reports it. null for non-active rows when included."},"period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Present ONLY on GET /agents/{id}/delegations?include=remaining (#3693); the key is absent entirely without the parameter. When the current period refills (ISO 8601), computed the same way as the analytics budget views. Still the PERIOD boundary when the delegation expires earlier — the row expires_at then shows the earlier expiry. null for non-active rows when included."}}},"MerchantFundingTarget":{"type":"object","required":["network","chain_id","pay_to","pay_to_status","erc7710"],"properties":{"network":{"type":"string","description":"CAIP-2, e.g. \"eip155:84532\"."},"chain_id":{"type":"integer"},"pay_to":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-f]{40}$"},{"type":"null"}],"description":"The lowercased payTo every active, verified x402 offer of the merchant on this network names — non-null only when pay_to_status is `verified`. A merchant-locked budget pins its recipient here."},"pay_to_status":{"type":"string","enum":["verified","conflicting","unstated","shared"],"description":"`conflicting`: two offers name different addresses. `unstated`: some offer names none (not yet probed, or its challenge carries none). `shared`: another merchant’s non-delisted offer on this network names the same address, so a pin would pay that merchant too. Only `verified` can be pinned to."},"erc7710":{"type":"boolean","description":"Every one of those offers advertises the ERC-7710 transfer method — the rail a pinned budget pays through. False: payments to this merchant use the open budget, so no merchant-locked budget is offered."}}},"TaskBudget":{"type":"object","required":["id","agent_id","chain_id","token_address","recipient_address","parent_delegation_hash","delegation_hash","label","max_atomic","status","expires_at","is_expired","created_at","opened_at","closed_at","close_tx_hash"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"token_address":{"type":"string","pattern":"^0x[0-9a-f]{40}$","description":"Stored lowercase."},"recipient_address":{"type":["string","null"],"pattern":"^0x[0-9a-f]{40}$","description":"Lowercase recipient pin, or null when this task budget carries none."},"parent_delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"This task budget's own child delegation hash."},"label":{"type":["string","null"],"maxLength":120},"max_atomic":{"type":"string","pattern":"^[0-9]+$"},"spent_atomic":{"type":["string","null"],"pattern":"^[0-9]+$","description":"What has been transferred through this child so far, read live from the ERC20TransferAmountEnforcer's spentMap for this delegation hash — the same authority the chain applies at redemption. Present on GET reads of an open task budget only; null when the on-chain read failed (a warning is logged server-side). Absent on responses that did not read the chain."},"remaining_atomic":{"type":["string","null"],"pattern":"^[0-9]+$","description":"max_atomic minus spent_atomic, clamped at zero — what the chain will still allow through this child. Never fabricated: a failed read reports null, never the full cap."},"spent_display":{"type":["string","null"],"description":"Human form of spent_atomic (\"0.001 USDC\"), derived server-side from the chain registry. Null exactly when spent_atomic is null; an unknown token renders the atomic figure explicitly labelled."},"remaining_display":{"type":["string","null"],"description":"Human form of remaining_atomic. Null exactly when remaining_atomic is null."},"remaining_is_from_chain":{"type":"boolean","description":"Provenance of remaining_atomic (#1319-style honesty flag): true when it came from the live enforcer read, false when that read failed (spent_atomic and remaining_atomic are then null). Present only where the read was attempted."},"status":{"type":"string","enum":["pending","open","closing","closed"]},"expires_at":{"type":"integer","description":"Unix seconds."},"is_expired":{"type":"boolean","description":"Derived: expires_at <= now."},"created_at":{"type":"string","format":"date-time"},"opened_at":{"type":["string","null"],"format":"date-time"},"closed_at":{"type":["string","null"],"format":"date-time"},"close_tx_hash":{"type":["string","null"]}}},"SubBudget":{"type":"object","required":["id","agent_id","parent_agent_id","parent_sub_budget_id","chain_id","token_address","recipient_address","parent_delegation_hash","delegation_hash","label","period_amount_atomic","status","expires_at","is_expired","created_at","opened_at","closed_at","close_tx_hash"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"parent_agent_id":{"type":"string","format":"uuid"},"parent_sub_budget_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"chain_id":{"type":"integer"},"token_address":{"type":"string","pattern":"^0x[0-9a-f]{40}$","description":"Stored lowercase."},"recipient_address":{"type":["string","null"],"pattern":"^0x[0-9a-f]{40}$","description":"Lowercase recipient pin, or null when this sub-budget carries none."},"parent_delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"This sub-budget's own child delegation hash."},"label":{"type":["string","null"],"maxLength":120},"period_amount_atomic":{"type":"string","pattern":"^[0-9]+$"},"status":{"type":"string","enum":["pending","open","closing","closed"]},"expires_at":{"type":"integer","description":"Unix seconds."},"is_expired":{"type":"boolean","description":"Derived: expires_at <= now."},"created_at":{"type":"string","format":"date-time"},"opened_at":{"type":["string","null"],"format":"date-time"},"closed_at":{"type":["string","null"],"format":"date-time"},"close_tx_hash":{"type":["string","null"]}}},"SubBudgetTree":{"type":"object","description":"One parent→child tree (#3330): the delegating agent's parent-child narrowing with its grants nested.","required":["parent_child_sub_budget","grants"],"properties":{"parent_child_sub_budget":{"$ref":"#/components/schemas/SubBudget"},"grants":{"type":"array","items":{"$ref":"#/components/schemas/SubBudget"}}},"additionalProperties":false},"Contact":{"type":"object","required":["id","name","address","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"Feedback":{"type":"object","required":["id","created_at","expires_at"],"properties":{"id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time","description":"Retention is 7 days from `created_at` (#3597)."}},"additionalProperties":false},"AttentionDismissal":{"type":"object","description":"One server-saved \"Needs you\" dismissal (#3813). `no-backup` rows carry `account_id` (the backup item is per account); `needs-setup` rows carry `agent_id` (per agent within its account). Permanent — there is no undismiss.","required":["id","item_kind","account_id","agent_id","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"item_kind":{"type":"string","enum":["no-backup","needs-setup"]},"account_id":{"type":"string","format":"uuid","nullable":true,"description":"Set on `no-backup` rows; null otherwise."},"agent_id":{"type":"string","format":"uuid","nullable":true,"description":"Set on `needs-setup` rows; null otherwise."},"created_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"AttentionDismissalsResponse":{"type":"object","required":["dismissals"],"properties":{"dismissals":{"type":"array","items":{"$ref":"#/components/schemas/AttentionDismissal"}}},"additionalProperties":false},"AttentionDismissalInput":{"description":"The kind decides which id the body carries; any other pairing is refused before the handler.","oneOf":[{"type":"object","required":["item_kind","account_id"],"properties":{"item_kind":{"type":"string","enum":["no-backup"]},"account_id":{"type":"string","format":"uuid"}},"additionalProperties":false},{"type":"object","required":["item_kind","agent_id"],"properties":{"item_kind":{"type":"string","enum":["needs-setup"]},"agent_id":{"type":"string","format":"uuid"}},"additionalProperties":false}]},"CatalogEntryMerchant":{"type":"object","required":["id","slug","name","listing_status","is_test_merchant"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"listing_status":{"type":"string","enum":["live","coming_soon"],"description":"Named apart from `CatalogEntry.status` (active|degraded|delisted) on purpose. A `coming_soon` merchant has no offers, so an entry never carries it in practice."},"is_test_merchant":{"type":"boolean","description":"True for Haven-run test content: the Haven demo store (real payments, demo goods) and the stranded-funds fixture. The structural signal for clients that pre-filter test content."}}},"Merchant":{"type":"object","required":["id","slug","name","description","website","logo_url","category","country","listing_status","is_test_merchant","offer_count","networks","verified_payable"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string","description":"URL key: `/merchants/{slug}` and `/marketplace/<slug>`."},"name":{"type":"string"},"description":{"type":"string"},"website":{"anyOf":[{"type":"string"},{"type":"null"}]},"logo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only Haven-run and Ampersend rows carry one; prospects never do (monogram only)."},"category":{"type":"string"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO 3166-1 alpha-2, when known."},"listing_status":{"type":"string","enum":["live","coming_soon"],"description":"`coming_soon` is a prospect Haven is talking to — shown only to an authenticated dashboard user on a deployment whose HAVEN_MARKETPLACE_CHAIN_IDS itself names a testnet chain and has HAVEN_MARKETPLACE_PROSPECTS on (epic #3077 decision 14); never an agreement, never payable, never in an agent read or the credential-less shape."},"is_test_merchant":{"type":"boolean"},"offer_count":{"type":"integer","description":"Non-delisted offers on the chains this deployment lists (an agent: its own chain) plus verified self-submitted offers. Zero for a prospect."},"networks":{"type":"array","items":{"type":"string"},"description":"Distinct CAIP-2 networks of the listed operator offers, e.g. [\"eip155:84532\"]. Ingestion offers carry none."},"verified_payable":{"type":"boolean","description":"Any offer verified payable — the same observation `CatalogEntry.verified_payable` records, at merchant level."}}},"CatalogEntry":{"type":"object","required":["id","name","description","category","resource_url","rail","protocol","status","tool_name","tool_arguments","tool_arguments_schema","http_method","body_type","body_example","price_display","price_atomic","asset","network","asset_transfer_methods","verified_at","source","domain_verified","verified_payable","merchant"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"resource_url":{"type":"string"},"merchant":{"anyOf":[{"$ref":"#/components/schemas/CatalogEntryMerchant"},{"type":"null"}],"description":"The merchant this entry belongs to (#3078, epic #3077): every operator row has one after migration 088; an ingestion row has one once it is verified payable. Null only for a row the merchant join could not resolve."},"rail":{"type":"string","enum":["x402","mpp"]},"protocol":{"type":"string","enum":["http","mcp"]},"tool_name":{"anyOf":[{"type":"string"},{"type":"null"}]},"tool_arguments":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"description":"Suggested MCP tool arguments for this catalog item, when the row represents a specific product variant. Agents should pass this object unchanged to the pay tool arguments field after confirming the live merchant quote."},"tool_arguments_schema":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"description":"#3769: the JSON Schema a per-call MCP tool's caller arguments must satisfy — haven_prepare_catalog_purchase validates a caller `arguments` against it (and refuses arguments on a row that declares none, the fixed-SKU contract). Null on a fixed-SKU row or a row that declares no schema."},"http_method":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"#3769: the HTTP method a plain-HTTP x402 resource needs (e.g. \"POST\"). Null means GET. Only meaningful for `protocol: \"http\"` rows; the catalog verifier probes with the declared method and body."},"body_type":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"#3769: the body encoding of `body_example` — \"json\" today. Null when the row declares no body."},"body_example":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"description":"#3769: an example request body the row's verifier probe and the discovery hint carry (the hint's `body` is its JSON string). Null when the row declares no body."},"price_display":{"anyOf":[{"type":"string"},{"type":"null"}]},"price_atomic":{"anyOf":[{"type":"string"},{"type":"null"}]},"asset":{"anyOf":[{"type":"string"},{"type":"null"}]},"network":{"anyOf":[{"type":"string"},{"type":"null"}]},"asset_transfer_methods":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated set of x402 assetTransferMethods the merchant advertises on the recorded network (e.g. \"eip3009\" or \"eip3009,erc7710\"). Null until the first successful x402 probe; MPP entries stay null."},"status":{"type":"string","enum":["active","degraded","delisted"]},"verified_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"source":{"type":"string","enum":["operator","ingestion"],"description":"Where the entry came from. `operator` = curated in migrations/scripts (the operator vouches for the listing; no domain-ownership proof runs). `ingestion` = self-submitted through the Verified Payable Directory (epic #1717) and passed domain-ownership proof plus the read-only quote probe."},"domain_verified":{"type":"boolean","description":"True only for `ingestion` entries whose seller proved control of the endpoint domain. Always false for operator-curated rows: no ownership proof ever runs for them, and this field must not claim one."},"verified_payable":{"type":"boolean","description":"True when Haven watched this endpoint answer a real x402 (or MPP) quote in its periodic probe: for `ingestion` rows that is the SSRF-hardened directory probe; for `operator` rows it is the catalog refresh probe (`status === active && verified_at` set) — same observation, no domain-ownership claim attached. False for a degraded row, or an operator row with no `verified_at` (migration 058 seeds its demo rows with one, so on a fresh database those carry the badge before the first refresh tick). Never treat this badge as proof of merchant honesty, quality, or settlement reliability."}}},"CatalogSubmitRequest":{"type":"object","required":["resource_url"],"properties":{"merchant_name":{"type":"string","maxLength":120,"description":"Optional (#3078): the seller's display name. Used to name the merchant when the submission is verified payable and no merchant owns the host yet; ignored when one does (the host proves the seller). Distinct from `website`."},"merchant_website":{"type":"string","maxLength":2048,"description":"Optional (#3078): the seller's public site, https. Same rules as `merchant_name`."},"resource_url":{"type":"string","description":"https URL of the payable x402/MCP endpoint the seller wants verified and listed. This endpoint makes no request to it: the submission is queue-only, and ownership proof plus the verification probe run later, asynchronously under the leader-locked catalog monitor."},"website":{"type":"string","description":"Honeypot. Bots that fill this plausible-looking field are dropped with a fake success and nothing is written; human submitters leave it empty."}},"additionalProperties":false},"CatalogSubmissionAccepted":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","format":"uuid"},"verify_token":{"type":"string","description":"Domain-ownership proof token. The seller proves control of the endpoint domain by serving the expected value at https://<host>/.well-known/haven-verify-<token>.txt (DNS TXT supported as fallback). Verification, and any public listing, waits for that proof; `submitted` alone guarantees nothing. PRESENT ONLY on the response to the request that created the submission — it is a credential minted for that one submitter. A request that de-duplicates onto an existing pending submission gets `id` and `status` only, so naming a hostname can never disclose another party's token."},"status":{"type":"string","enum":["submitted","ownership_verified","verified_payable"]}},"additionalProperties":false},"CatalogOwnershipInstructions":{"type":"object","required":["expires_at","well_known","dns_txt"],"properties":{"expires_at":{"type":"string","format":"date-time"},"well_known":{"type":"object","required":["url","content","instruction"],"properties":{"url":{"type":"string"},"content":{"type":"string"},"instruction":{"type":"string"}},"additionalProperties":false},"dns_txt":{"type":"object","required":["name","value","instruction"],"properties":{"name":{"type":"string"},"value":{"type":"string"},"instruction":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false},"CatalogSubmissionStatus":{"type":"object","required":["id","status","created_at","updated_at","last_verified_at","name","description","entrypoint"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["submitted","ownership_verified","verified_payable","failed","delisted"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"last_verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"entrypoint":{"anyOf":[{"type":"string"},{"type":"null"}]},"instructions":{"anyOf":[{"$ref":"#/components/schemas/CatalogOwnershipInstructions"},{"type":"null"}],"description":"Present while the submission can still prove ownership (submitted / ownership_verified) and the deployment has CATALOG_OWNERSHIP_SECRET set. Absent once verified_payable, failed or delisted — the proof is no longer actionable."}},"additionalProperties":false},"AgentPaymentPhase":{"type":"string","enum":["agent_signature_required","payment_submitted","payment_confirmed","user_approval_required","user_execution_required","waiting_for_additional_approvals","funding_sent","rejected","expired","failed","insufficient_funds","funded_but_unsettled","delivered_unverified"],"description":"Stable Haven agent payment state phase.","x-enumDescriptions":{"agent_signature_required":"The agent must sign and submit the prepared payment before Haven can relay it.","payment_submitted":"Haven has received the signed payment and the agent should poll for confirmation.","payment_confirmed":"The direct payment is confirmed; the agent does not need to do more for this payment id.","user_approval_required":"Retired wire value: no live rail produces it. It described the Safe rail's approval queue, which no longer exists — an out-of-policy payment is declined before any money moves. If it is ever seen, stop and tell the user; no approval is pending.","user_execution_required":"Retired wire value: no live rail produces it. Stop and tell the user.","waiting_for_additional_approvals":"Retired wire value: no live rail produces it. Stop and tell the user.","funding_sent":"The Haven funding leg was sent; the agent can continue the merchant/protocol leg.","rejected":"The payment was rejected and cannot proceed; the agent should stop and tell the user.","expired":"The payment expired before completion.","failed":"Haven could not complete the payment; the agent should stop and surface the failure.","insufficient_funds":"Pre-flight check determined the delegate balance plus the remaining on-chain budget cannot cover the requested amount, so no payment was created. The account must be funded or the agent budget raised before retrying.","funded_but_unsettled":"Haven's funding leg confirmed on-chain but the merchant rejected the x402 retry. The delegate wallet may hold stranded funds. The agent should stop and wait for the wallet owner to sweep the stranded funds back to the account.","delivered_unverified":"Terminal: the merchant delivered the goods and the settlement window plus the sweep's last attribution chance have both passed with no verified on-chain evidence. The payment can no longer resolve by polling. Tell the user the goods were delivered but Haven holds no verified settlement evidence for this payment."}},"AgentPaymentNextAction":{"type":"string","enum":["sign_and_submit_payment","check_status_later","none","wait_for_user_approval","wait_for_user_to_complete_payment","retry_original_x402_request","stop_and_tell_user","request_again_if_user_still_wants_it","retry_with_explicit_context","payment_window_expired","fund_account_or_raise_allowance","sweep_stranded_funds","awaiting_settlement_evidence"],"description":"Stable next action an agent should take for a Haven payment state.","x-enumDescriptions":{"sign_and_submit_payment":"Sign with the delegate key and submit the payment to Haven.","check_status_later":"Poll getPaymentStatus later using this payment id.","none":"No further agent action is required for this payment id.","wait_for_user_approval":"Retired wire value: no live rail produces it, and nothing maps to it. It described a per-payment approval queue that no longer exists. If it is ever seen, stop and tell the user rather than polling — no approval will arrive.","wait_for_user_to_complete_payment":"Retired wire value: no live rail produces it. Stop and tell the user rather than polling.","retry_original_x402_request":"Resume this payment id and retry the original x402 request with the merchant payment header.","stop_and_tell_user":"Stop retrying this payment and tell the user what happened.","request_again_if_user_still_wants_it":"Ask again only if the user still wants the payment after expiry.","payment_window_expired":"The x402 funding/quote window expired. Re-quote with the same idempotency key before asking the signer to build a merchant payment header again.","fund_account_or_raise_allowance":"Stop and tell the user that the account needs to be funded or the agent budget raised before the payment can succeed.","retry_with_explicit_context":"Retry the same tool call, this time passing merchant_url, tool_name, arguments, and mcp_transport explicitly — the server had no stored context to rehydrate for this payment id.","sweep_stranded_funds":"Tell the user that funds may be stranded in the delegate wallet and prompt them to initiate a sweep in Haven to return them to the originating account.","awaiting_settlement_evidence":"The settlement window passed with no verified on-chain evidence yet. If you hold the merchant's real settlement transaction hash, report it with haven_report_settlement_evidence. Otherwise, Haven's settlement sweep may still attribute it within about two minutes — poll getPaymentStatus once more, then tell the user the goods were delivered but unverified if it still shows nothing."}},"AgentPaymentRail":{"type":"string","enum":["direct","x402","mpp","mpp_demo","mpp_crypto","stripe_deposit","spt"],"description":"Stable rail identifier for Haven agent payment states."},"ApiRootDocument":{"type":"object","required":["name","openapi","auth","health","manifest"],"properties":{"name":{"type":"string","enum":["haven-api"]},"description":{"type":"string"},"openapi":{"type":"string","format":"uri","description":"Absolute URL of this document, derived from the request — so the dev backend names the dev backend and a request through the frontend proxy names the proxy."},"docs":{"type":"string","format":"uri","description":"Agent-readable product docs."},"manifest":{"type":"string","format":"uri","description":"Capability manifest on the configured dashboard origin."},"auth":{"type":"object","required":["agent","owner"],"properties":{"agent":{"type":"string","description":"How an agent credential is presented."},"owner":{"type":"string","description":"How an owner session is obtained."}},"additionalProperties":false},"health":{"type":"string","format":"uri"}},"additionalProperties":false},"DiscoveryDocument":{"type":"object","required":["hosted_mcp_url","connector_package","cli_package","openapi_url","chains","client_releases"],"properties":{"hosted_mcp_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Null when this deployment has none configured — a discovery document that refuses is less useful than one that says so, so the connect handout's configuration error is reported here rather than propagated as a 500."},"hosted_mcp_note":{"type":"string","description":"Why the URL is null, when it is."},"connector_package":{"type":"string","pattern":"^@haven_ai/connect@[a-z][a-z0-9-]{0,31}$"},"cli_package":{"type":"string","pattern":"^@haven_ai/cli@[a-z][a-z0-9-]{0,31}$"},"openapi_url":{"type":"string","format":"uri"},"chains":{"type":"object","required":["default","deployable","supported"],"properties":{"default":{"type":"integer","description":"This deployment's effective default chain id (#3431): the product default (`DEFAULT_CHAIN_ID`) when this deployment deploys on it, else the first chain it does deploy on, else the product default again if this deployment deploys on nothing (a misconfigured deployable set). Not necessarily the product-wide default — a deployment scoped to one chain (dev: Base Sepolia only) reports that chain here, not Base mainnet. Confirm the funding chain with `haven wallets funding` or the dashboard's Receive-funds screen before sending money."},"deployable":{"type":"array","items":{"type":"integer"}},"supported":{"type":"array","items":{"type":"integer"}}},"additionalProperties":false},"client_releases":{"type":"object","description":"#3304: per published client, the version released in source (not a claim about npm's `latest` dist-tag — npm publishes later, on promotion), the thresholds this deployment enforces (the same table the `client_outdated` refusal reads), the update command on this deployment's channel, and short notes. `release_notes_url` is the human-readable page on the dashboard origin.","required":["release_notes_url","packages"],"properties":{"release_notes_url":{"type":"string","format":"uri"},"packages":{"type":"object","required":["@haven_ai/sdk","@haven_ai/signer","@haven_ai/mcp","@haven_ai/connect","@haven_ai/cli"],"properties":{"@haven_ai/sdk":{"$ref":"#/components/schemas/PackageReleaseCompat"},"@haven_ai/signer":{"$ref":"#/components/schemas/PackageReleaseCompat"},"@haven_ai/mcp":{"$ref":"#/components/schemas/PackageReleaseCompat"},"@haven_ai/connect":{"$ref":"#/components/schemas/PackageReleaseCompat"},"@haven_ai/cli":{"$ref":"#/components/schemas/PackageReleaseCompat"}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"PackageReleaseCompat":{"type":"object","description":"#3304: one published client in `DiscoveryDocument.client_releases`.","required":["released_version","recommended_version","min_version","upgrade_command","notes"],"properties":{"released_version":{"type":"string","description":"The newest version released in source. Not npm's `latest` dist-tag."},"recommended_version":{"type":["string","null"],"description":"Below this, responses carry a non-blocking `client_update` hint. Null = no hint."},"min_version":{"type":["string","null"],"description":"Below this, the package's refusal points answer `client_outdated`. Null = never refused."},"upgrade_command":{"type":["string","null"],"examples":["npx -y @haven_ai/connect@alpha --doctor"]},"notes":{"type":"array","description":"Newest first. What changed, for deciding whether to update — not the full CHANGELOG.","items":{"type":"object","required":["version","date","summary","summary_segments","action_required"],"properties":{"version":{"type":"string"},"date":{"type":"string","format":"date"},"summary":{"type":"string","description":"Plain text: code spans keep their content, without backticks."},"summary_segments":{"type":"array","description":"#3393: `summary` split into parts, so a renderer can show code as code. The texts join to exactly `summary`.","items":{"type":"object","required":["text","code"],"properties":{"text":{"type":"string"},"code":{"type":"boolean","description":"True when the part was a code span in the CHANGELOG."}},"additionalProperties":false}},"action_required":{"type":"boolean","description":"True when a client must update to keep paying. Not the same as a breaking change."}},"additionalProperties":false}}},"additionalProperties":false},"DeviceAuthorizationStart":{"type":"object","required":["device_code","user_code","verification_url","expires_in","interval"],"properties":{"device_code":{"type":"string","description":"The client's bearer credential for polling. Stored hashed."},"user_code":{"type":"string","description":"Typed by the human. 8 characters from an unambiguous alphabet."},"verification_url":{"type":"string","format":"uri"},"expires_in":{"type":"integer","description":"Seconds. 600."},"interval":{"type":"integer","description":"Seconds between polls. 5."}},"additionalProperties":false},"HealthResponse":{"type":"object","required":["status","timestamp","db"],"properties":{"status":{"type":"string","enum":["ok","degraded"]},"timestamp":{"type":"string","format":"date-time"},"db":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ok","error"]},"latencyMs":{"type":"integer"}},"additionalProperties":false}},"additionalProperties":false},"OpsSystemHealth":{"type":"object","required":["sweepable_intents","evidence_orphans","stuck_revocations","stuck_reanchors","stuck_lanes","delegate_balances","ops_diagnostics","generated_at"],"properties":{"sweepable_intents":{"type":"array","description":"ERC-7710 payment intents the settlement sweeper works on, oldest first, at most 50. in_window: still inside the recovery window it retries. past_horizon: past 24 h up to 30 days — the sweeper no longer retries these, the payments actually lost without an operator.","items":{"type":"object","required":["id","agent_id","chain_id","token_symbol","amount_human","status","window","age_seconds"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"token_symbol":{"type":"string"},"amount_human":{"type":"string"},"status":{"type":"string"},"window":{"type":"string","enum":["in_window","past_horizon"]},"age_seconds":{"type":"integer","minimum":0}},"additionalProperties":false}},"evidence_orphans":{"type":"array","description":"Confirmed ERC-7710 payments no machine_payment_evidence row references (#2213): settled on-chain, booked nowhere, outside every automated retry path.","items":{"type":"object","required":["id","agent_id","chain_id","token_symbol","amount_human","status","age_seconds"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"token_symbol":{"type":"string"},"amount_human":{"type":"string"},"status":{"type":"string"},"age_seconds":{"type":"integer","minimum":0}},"additionalProperties":false}},"stuck_revocations":{"type":"array","description":"Revocations unreconciled past 1 h (#973): Haven's DB says the agent is revoked, the live attestation does not. A merchant reading only the chain still sees the agent as valid.","items":{"type":"object","required":["agent_id","revocation_requested_at","revocation_attempts","age_seconds"],"properties":{"agent_id":{"type":"string","format":"uuid"},"revocation_requested_at":{"type":["string","null"],"format":"date-time"},"revocation_attempts":{"type":"integer"},"age_seconds":{"type":"integer","minimum":0}},"additionalProperties":false}},"stuck_reanchors":{"type":"array","description":"Re-anchors unreconciled past 1 h (#1699): the live attestation names the retired key.","items":{"type":"object","required":["agent_id","agent_eoa","delegate_address","revocation_attempts"],"properties":{"agent_id":{"type":"string","format":"uuid"},"agent_eoa":{"type":["string","null"]},"delegate_address":{"type":["string","null"]},"revocation_attempts":{"type":"integer"}},"additionalProperties":false}},"stuck_lanes":{"type":"array","description":"Broadcast outbound transactions unmined past the bump worker's stale threshold, per served chain. No receipt is read on this path: a row listed here MAY ALREADY BE MINED — the worker's chain-first tick closes those. id is the unmasked outbound_txs id the operator pastes into ops:cancel-stuck-lane; it links to no user. capped_needs_operator: a rebroadcast-safe submitter's lane at the bump cap — the worker has stopped for good and the lane is the operator's.","items":{"type":"object","required":["id","chain_id","submitter","nonce","age_seconds","reason"],"properties":{"id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"submitter":{"type":"string"},"nonce":{"type":"string"},"age_seconds":{"type":"integer","minimum":0},"reason":{"type":"string","enum":["stale_unmined","capped_needs_operator"]}},"additionalProperties":false}},"delegate_balances":{"oneOf":[{"type":"object","description":"The delegate balance monitor's last scan — kept in memory, never a scan on request.","required":["available","scanned_at","report"],"properties":{"available":{"type":"boolean","enum":[true]},"scanned_at":{"type":"string","format":"date-time"},"report":{"type":"object","required":["scanned_delegates","unread","lingering","dust_total_atomic","dust_alert","chain_errors"],"properties":{"scanned_delegates":{"type":"integer","minimum":0},"unread":{"type":"integer","minimum":0},"lingering":{"type":"array","description":"Balances at/above the sweep floor with no fresh pending payment — sweepable money on a hot EOA.","items":{"type":"object","required":["agent_id","agent_name","delegate_address","chain_id","balance_atomic"],"properties":{"agent_id":{"type":"string","format":"uuid"},"agent_name":{"type":"string"},"delegate_address":{"type":"string"},"chain_id":{"type":"integer"},"balance_atomic":{"type":"string"}},"additionalProperties":false}},"dust_total_atomic":{"type":"string"},"dust_alert":{"type":"boolean"},"chain_errors":{"type":"object","additionalProperties":{"type":"string"}}},"additionalProperties":false}},"additionalProperties":false},{"type":"object","description":"This replica does not hold the monitor leader lock, so it has no last report. There is never a scan on request.","required":["available","reason"],"properties":{"available":{"type":"boolean","enum":[false]},"reason":{"type":"string","enum":["not_available_on_this_replica"]}},"additionalProperties":false}]},"ops_diagnostics":{"$ref":"#/components/schemas/HealthOpsResponse"},"generated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OpsSession":{"type":"object","required":["github_id","login","expires_at"],"properties":{"github_id":{"type":"string","description":"Numeric GitHub user id (the allowlist key)."},"login":{"type":"string","description":"GitHub login at sign-in time; display only."},"expires_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OpsOverview":{"type":"object","required":["users","smart_accounts","agents_by_status","active_delegations","payment_intents_24h","payment_refusals_24h","generated_at"],"properties":{"users":{"type":"integer"},"smart_accounts":{"type":"array","items":{"type":"object","required":["chain_id","account_type","count"],"properties":{"chain_id":{"type":"integer"},"account_type":{"type":"string"},"count":{"type":"integer"}},"additionalProperties":false}},"agents_by_status":{"type":"array","items":{"type":"object","required":["status","count"],"properties":{"status":{"type":"string"},"count":{"type":"integer"}},"additionalProperties":false}},"active_delegations":{"type":"integer"},"payment_intents_24h":{"type":"array","items":{"type":"object","required":["status","count"],"properties":{"status":{"type":"string"},"count":{"type":"integer"}},"additionalProperties":false}},"payment_refusals_24h":{"type":"array","items":{"type":"object","required":["reason","count"],"properties":{"reason":{"type":"string"},"count":{"type":"integer"}},"additionalProperties":false}},"generated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OpsSearchResponse":{"type":"object","required":["key_type","hits","timed_out"],"properties":{"key_type":{"type":"string","enum":["uuid","address","tx_hash","email"]},"hits":{"type":"array","description":"Typed by kind: user (email masked), agent, payment_intent, smart_account, or system_tx (no user link).","items":{"type":"object","required":["kind","id"],"properties":{"kind":{"type":"string","enum":["user","agent","payment_intent","smart_account","system_tx"]},"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"email":{"type":"string","description":"Masked."},"status":{"type":"string"},"chain_id":{"type":"integer"},"delegate_address":{"type":["string","null"]},"account_address":{"type":"string"},"account_type":{"type":"string"},"submitter":{"type":"string"},"created_at":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false}},"timed_out":{"type":"array","items":{"type":"string","enum":["users","agents","payment_intents","smart_accounts","system_txs"]}}},"additionalProperties":false},"OpsUserDetail":{"type":"object","required":["user","smart_accounts","agents","active_delegations","payment_intents","payment_refusals"],"properties":{"user":{"type":"object","required":["id","email","name","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","description":"Masked; reveal through POST /ops/reveal."},"name":{"type":["string","null"],"description":"Masked; reveal through POST /ops/reveal."},"created_at":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false},"smart_accounts":{"type":"array","items":{"type":"object","required":["id","chain_id","account_address","account_type","execution_rail","name","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"account_address":{"type":"string"},"account_type":{"type":"string"},"execution_rail":{"type":"string"},"name":{"type":"string"},"created_at":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false}},"agents":{"type":"array","items":{"type":"object","required":["id","account_id","name","status","delegate_address","created_at","archived_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":["string","null"],"format":"uuid"},"name":{"type":"string"},"status":{"type":"string"},"delegate_address":{"type":["string","null"]},"created_at":{"type":["string","null"],"format":"date-time"},"archived_at":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false}},"active_delegations":{"type":"array","items":{"type":"object","required":["id","agent_id","chain_id","token_address","recipient_address","merchant_id","budget_atomic","period_seconds","start_date","expires_at"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"token_address":{"type":"string"},"recipient_address":{"type":["string","null"],"description":"The recipient pin; null for an open budget."},"merchant_id":{"type":["string","null"],"format":"uuid"},"budget_atomic":{"type":"string"},"period_seconds":{"type":"integer"},"start_date":{"type":"integer","description":"Unix seconds."},"expires_at":{"type":"integer","description":"Unix seconds."}},"additionalProperties":false}},"payment_intents":{"type":"array","items":{"type":"object","required":["id","agent_id","status","chain_id","token_symbol","amount_human","to_address","error_message","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"chain_id":{"type":"integer"},"token_symbol":{"type":"string"},"amount_human":{"type":"string"},"to_address":{"type":"string"},"error_message":{"type":["string","null"],"description":"Stored redacted of vendor secrets."},"created_at":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false}},"payment_refusals":{"type":"array","items":{"type":"object","required":["id","agent_id","chain_id","token_symbol","amount_atomic","reason","source","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"token_symbol":{"type":"string"},"amount_atomic":{"type":"string"},"reason":{"type":"string"},"source":{"type":"string"},"created_at":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false}}},"additionalProperties":false},"OpsOnchainDelegation":{"type":"object","required":["budget_atomic","onchain","budget_status","budget_remaining_atomic"],"properties":{"budget_atomic":{"type":"string","description":"The ACTIVE stored delegation period budget, atomic units."},"onchain":{"type":"string","enum":["enabled","disabled","unknown","unavailable"],"description":"Whether the DelegationManager reports the delegation disabled. 'unknown': the cached read-set does not cover it yet; 'unavailable': the read failed."},"budget_status":{"type":"string","enum":["from_chain","unknown","unavailable"],"description":"'from_chain': budget_remaining_atomic is the enforcer's answer. A failed read renders 'unavailable' — never the fallback figure."},"budget_remaining_atomic":{"type":["string","null"],"description":"The enforcer's remaining period budget; null unless budget_status is 'from_chain'."}},"additionalProperties":false},"OpsOnchainAccount":{"type":"object","required":["account_id","chain_id","account_address","account_type","execution_rail","name","db","chain","flags"],"properties":{"account_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"account_address":{"type":"string","description":"Masked."},"account_type":{"type":"string","enum":["delegator_hybrid"]},"execution_rail":{"type":"string"},"name":{"type":"string"},"db":{"type":"object","required":["active_delegations"],"properties":{"active_delegations":{"type":"array","items":{"type":"object","required":["budget_atomic"],"properties":{"budget_atomic":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"chain":{"type":"object","required":["deploy_status","delegations"],"properties":{"deploy_status":{"type":"string","enum":["deployed","counterfactual","unknown","unavailable"],"description":"Best-effort: a lagging RPC node can report a deployed account as having no code."},"delegations":{"type":"array","description":"Same order as db.active_delegations — position identifies the delegation.","items":{"$ref":"#/components/schemas/OpsOnchainDelegation"}}},"additionalProperties":false},"flags":{"type":"object","required":["counterfactual_with_active_delegation","delegation_disabled_onchain_active_in_db"],"properties":{"counterfactual_with_active_delegation":{"type":"boolean","description":"No on-chain code while an active delegation is stored. Best-effort; may be a lagging node."},"delegation_disabled_onchain_active_in_db":{"type":"boolean","description":"The DelegationManager reports a delegation disabled that the DB still holds as active."}},"additionalProperties":false}},"additionalProperties":false},"OpsOnchainNotServedAccount":{"type":"object","required":["account_id","chain_id","account_address","account_type","execution_rail","status","reason"],"properties":{"account_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"account_address":{"type":"string","description":"Masked."},"account_type":{"type":"string"},"execution_rail":{"type":"string"},"status":{"type":"string","enum":["not_served"]},"reason":{"type":"string","enum":["legacy_safe","chain_not_served","chain_not_pinned"]}},"additionalProperties":false},"OpsOnchainView":{"type":"object","required":["user_id","accounts","generated_at"],"properties":{"user_id":{"type":"string","format":"uuid"},"accounts":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/OpsOnchainAccount"},{"$ref":"#/components/schemas/OpsOnchainNotServedAccount"}]}},"generated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OpsSponsoredGas":{"type":"object","required":["days","eth_price_usd","eth_priced_at","gas_basis","rows","generated_at"],"properties":{"days":{"type":"integer"},"eth_price_usd":{"type":["number","null"],"description":"The view-time ETH/USD quote; null when the price feed returned nothing usable."},"eth_priced_at":{"type":"string","enum":["view_time"]},"gas_basis":{"type":"string","description":"Which on-chain figure the gas numbers are and how ETH is priced."},"rows":{"type":"array","items":{"type":"object","required":["day","merchant_host","leg","funding_legs","gas_cost_wei","gas_eth","gas_usd","value_moved_usd","gas_value_ratio"],"properties":{"day":{"type":"string","format":"date","description":"UTC day bucket."},"merchant_host":{"type":["string","null"],"description":"The merchant host from the intent resource URL; null for direct payments."},"leg":{"type":"string","enum":["direct","x402_funding"]},"funding_legs":{"type":"integer"},"gas_cost_wei":{"type":["string","null"],"description":"Sum of actualGasCost in wei; null while no leg in the bucket has a known cost."},"gas_eth":{"type":["number","null"]},"gas_usd":{"type":["number","null"],"description":"gas_eth priced at view time; null when no usable ETH quote."},"value_moved_usd":{"type":"number","description":"The confirmed intents' usd_value; reverted legs moved nothing."},"gas_value_ratio":{"type":["number","null"],"description":"gas_usd / value_moved_usd; null when gas was not priced or nothing moved."}},"additionalProperties":false}},"generated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OpsFeedbackList":{"type":"object","required":["feedback","generated_at"],"properties":{"feedback":{"type":"array","items":{"$ref":"#/components/schemas/OpsFeedback"}},"generated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OpsFeedback":{"type":"object","required":["id","email","text","created_at","expires_at"],"properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","description":"The submitter, masked."},"text":{"type":"string","description":"The masked message: a character count only. The unmasked text leaves only through POST /ops/reveal (target_type feedback, field text)."},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OpsRevealRequest":{"type":"object","required":["target_type","target_id","field"],"properties":{"target_type":{"type":"string","enum":["user","feedback"]},"target_id":{"type":"string","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"},"field":{"type":"string","enum":["email","name","text"]}},"additionalProperties":false},"OpsRevealResponse":{"type":"object","required":["target_type","target_id","field","value"],"properties":{"target_type":{"type":"string"},"target_id":{"type":"string"},"field":{"type":"string"},"value":{"type":["string","null"]}},"additionalProperties":false},"HealthOpsResponse":{"type":"object","required":["relayer","passport","trustProxy","accounting","request_validation"],"properties":{"relayer":{"type":"array","description":"Cached per-chain relayer gas balance from the hourly scan. Never a live RPC read.","items":{"type":"object","required":["chainId","address","balanceWei","low","checkedAt"],"properties":{"chainId":{"type":"integer"},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"balanceWei":{"type":"string"},"low":{"type":"boolean"},"checkedAt":{"type":"string","format":"date-time"}},"additionalProperties":false}},"passport":{"type":"object","description":"L0 Agent Passport configuration state (#1151). Booleans plus the published issuer address only — never key material, never the schema UID. A chain in `issuance_only` anchors passports no merchant can verify.","required":["verification","chains","unverifiableChainIds"],"properties":{"verification":{"type":"object","required":["configured","issuer"],"properties":{"configured":{"type":"boolean"},"issuer":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]}},"additionalProperties":false},"chains":{"type":"array","items":{"type":"object","required":["chainId","issuanceConfigured","verificationConfigured","state"],"properties":{"chainId":{"type":"integer"},"issuanceConfigured":{"type":"boolean"},"verificationConfigured":{"type":"boolean"},"state":{"type":"string","enum":["ready","issuance_only","verification_only","unconfigured"]}},"additionalProperties":false}},"unverifiableChainIds":{"type":"array","items":{"type":"integer"}}},"additionalProperties":false},"trustProxy":{"type":"object","description":"Trust-proxy state (#1670): the hop count the process actually read, and whether the per-IP auth rate-limit tier is therefore armed. Exists because the armed/disarmed split is otherwise invisible from outside — the tier deliberately returns NO limit when the proxy is untrusted, which a probe cannot tell apart from a variable the process never saw.","required":["hops","authRateLimitArmed"],"properties":{"hops":{"type":"integer"},"authRateLimitArmed":{"type":"boolean"}},"additionalProperties":false},"accounting":{"type":"object","description":"Accounting-feed on-call counters (#2872, widened by #3019). `exhaustedSyncs`: sync rows the retry sweep has given up on (`failed` at the attempt cap) — fix the cause, then the user presses Sync now. `connectionsNeedingAttention`: connections in `needs_reauthorisation`, `scope_missing`, `revoked_at_provider` or `needs_attention` — only the user's re-consent (or, for `needs_attention`, a reconnect after the deployment fix) resolves them. `webhookCounters`: the Accounted webhook receiver’s nine per-answer-class counters (received / bad_signature / stale / unknown_token / duplicate / processed / feature_off / unknown_type / confirmed) — IN-PROCESS, process-lifetime, reset on restart; the durable facts are the `accounting_webhook_deliveries` rows, not these. Thresholds: docs/operations/accounting-feed.md. The two integer counters are the one database read on this payload: when the queries throw, both are `null` (and `webhookCounters` with them) and `unavailable` is `true` while the in-memory siblings still answer.","required":["exhaustedSyncs","connectionsNeedingAttention","webhookCounters"],"properties":{"exhaustedSyncs":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}]},"connectionsNeedingAttention":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}]},"webhookCounters":{"anyOf":[{"type":"object","required":["received","bad_signature","stale","unknown_token","duplicate","processed","feature_off","unknown_type","confirmed"],"properties":{"received":{"type":"integer","minimum":0},"bad_signature":{"type":"integer","minimum":0},"stale":{"type":"integer","minimum":0},"unknown_token":{"type":"integer","minimum":0},"duplicate":{"type":"integer","minimum":0},"processed":{"type":"integer","minimum":0},"feature_off":{"type":"integer","minimum":0},"unknown_type":{"type":"integer","minimum":0},"confirmed":{"type":"integer","minimum":0}},"additionalProperties":false},{"type":"null"}]},"unavailable":{"type":"boolean","description":"Present and `true` only when the counters could not be read; the two integers and `webhookCounters` are then `null`."}},"additionalProperties":false},"request_validation":{"type":"object","description":"The request-validation plugin's shadow counters (#3029, epic #3028) — served since the plugin shipped, declared here since #3208. IN-PROCESS: they start at `since` (the plugin install, one per process) and dev redeploys on every merge, so a reading is only as wide as that window. `seenByRoute` (#3208) is what makes a zero readable: a shadowed route with `seen: 0` in the window is NOT PROVEN, never clean. Enforced routes are absent from `seenByRoute` — they refuse for real. The same events ride the log stream (`request_validation.would_refuse`, `would_coerce`, `seen`), which survives deploys; `scripts/ci/shadow-reading.mjs` aggregates them.","required":["mode","wouldRefuse","wouldCoerce","byRouteField","coerceByRouteField","since","seenByRoute"],"properties":{"mode":{"type":"string","enum":["off","shadow","enforce"]},"wouldRefuse":{"type":"integer","minimum":0,"description":"Would-be refusals in the window; at most one per request."},"wouldCoerce":{"type":"integer","minimum":0,"description":"Body FIELDS ajv rewrote and #3082 restored; one per field, never summed with `wouldRefuse`."},"byRouteField":{"type":"object","additionalProperties":{"type":"integer","minimum":0},"description":"Keyed `METHOD /path field`."},"coerceByRouteField":{"type":"object","additionalProperties":{"type":"integer","minimum":0},"description":"Keyed `METHOD /path field`."},"since":{"type":"string","format":"date-time","description":"When these counters started — the process's plugin install."},"seenByRoute":{"type":"object","additionalProperties":{"type":"integer","minimum":0},"description":"Requests that reached validation per SHADOWED route (`METHOD /path`), whatever the verdict."}},"additionalProperties":false}},"additionalProperties":false},"SuccessResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}},"additionalProperties":false},"Label":{"type":"object","required":["id","name","color","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":64},"color":{"type":"string","enum":["neutral","brand","success","debit"]},"created_at":{"type":"string","format":"date-time"}}},"LabelListResponse":{"type":"object","required":["labels"],"properties":{"labels":{"type":"array","items":{"$ref":"#/components/schemas/Label"}}},"additionalProperties":false},"CreateLabelRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":64},"color":{"type":"string","enum":["neutral","brand","success","debit"]}},"additionalProperties":false},"UpdateLabelRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"color":{"type":"string","enum":["neutral","brand","success","debit"]}},"additionalProperties":false},"Organization":{"type":"object","required":["id","parent_organization_id","name","created_at","updated_at","agent_count"],"properties":{"id":{"type":"string","format":"uuid"},"parent_organization_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"name":{"type":"string","minLength":1,"maxLength":64},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"agent_count":{"type":"integer","minimum":0}},"additionalProperties":false},"OrganizationListResponse":{"type":"object","required":["organizations"],"properties":{"organizations":{"type":"array","items":{"$ref":"#/components/schemas/Organization"}}},"additionalProperties":false},"CreateOrganizationRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":64},"parent_organization_id":{"type":"string","format":"uuid"}},"additionalProperties":false},"UpdateOrganizationRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"parent_organization_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]}},"additionalProperties":false,"description":"Rename and/or move. An empty object is accepted and changes nothing."},"DeleteOrganizationResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}},"additionalProperties":false},"CompanyDetails":{"type":"object","required":["legal_name","country","org_number","vat_number","vies_status","vies_checked_at","created_at","updated_at"],"properties":{"legal_name":{"type":"string","minLength":1,"maxLength":200},"country":{"type":"string","pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2."},"org_number":{"type":"string","minLength":1,"maxLength":32,"description":"For a sole trader this is the personal identity number — see docs/product/owner-company-details.md."},"vat_number":{"type":["string","null"],"description":"Normalised: uppercase, no spaces."},"vies_status":{"type":["string","null"],"enum":["pending","valid","invalid","not_verifiable",null]},"vies_checked_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"UpsertCompanyDetailsRequest":{"type":"object","required":["legal_name","country","org_number"],"properties":{"legal_name":{"type":"string","minLength":1,"maxLength":200},"country":{"type":"string","minLength":2,"maxLength":2},"org_number":{"type":"string","minLength":1,"maxLength":32},"vat_number":{"type":["string","null"],"maxLength":32}},"additionalProperties":false},"DeleteCompanyDetailsResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}},"additionalProperties":false},"UpsertAgentTaxDeclarationRequest":{"type":"object","required":["tax_declaration_enabled"],"properties":{"tax_declaration_enabled":{"type":"boolean","description":"True opts THIS agent in; false withdraws the opt-in (never refused)."}},"additionalProperties":false},"AgentTaxDeclarationState":{"type":"object","required":["id","tax_declaration_enabled"],"properties":{"id":{"type":"string","format":"uuid"},"tax_declaration_enabled":{"type":"boolean"}},"additionalProperties":false},"AgentTaxDeclarationUnavailableReason":{"type":"string","enum":["feature_disabled","disabled","no_company_details","vies_not_valid"],"description":"Why no declaration content is available: the deployment flag is off, the agent is not opted in, no VAT number is saved, or VIES is anything-but-valid at read time."},"AgentTaxDeclaration":{"type":"object","required":["version","jurisdiction","taxableStatus","taxId","validUntil"],"properties":{"version":{"type":"string","enum":["x402-tax-1"],"description":"The §2.1 version discriminator of the first profile."},"jurisdiction":{"type":"string","pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2 — the owner's saved country. Never derived from the VAT number's prefix."},"taxableStatus":{"type":"string","enum":["TAXABLE_PERSON"]},"taxId":{"type":"string","description":"The owner's saved VAT number, normalised (uppercase, no spaces)."},"validUntil":{"type":"integer","description":"Integer MILLISECONDS. The lesser of now + 24h and the VIES check's completion time + 24h — the check's own freshness caps the declaration."}},"additionalProperties":false},"AgentTaxDeclarationContent":{"oneOf":[{"type":"object","required":["available","declaration"],"properties":{"available":{"type":"boolean","enum":[true]},"declaration":{"$ref":"#/components/schemas/AgentTaxDeclaration"}},"additionalProperties":false},{"type":"object","required":["available","reason"],"properties":{"available":{"type":"boolean","enum":[false]},"reason":{"$ref":"#/components/schemas/AgentTaxDeclarationUnavailableReason"}},"additionalProperties":false}]},"ReplaceAgentLabelsRequest":{"type":"object","required":["label_ids"],"properties":{"label_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":32}},"additionalProperties":false},"AgentLabelsResponse":{"type":"object","required":["labels"],"properties":{"labels":{"type":"array","items":{"$ref":"#/components/schemas/Label"}}},"additionalProperties":false},"AgentConnectionSetupState":{"type":"string","enum":["awaiting_connection","connected_local","awaiting_wallet_approval","approval_in_progress","proposed","active","expired","cancelled","failed"],"description":"Connect Agent 2 setup state. Pending/proposed states are not payment authority."},"AgentConnectionAllowanceInput":{"type":"object","description":"One requested budget on a connect setup. Its `allowance_amount` is ATOMIC — the opposite shape to the identically named field on AgentAllowance, which is the human-decimal delegation projection (#2295).","required":["token_address","token_symbol","allowance_amount","reset_period_min"],"properties":{"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"token_symbol":{"type":"string","minLength":1,"maxLength":20},"allowance_amount":{"type":"string","pattern":"^[0-9]+$","description":"ATOMIC token amount — an integer string in the token's smallest unit (25 USDC is \"25000000\"). Leading zeroes are accepted and canonicalized; effective amount must be positive and capped at uint96 — the word size of the delegation rail's ERC20PeriodTransferEnforcer. NOT interchangeable with the human-decimal shape the delegation-rail budget projection returns for the same field name — see AgentAllowance.allowance_amount (#2295)."},"reset_period_min":{"type":"integer","minimum":0,"maximum":65535}},"additionalProperties":false},"AgentConnectionAllowance":{"allOf":[{"$ref":"#/components/schemas/AgentConnectionAllowanceInput"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}]},"AgentConnectionWallet":{"type":"object","required":["id","name","address","chain_id","network"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"network":{"type":"string"}},"additionalProperties":false},"AgentConnectionConnector":{"type":"object","properties":{"connector_version":{"type":["string","null"]},"environment_label":{"type":"string"},"runtime_version":{"type":"string"},"config_target":{"type":"string"}},"additionalProperties":false},"AgentConnectionInstallStatus":{"type":"object","properties":{"runtime":{"type":"string"},"runtime_mcp_mode":{"type":"string"},"connector_version":{"type":"string"},"hosted_mcp_configured":{"type":"boolean"},"local_signer_configured":{"type":"boolean"},"local_mcp_configured":{"type":"boolean"},"credential_files_written":{"type":"boolean"},"signer_acknowledged":{"type":"boolean"},"local_mcp_acknowledged":{"type":"boolean"},"activation_command_available":{"type":"boolean"},"manual_credential_fallback":{"type":"boolean"},"skill_installed":{"type":"boolean"},"probe_result":{"type":"string"},"restart_required":{"type":"boolean"},"next_user_action":{"type":"string"},"error_code":{"type":["string","null"]},"environment_label":{"type":"string"},"superseded_agent_ids":{"type":["array","null"],"items":{"type":"string","maxLength":120},"maxItems":50},"last_probe_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string"}]}},"additionalProperties":false},"CreateAgentConnectionSetupRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"account_id":{"type":"string","format":"uuid","description":"The linked account the agent spends from. The retired `safe_id` spelling is REFUSED with a 400 naming this field (#2914) rather than ignored."},"safe_id":{"type":"string","format":"uuid","deprecated":true,"description":"RETIRED (#2914) and REFUSED, not accepted — declared here only so the request-validation layer agrees with the handler, which applies the same reliance rule as `POST /agents`: `safe_id` alone refuses, both-and-matching is accepted, both-and-disagreeing refuses. Undeclared under `additionalProperties: false` it would be rejected by ajv before the handler ever ran."},"runtime":{"type":"string"},"local_mcp":{"type":"boolean","description":"Legacy local-MCP opt-in (#3032: sent by the dashboard, `useAgentConnectionSetup.ts`, whenever the owner picked local MCP). `true` with an explicit runtime outside Claude Code / Codex / Cowork is refused with 400; otherwise it records the preference."},"allowances":{"type":"array","items":{"$ref":"#/components/schemas/AgentConnectionAllowanceInput"}},"issue_passport":{"type":"boolean","description":"Opt in to an L0 Agent Passport for the agent this setup creates. Default false."},"source":{"type":"string","maxLength":64,"description":"Discovery-source slug for connect attribution (#2302) — e.g. 402-page, registry, template, skill. Sanitized server-side; a malformed value is stored as null rather than refused."},"via":{"type":"string","enum":["agent"],"description":"Agent hand-off marker (#2522). Present when the link the user followed was pasted by an agent. An ENUM, not a slug like `source`: it answers one closed question and the agent-driven funnel is segmented on it, so a free-text field would let a link author write anything into that metric. Sanitized server-side; any other value is stored as null rather than refused."}},"additionalProperties":false},"CreateAgentConnectionSetupResponse":{"type":"object","required":["setup_id","status","setup_token","expires_at","connector_command","connector_package","setup_prompt","approval_url"],"properties":{"setup_id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/AgentConnectionSetupState"},"setup_token":{"type":"string","pattern":"^hv_setup_"},"expires_at":{"type":"string","format":"date-time"},"connector_command":{"type":"string"},"connector_package":{"type":"string","pattern":"^@haven_ai/connect@[a-z][a-z0-9-]{0,31}$"},"setup_prompt":{"type":"string"},"approval_url":{"type":"string","format":"uri"}},"additionalProperties":false},"ResolveAgentConnectionSetupRequest":{"type":"object","required":["setup_token"],"properties":{"setup_token":{"type":"string","pattern":"^hv_setup_"},"connector_version":{"type":"string"},"runtime":{"type":"string"}},"additionalProperties":false},"ResolveAgentConnectionSetupResponse":{"type":"object","required":["setup_id","status","agent","haven_wallet","agent_budget","hosted_mcp_url","challenge"],"properties":{"setup_id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/AgentConnectionSetupState"},"agent":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":["string","null"]}},"additionalProperties":false},"haven_wallet":{"$ref":"#/components/schemas/AgentConnectionWallet"},"agent_budget":{"type":"array","items":{"$ref":"#/components/schemas/AgentConnectionAllowance"}},"hosted_mcp_url":{"type":"string","format":"uri"},"challenge":{"type":"object","required":["id","message","expires_at"],"properties":{"id":{"type":"string","format":"uuid"},"message":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}},"additionalProperties":false}},"additionalProperties":false},"RegisterAgentConnectionSetupRequest":{"type":"object","required":["setup_token","challenge_id","delegate_address","proof_signature","api_key_hash","api_key_prefix"],"properties":{"setup_token":{"type":"string","pattern":"^hv_setup_"},"challenge_id":{"type":"string","format":"uuid"},"delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"proof_signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$"},"api_key_hash":{"type":"string","pattern":"^[0-9a-fA-F]{64}$"},"api_key_prefix":{"type":"string","pattern":"^sk_agent_[0-9a-f]{3}$"},"runtime":{"type":"string"},"run_mode":{"type":"string","enum":["json","prose"],"description":"#2528: how the connector was invoked — 'json' when `--json` was passed, 'prose' otherwise. The connector is the only party that can report this: the request is identical over the wire either way. Optional, because a connector older than #2528 sends nothing and registers unchanged; an unrecognised value is refused with 400 rather than stored, since this dimension segments the onboarding funnel and a value nothing recognises must not enter it silently. Case and surrounding whitespace are normalised before storage."},"connector_version":{"type":"string"},"mcp_server_name":{"type":"string","description":"The MCP server name the connector wired this agent under (#3032: sent by `@haven_ai/connect`, `api.ts` `registerSetup`). Normalised server-side: trimmed, and an empty, over-64-character or otherwise malformed name is stored as null rather than refused."},"connector_context":{"$ref":"#/components/schemas/AgentConnectionConnector"},"install_capabilities":{"type":"object","properties":{"can_write_runtime_config":{"type":"boolean"},"restart_required":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"RegisterAgentConnectionSetupResponse":{"type":"object","required":["setup_id","agent_id","status","agent_status","api_key_prefix","api_key_scope","delegate_address","hosted_mcp_url","next_action","approval_url"],"properties":{"setup_id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/AgentConnectionSetupState"},"agent_status":{"type":"string","enum":["pending_approval"]},"api_key_prefix":{"type":"string","pattern":"^sk_agent_"},"api_key_scope":{"type":"string","enum":["setup_pending"]},"delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"hosted_mcp_url":{"type":"string","format":"uri"},"next_action":{"type":"string","enum":["return_to_haven_for_wallet_approval"]},"approval_url":{"type":"string","format":"uri"},"passport_requested":{"type":"boolean","description":"True when the setup opted in and its chain issues L0 passports."}},"additionalProperties":false},"AgentConnectionSetupStatus":{"type":"object","required":["setup_id","status","agent","haven_wallet","agent_budget","connector_package","install_status","approval","approval_url"],"properties":{"setup_id":{"type":"string","format":"uuid"},"agent_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"approval_url":{"type":"string","format":"uri"},"status":{"$ref":"#/components/schemas/AgentConnectionSetupState"},"expires_at":{"type":"string","format":"date-time"},"agent":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":["string","null"]}},"additionalProperties":false},"haven_wallet":{"$ref":"#/components/schemas/AgentConnectionWallet"},"agent_budget":{"type":"array","items":{"$ref":"#/components/schemas/AgentConnectionAllowance"}},"delegate_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"api_key_prefix":{"type":["string","null"]},"runtime":{"type":["string","null"]},"connector_package":{"type":"string","pattern":"^@haven_ai/connect@[a-z][a-z0-9-]{0,31}$"},"connector":{"$ref":"#/components/schemas/AgentConnectionConnector"},"install_status":{"$ref":"#/components/schemas/AgentConnectionInstallStatus"},"approval":{"type":"object","required":["account_tx_hash","tx_hash","status"],"properties":{"account_tx_hash":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{64}$"},"tx_hash":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{64}$"},"status":{"type":"string"}},"additionalProperties":false},"failure_reason":{"type":["string","null"]}},"additionalProperties":false},"UpdateConnectorInstallStatusRequest":{"type":"object","properties":{"setup_token":{"type":"string","pattern":"^hv_setup_"},"runtime":{"type":"string"},"runtime_mcp_mode":{"type":"string"},"connector_version":{"type":"string"},"hosted_mcp_configured":{"type":"boolean"},"local_signer_configured":{"type":"boolean"},"local_mcp_configured":{"type":"boolean"},"credential_files_written":{"type":"boolean"},"signer_acknowledged":{"type":"boolean"},"local_mcp_acknowledged":{"type":"boolean"},"activation_command_available":{"type":"boolean"},"probe_result":{"type":"string"},"restart_required":{"type":"boolean"},"next_user_action":{"type":"string"},"error_code":{"type":["string","null"]},"environment_label":{"type":"string"},"skill_installed":{"type":"boolean","description":"Whether the connector installed the Haven agent skill for this runtime (#3032: sent by `@haven_ai/connect`, `api.ts`)."},"superseded_agent_ids":{"type":["array","null"],"items":{"type":"string"},"description":"Other agent directories the connector found on this machine (#2561, #3032: sent by `@haven_ai/connect`, `api.ts` `updateInstallStatus`). A tri-state: a list (found these), `[]` (scanned, found none), `null` (the scan could not run). Not trusted beyond its shape: entries are trimmed, path- or secret-shaped strings dropped, and the list capped server-side rather than refused."}},"additionalProperties":false},"RecordRetiredAgentsRequest":{"type":"object","required":["retired_agent_ids"],"properties":{"retired_agent_ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":32,"uniqueItems":true,"description":"Ids of the agents `--replace` retired on this machine."}},"additionalProperties":false},"RecordRetiredAgentsResponse":{"type":"object","required":["setup_id","recorded_agent_ids"],"properties":{"setup_id":{"type":"string","format":"uuid"},"recorded_agent_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The subset of the reported ids that was stamped (same user, not the caller itself)."}},"additionalProperties":false},"AgentSupersession":{"type":["object","null"],"required":["agent_id","superseded_at"],"properties":{"agent_id":{"type":"string","format":"uuid","description":"The agent that replaced this one (#3772)."},"superseded_at":{"type":"string","format":"date-time"}},"additionalProperties":false,"description":"#3772: set when `@haven_ai/connect --replace` retired this agent in favour of a newer one — a stale-duplicate WARNING. Reporting metadata only: this agent's budget, status and credentials are unchanged. Null when never superseded, when the replacement has since been revoked or archived, or when the read failed."},"UpdateConnectorInstallStatusResponse":{"type":"object","required":["setup_id","status","install_status"],"properties":{"setup_id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/AgentConnectionSetupState"},"install_status":{"$ref":"#/components/schemas/AgentConnectionInstallStatus"}},"additionalProperties":false},"AgentConnectionConnectorStatus":{"type":"object","required":["status","approved_budget"],"properties":{"status":{"$ref":"#/components/schemas/AgentConnectionSetupState"},"approved_budget":{"oneOf":[{"type":"object","required":["token_symbol","token_address","amount","reset_period_min"],"properties":{"token_symbol":{"type":"string"},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"amount":{"type":"string"},"reset_period_min":{"type":"integer"}},"additionalProperties":false},{"type":"null"}]}},"additionalProperties":false},"AgentAllowance":{"type":"object","description":"One element of an agent's derived budget view (GET /agents, GET /agents/{id}, PUT /agents/{id}; POST /agents carries it as a literal empty array). Projected from the agent's active, unexpired delegations, never from stored allowance rows (#1090/#2020). Its `allowance_amount` is HUMAN-DECIMAL — the opposite shape to the identically named field on AgentConnectionAllowance, which is atomic (#2295).","required":["id","agent_id","token_address","token_symbol","allowance_amount","reset_period_min"],"properties":{"id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"token_symbol":{"type":"string"},"allowance_amount":{"type":"string","pattern":"^(0|[0-9]+\\.[0-9]{2,6})$","description":"HUMAN-DECIMAL token amount — whole token units, NOT the atomic integer (25 USDC is \"25.00\", a zero budget is \"0\"). Projected from the agent's active delegation by rails/delegation-budget-view.ts via formatTokenValue(budget_atomic, decimals), whose output is always \"0\" or <integer>.<2–6 fraction digits> — so this pattern REJECTS an atomic value such as \"500\" (#2408). \"0\" is the one value both shapes share. Do not BigInt() this value: it is the shape that made #2283 a production bug. To compare it against an atomic price, scale it by the token's decimals first (#2295)."},"reset_period_min":{"type":"integer"}},"additionalProperties":false},"Agent":{"type":"object","required":["id","name","delegate_address","account_id","account_address","account_name","account_chain_id","api_key_prefix","status","created_at","allowances","labels","organization_id","tax_declaration_enabled"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"delegate_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"account_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"account_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"account_name":{"type":["string","null"]},"account_chain_id":{"type":["integer","null"]},"account_type":{"type":["string","null"]},"api_key_prefix":{"type":["string","null"]},"status":{"type":"string","enum":["active","paused","pending_approval","revoked"]},"created_at":{"type":"string","format":"date-time"},"archived_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"allowances":{"type":"array","items":{"$ref":"#/components/schemas/AgentAllowance"}},"labels":{"type":"array","items":{"$ref":"#/components/schemas/Label"}},"organization_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"tax_declaration_enabled":{"type":"boolean"},"mcp_last_seen_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"mcp_server_name":{"type":["string","null"]},"has_stranded_funds":{"type":"boolean"},"live_delegation_count":{"type":"integer","minimum":0}},"additionalProperties":true},"CreateAgentRequest":{"type":"object","required":["name","delegate_address"],"properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"account_id":{"type":"string","format":"uuid","description":"The linked account the agent spends from. The retired `safe_id` spelling is REFUSED with a 400 naming this field (#2914) rather than ignored — an ignored account id would create an unlinked agent that looks successfully created."},"safe_id":{"type":"string","format":"uuid","deprecated":true,"description":"RETIRED (#2914) and REFUSED, not accepted — declared here only so the request-validation layer agrees with the handler. #2908 told every published client to send both names for the window, so the handler applies a RELIANCE rule: `safe_id` alone is refused with a 400 naming `account_id`, both-and-matching is accepted (the new name is read), both-and-disagreeing is refused. Were this field left undeclared under `additionalProperties: false`, a correctly dual-sending client would raise a `would_refuse` shadow counter today and be rejected outright the moment request validation is set to `enforce` — silently reversing the handler decision above."},"allowances":{"type":"array","maxItems":0,"description":"RETIRED (#1440/#2020): per-token allowances died with the Safe rail. A non-empty array is refused with 400 — grant the agent a budget delegation after creation instead. The field survives (empty-only) so older clients sending `allowances: []` keep working.","items":{"type":"object","additionalProperties":true}}},"additionalProperties":false},"DelegateBalance":{"type":"object","required":["delegate_address","account_address","chain_id","eth","eth_atomic","usdc","usdc_atomic","usdc_address","sweep_min_usdc"],"properties":{"delegate_address":{"type":"string"},"account_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"chain_id":{"type":"integer"},"eth":{"type":"string"},"eth_atomic":{"type":"string"},"usdc":{"type":"string"},"usdc_atomic":{"type":"string"},"usdc_address":{"anyOf":[{"type":"string"},{"type":"null"}]},"sweep_min_usdc":{"type":"string","description":"Minimum USDC balance eligible for gasless delegate recovery in human token units."}}},"CreateAgentResponse":{"allOf":[{"$ref":"#/components/schemas/Agent"},{"type":"object","required":["api_key","passport_requested"],"properties":{"api_key":{"type":"string","pattern":"^sk_agent_"},"passport_requested":{"type":"boolean"}}}]},"CreatePaymentRequest":{"type":"object","required":["token","amount","to"],"properties":{"token":{"type":"string","examples":["USDC"]},"amount":{"type":"string","description":"Human-readable token amount."},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"idempotency_key":{"type":"string","minLength":1,"maxLength":128,"description":"Optional dedupe key (#1207): a retried request with the same key returns the first request's result (idempotent_replay: true) instead of minting a second transfer or approval. A key reused for a different transfer — token, recipient, amount or task budget (#3392) — is a 409."},"task_budget_id":{"type":"string","minLength":1,"description":"#3329: an OPEN task budget to authorize this payment through, instead of the budget delegation directly — the redemption chain becomes [taskChild, budget]. Refused with 404 task_budget_not_found, 409 task_budget_not_open/token_mismatch/recipient_mismatch/parent_mismatch, or (#3500) 403 task_budget_exceeded when the task child's ERC20TransferAmountEnforcer shows too little left of its cap (task_budget_id, remaining_atomic, max_atomic, amount_atomic; decided before anything is built, fails open on an unreadable chain). Part of the idempotency pin (#3392): a key replayed under a different task budget — or none, or from none to one — is a 409, not a replay charged to another budget."},"sub_budget_id":{"type":"string","minLength":1,"description":"#3330: an OPEN sub-budget another agent in the same account granted THIS agent — the redemption chain becomes [grant, parent-child, budget]. Mutually exclusive with task_budget_id (400 when both). Refused with 404 sub_budget_not_found, 409 sub_budget_not_open/sub_budget_parent_mismatch/sub_budget_token_mismatch/sub_budget_recipient_mismatch, or (#3519) 403 delegation_budget_exceeded when the smallest live remaining across the three links cannot cover the amount (fails open per link on an unreadable read). Part of the idempotency pin, like task_budget_id: a key replayed under a different sub-budget is a 409."}},"additionalProperties":false},"SignablePaymentIntent":{"type":"object","required":["payment_id","status","expires_at","sign_data"],"properties":{"payment_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending_signature"]},"expires_at":{"type":"string","format":"date-time"},"sign_data":{"type":"object","required":["hash","signature_scheme","typed_data","components","instructions"],"properties":{"hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The UserOperation hash. Present for reference and replay-matching — do NOT sign it directly; sign `typed_data`."},"signature_scheme":{"type":"string","enum":["eip712_userop"],"description":"The delegation rail's only scheme. The retired AllowanceModule rail's raw-ECDSA-over-`hash` scheme died with it (#1986) and is not offered here."},"typed_data":{"type":"object","additionalProperties":true,"description":"The EIP-712 payload to sign VERBATIM with the delegate key. The account validates this, not `hash`."},"components":{"type":"object","required":["token","to","amount"],"properties":{"account":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The delegator account the UserOperation runs on."},"payer_account":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Present on the x402 funding shape only. Same value as the deprecated `safe`."},"token":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"amount":{"type":"string","description":"Atomic token amount."}},"additionalProperties":false},"instructions":{"type":"string"}},"additionalProperties":false},"recipient":{"type":"object","description":"Advisory, history-only recipient classification. Present only on a direct `POST /payments` response (fresh or idempotent-replay) and only when the underlying read succeeded — absent, never null, otherwise. `x402/authorize` never sets it, even though this schema is shared with its response shape.","required":["class"],"properties":{"class":{"type":"string","enum":["previously_paid","new_address"],"description":"`previously_paid`: this agent has a prior CONFIRMED payment (any rail, any token) to this exact recipient address on this chain. `new_address`: it does not. Never derived from another agent, another owner, or any own_account/contact/catalog lookup."}},"additionalProperties":false}},"additionalProperties":false},"DirectSignContext":{"type":"object","required":["payment_id","status","expires_at","direct_sign_context_version","sign_data"],"properties":{"payment_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending_signature"]},"expires_at":{"type":"string","format":"date-time"},"direct_sign_context_version":{"type":"integer","enum":[1],"description":"DIRECT_SIGN_CONTEXT_VERSION from @haven_ai/sdk (`userop-binding.ts`) — the version every client's assertUserOpTypedDataBinding pins against."},"sign_data":{"type":"object","required":["hash","signature_scheme","typed_data"],"properties":{"hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The stored ERC-4337 v0.7 UserOperation hash. Present for the integrity check (#3271) — do NOT sign it directly; sign typed_data."},"signature_scheme":{"type":"string","enum":["eip712_userop"]},"typed_data":{"type":"object","additionalProperties":true,"description":"The EIP-712 PackedUserOperation payload to sign VERBATIM, byte-identical to the typed_data the original POST /payments (or its idempotent replay) returned for this intent."}},"additionalProperties":false}},"additionalProperties":false},"PaymentIntentStatus":{"type":"object","required":["payment_id","status","token","amount","to","tx_hash","error_message","created_at","signed_at","submitted_at","confirmed_at","expires_at"],"properties":{"payment_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"chain_id":{"type":"integer"},"token":{"type":"string"},"amount":{"type":"string"},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"tx_hash":{"type":["string","null"]},"explorer_url":{"type":["string","null"]},"error_message":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"signed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"submitted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"expires_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"PaymentExecutionResult":{"type":"object","required":["payment_id","status"],"properties":{"payment_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"tx_hash":{"type":"string"},"chain_id":{"type":"integer"},"explorer_url":{"type":"string"},"token":{"type":"string"},"amount":{"type":"string"},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"error":{"type":"string"},"details":{"type":"string"}},"additionalProperties":true},"PaymentListItem":{"type":"object","required":["payment_id","status","token","amount","to","tx_hash","created_at","confirmed_at"],"properties":{"payment_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"token":{"type":"string"},"amount":{"type":"string"},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"tx_hash":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"additionalProperties":false},"RailContext":{"type":"object","required":["amount_atomic","asset","network","resource_url","merchant_address","description","idempotency_key"],"properties":{"amount_atomic":{"type":["string","null"]},"asset":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"network":{"type":["string","null"]},"resource_url":{"type":["string","null"],"format":"uri"},"merchant_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"description":{"type":["string","null"],"description":"The same value as the status `description` (#3610): the merchant's resource description, untrusted display text, cut at 300 code points plus an ellipsis; null when the merchant gave none."},"idempotency_key":{"type":["string","null"]}},"additionalProperties":false},"Parties":{"type":"object","required":["treasury_account","delegate","delegate_account","merchant"],"properties":{"treasury_account":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}],"description":"The owner smart account the funds left."},"delegate":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}],"description":"The agent's signing EOA."},"delegate_account":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}],"description":"The agent's delegate smart account (erc7710 `delegator`), persisted at authorize time. Null for rows authorized before #2960 and on the legacy rail."},"merchant":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}],"description":"`payTo`."},"buyer":{"$ref":"#/components/schemas/PartiesBuyer","description":"The paying agent's owner's company details, when the owner has saved them and the deployment has the feature on (#3332). Present only on `GET /payments/:id/receipt` and `GET /machine-payments/receipts` — never on `GET /machine-payments/:id/status` or the `POST /machine-payments/evidence` attach echo, even with the feature on (out of scope for #3332; see docs/product/owner-company-details.md). Absent otherwise."}},"additionalProperties":false},"PartiesBuyer":{"type":"object","required":["legal_name","country","org_number","vat_number","vies_status","vies_checked_at"],"properties":{"legal_name":{"type":"string"},"country":{"type":"string","pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2."},"org_number":{"type":"string"},"vat_number":{"type":["string","null"]},"vies_status":{"type":["string","null"],"enum":["pending","valid","invalid","not_verifiable",null]},"vies_checked_at":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false},"AgentPaymentStatus":{"type":"object","required":["payment_id","kind","rail","status","phase","next_action","amount","token","resource_url","merchant_address","tx_hash","expires_at","chain_id","message"],"properties":{"payment_id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["payment_intent","approval_request"],"description":"Always `payment_intent` in practice. `approval_request` is kept for wire compatibility in the #2055 style: the approval queue died with the Safe rail and `approval_requests` was dropped (migration 070), so no payment can resolve to that kind any more — but the value stays declared because the backend's own status type still carries it and this route still serializes the field. Do not write a branch on it."},"rail":{"$ref":"#/components/schemas/AgentPaymentRail"},"status":{"type":"string"},"phase":{"$ref":"#/components/schemas/AgentPaymentPhase"},"next_action":{"$ref":"#/components/schemas/AgentPaymentNextAction"},"amount":{"type":"string","description":"Human-readable token amount."},"token":{"type":"string"},"resource_url":{"type":["string","null"],"format":"uri"},"merchant_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"payer_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}],"deprecated":true,"description":"Delegate EOA captured on a payment intent. Deprecated: same value as `parties.delegate`; prefer `parties`."},"parties":{"$ref":"#/components/schemas/Parties"},"tx_hash":{"type":["string","null"],"pattern":"^0x[0-9a-fA-F]{64}$"},"expires_at":{"type":"string","format":"date-time"},"chain_id":{"type":"integer"},"message":{"type":"string"},"delivered":{"type":"boolean","description":"True when the merchant answered 2xx and the response is recorded (evidence row). Omitted when unknown."},"submission_outcome_pending":{"type":"boolean","description":"True while the payment was submitted but its on-chain outcome is not known yet (#3564): do not create a new payment — the status becomes the real outcome once Haven reconciles it from the chain. Omitted on every other row."},"settlement_scheme":{"anyOf":[{"type":"string","enum":["eip3009","erc7710"]},{"type":"null"}],"description":"Which settlement branch this x402 payment runs (#946), from machine_metadata. Null on the legacy rail, on any x402 intent whose scheme metadata predates #946, or any stored value outside this enum."},"merchant_settlement_recorded":{"type":"boolean","description":"True only when an eip3009 payment's merchant settlement transaction is already recorded and on-chain-verified (#3475). Always omitted on erc7710, whose one settlement transaction IS the confirmed intent rather than a separately recorded hash. Omitted — never false — when unknown."},"failure_reason":{"type":["string","null"],"description":"A bounded, redacted cause for a `failed` payment — the stored error message (already scrubbed of vendor secrets before it was written), capped so a viem/bundler dump never rides this response. Present (possibly `null`, when no message was recorded) only when `status` is `failed`; omitted on every other status."},"budget_delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The budget delegation that metered this payment (#1059), recorded at authorize. The settle summary keys its allowance rows on this — the budget that PAID, never a re-derived (token, payee) first match. Omitted on the legacy rail and on intents predating migration 053."},"fee":{"type":["object","null"],"properties":{"amount":{"type":"string"},"token":{"type":"string"},"basis_points":{"type":"integer"},"applied":{"type":"boolean"}},"required":["amount","token","basis_points","applied"],"additionalProperties":false},"amount_atomic":{"type":["string","null"]},"asset":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"network":{"type":["string","null"]},"description":{"type":["string","null"],"description":"What the payment was for, in the merchant's own words — untrusted display text. On x402 it is the 402's `resource.description` (persisted at authorize since #3610; read from the stored 402 on older rows), control and bidi characters stripped, trimmed and cut at 300 code points plus an ellipsis. Null when the merchant gave none."},"idempotency_key":{"type":["string","null"]},"x402":{"$ref":"#/components/schemas/RailContext"},"mpp":{"type":"object","required":["amount_atomic","asset","network","resource_url","merchant_address","description","idempotency_key"],"properties":{"amount_atomic":{"type":["string","null"]},"asset":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"network":{"type":["string","null"]},"resource_url":{"type":["string","null"],"format":"uri"},"merchant_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"description":{"type":["string","null"]},"idempotency_key":{"type":["string","null"]},"challenge_id":{"type":["string","null"]}},"additionalProperties":false}},"additionalProperties":false},"X402PaymentOption":{"type":"object","required":["scheme","network","amount","asset","payTo","maxTimeoutSeconds"],"properties":{"scheme":{"type":"string","enum":["exact"]},"network":{"type":"string"},"amount":{"type":"string"},"maxAmountRequired":{"type":"string"},"resource":{"type":"string"},"description":{"type":"string"},"mimeType":{"type":"string"},"asset":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"payTo":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"maxTimeoutSeconds":{"type":"integer"},"extra":{"type":"object","additionalProperties":true}},"additionalProperties":true},"X402PaymentRequired":{"type":"object","required":["x402Version","resource","accepts"],"properties":{"x402Version":{"type":"integer"},"resource":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"description":{"type":"string"},"mimeType":{"type":"string"}},"additionalProperties":true},"accepts":{"type":"array","items":{"$ref":"#/components/schemas/X402PaymentOption"}},"error":{"type":"string"}},"additionalProperties":true},"X402AuthorizeRequest":{"type":"object","required":["url","payTo","amount","asset","network"],"properties":{"url":{"type":"string","format":"uri"},"payTo":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"merchantPayTo":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"amount":{"type":"string","pattern":"^[0-9]+$","description":"Atomic token amount from the x402 challenge. Digits only; the route additionally refuses zero (`isPositiveDecimalAtomicAmount`), which JSON Schema does not express."},"asset":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"network":{"type":"string","examples":["base","eip155:8453"]},"description":{"type":"string","description":"The 402's `resource.description` (untrusted merchant text). Persisted, with control and bidi characters stripped, trimmed and cut at 300 code points plus an ellipsis, as the payment status `description` (#3610); when omitted, the stored `paymentRequired`'s `resource.description` is used."},"maxTimeoutSeconds":{"type":"number","minimum":1},"category":{"type":"string"},"idempotencyKey":{"type":"string","minLength":1,"maxLength":128},"settlementScheme":{"type":"string","enum":["erc7710","eip3009"]},"facilitatorAddresses":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"description":"#1058: the erc7710 challenge entry's extra.facilitatorAddresses — the facilitator pin carried into the settlement child delegation."},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]{130}$"},"mcpCallContext":{"type":"object","description":"#1307: the merchant MCP-tool call this quote was made against (haven_pay_mcp_tool / haven_prepare_catalog_purchase). Persisted so GET /x402/{id}/merchant-call-context can rehydrate it at settle/complete time. #3781: catalogName carries Haven's own catalog row name when the purchase came from a catalog entry — display only, never merchant content.","required":["merchantUrl","toolName"],"properties":{"merchantUrl":{"type":"string","format":"uri"},"toolName":{"type":"string","minLength":1},"arguments":{"type":"object","additionalProperties":true},"catalogName":{"type":"string","minLength":1},"mcpTransport":{"type":"object","required":["handshakeRequired","source"],"properties":{"handshakeRequired":{"type":"boolean"},"source":{"type":"string","enum":["path","bazaar"]}},"additionalProperties":false}},"additionalProperties":false},"paymentRequired":{"type":"object","description":"#1355: the merchant's full parsed 402 PaymentRequired (max 64KB serialized). Persisted so GET /x402/{id}/sign-context can re-serve it and a local signer needs only payment_id. Reporting material, not payment authority — the signer verifies whichever copy it uses against the Haven-signed expected context.","additionalProperties":true},"taskBudgetId":{"type":"string","minLength":1,"description":"#3329: an OPEN task budget to authorize this settlement through, instead of the budget delegation directly. erc7710: the settlement child is carved from the task budget's signed child ([settlement, taskChild, budget]). EIP-3009: the funding leg redeems the same chain to fund the agent's delegate EOA. Refused with 404 task_budget_not_found, 409 task_budget_not_open/token_mismatch/recipient_mismatch/parent_mismatch, or (#3500) 403 task_budget_exceeded when the task child's ERC20TransferAmountEnforcer shows too little left of its cap (task_budget_id, remaining_atomic, max_atomic, amount_atomic; decided before anything is built, fails open on an unreadable chain). Part of the idempotency pin (#3392): a key replayed under a different task budget — or none, or from none to one — is a 409, not a replay charged to another budget."},"subBudgetId":{"type":"string","minLength":1,"description":"#3330: an OPEN sub-budget another agent in the same account granted THIS agent. erc7710: the settlement child is carved under the grant ([settlement, grant, parent-child, budget]). EIP-3009: the funding leg redeems [grant, parent-child, budget] to fund the agent's delegate EOA. Mutually exclusive with taskBudgetId (400 when both). Refused with 404 sub_budget_not_found, 409 sub_budget_not_open/sub_budget_parent_mismatch/sub_budget_token_mismatch/sub_budget_recipient_mismatch, or 403 delegation_budget_exceeded. Shared by POST /x402 and POST /x402/authorize."}},"additionalProperties":false},"X402MerchantCallContext":{"type":"object","required":["payment_id","merchant_url","tool_name"],"properties":{"payment_id":{"type":"string","format":"uuid"},"merchant_url":{"type":"string","format":"uri"},"tool_name":{"type":"string"},"arguments":{"type":"object","additionalProperties":true},"catalog_name":{"type":"string","minLength":1},"mcp_transport":{"type":"object","properties":{"handshake_required":{"type":"boolean"},"source":{"type":"string","enum":["path","bazaar"]}},"additionalProperties":false}},"additionalProperties":false},"X402SignablePayment":{"allOf":[{"$ref":"#/components/schemas/SignablePaymentIntent"},{"type":"object","required":["x402_expected_auth"],"properties":{"chain_id":{"type":"integer"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"payer":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"token":{"type":"string"},"amount":{"type":"string"},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"merchant_to":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"resource_url":{"type":"string","format":"uri"},"x402_expected_auth":{"type":"object","required":["version","message","signature","signer"],"properties":{"version":{"type":"integer","enum":[2,3],"description":"Contents-derived, never chosen: 2 = commits to the EIP-712 typedDataHash (delegation rail, #1138); 3 = additionally binds the payer identity (#1690). Version 1 (hash-only, retired Safe rail) is never emitted since #3272 — signX402ExpectedContext requires typedDataHash — and the signer refuses it."},"message":{"type":"string","description":"Haven-signed expected x402 context. Includes expiresAt when the funding window is time-bound."},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]{130}$"},"signer":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},"additionalProperties":false},"payer_delegate":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"#1690: the delegate this quote was created FOR, bound inside the Haven-signed expected context (version 3). The edge signer refuses to sign when it is not its own delegate — the guard that turns a stale-host quote-as-A-sign-as-B into a named refusal instead of an on-chain revert. Emitted only when the deployment has flipped X402_EMIT_PAYER_CONTEXT (signer-first rollout)."},"payer_agent_id":{"type":"string","description":"#1690: the paying agent's id, carried so the signer's refusal can name both sides. Same gate as payer_delegate."},"payment_required":{"type":"object","description":"#1355: the stored 402 PaymentRequired, present on GET /x402/{id}/sign-context responses when it was persisted at authorize time. Lets the local signer build the merchant header from the context fetch alone.","additionalProperties":true}}}]},"X402ConfirmedPayment":{"type":"object","required":["success","payment_id","status","tx_hash"],"properties":{"success":{"type":"boolean"},"payment_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"tx_hash":{"type":"string"},"chain_id":{"type":"integer"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"payer":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"token":{"type":"string"},"amount":{"type":"string"},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"merchant_to":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"resource_url":{"type":"string","format":"uri"},"explorer_url":{"type":"string"}},"additionalProperties":false},"X402ResumeState":{"type":"object","required":["rail","paymentId","idempotencyKey","paymentRequired","accepted","url","resourceUrl","amountAtomic","amount","token","asset","network","chainId","merchantAddress"],"properties":{"rail":{"type":"string","enum":["x402"]},"paymentId":{"type":"string","format":"uuid"},"idempotencyKey":{"type":"string"},"paymentRequired":{"$ref":"#/components/schemas/X402PaymentRequired"},"accepted":{"$ref":"#/components/schemas/X402PaymentOption"},"url":{"type":"string","format":"uri"},"request":{"$ref":"#/components/schemas/SerializableRequest"},"resourceUrl":{"type":"string","format":"uri"},"description":{"type":["string","null"]},"amountAtomic":{"type":"string"},"amount":{"type":"string"},"token":{"type":"string"},"asset":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"network":{"type":"string"},"chainId":{"type":["integer","null"]},"merchantAddress":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},"additionalProperties":false},"SerializableRequest":{"type":"object","required":["url","method","headers"],"properties":{"url":{"type":"string","format":"uri"},"method":{"type":"string"},"headers":{"type":"array","items":{"type":"array","prefixItems":[{"type":"string"},{"type":"string"}],"minItems":2,"maxItems":2}},"body":{"type":"string"}},"additionalProperties":false},"MachinePaymentChallenge":{"type":"object","required":["rail","version","challengeId","resource","description","network","asset","amount","recipient","expiresAt"],"properties":{"rail":{"type":"string","enum":["mpp_demo","mpp_crypto","stripe_deposit","spt"]},"version":{"type":"string"},"challengeId":{"type":"string"},"resource":{"type":"string","format":"uri"},"description":{"type":"string"},"network":{"type":"object","required":["chainId","name"],"properties":{"chainId":{"type":"integer"},"name":{"type":"string","enum":["base"]}},"additionalProperties":false},"asset":{"type":"object","required":["symbol","address","decimals"],"properties":{"symbol":{"type":"string","enum":["USDC"]},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"decimals":{"type":"integer","enum":[6]}},"additionalProperties":false},"amount":{"type":"object","required":["display","atomic"],"properties":{"display":{"type":"string"},"atomic":{"type":"string"}},"additionalProperties":false},"recipient":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"expiresAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":true}},"additionalProperties":false},"MachinePaymentAuthorizeRequest":{"type":"object","required":["challenge","idempotencyKey"],"properties":{"challenge":{"$ref":"#/components/schemas/MachinePaymentChallenge"},"idempotencyKey":{"type":"string"},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]{130}$"}},"additionalProperties":false},"MachinePaymentAuthorizeResponse":{"oneOf":[{"$ref":"#/components/schemas/AgentPaymentStatus"},{"$ref":"#/components/schemas/X402SignablePayment"},{"$ref":"#/components/schemas/X402ConfirmedPayment"}]},"MppResumeState":{"type":"object","required":["rail","paymentRail","paymentId","idempotencyKey","challenge","url","resourceUrl","amountAtomic","amount","token","asset","network","chainId","merchantAddress","expiresAt"],"properties":{"rail":{"type":"string","enum":["mpp"]},"paymentRail":{"type":"string"},"paymentId":{"type":"string","format":"uuid"},"idempotencyKey":{"type":"string"},"challenge":{"$ref":"#/components/schemas/MachinePaymentChallenge"},"url":{"type":"string","format":"uri"},"request":{"$ref":"#/components/schemas/SerializableRequest"},"resourceUrl":{"type":"string","format":"uri"},"description":{"type":["string","null"]},"amountAtomic":{"type":"string"},"amount":{"type":"string"},"token":{"type":"string"},"asset":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"network":{"type":"string"},"chainId":{"type":"integer"},"merchantAddress":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"expiresAt":{"type":"string","format":"date-time"}},"additionalProperties":false},"PaymentResumeState":{"oneOf":[{"$ref":"#/components/schemas/X402ResumeState"},{"$ref":"#/components/schemas/MppResumeState"}]},"MachinePaymentAgent":{"type":"object","required":["id","name","status","account_address","delegate_address","delegate_account_address","chain_id","execution_rail","superseded_by"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegate_account_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"superseded_by":{"$ref":"#/components/schemas/AgentSupersession"},"chain_id":{"type":"integer"},"execution_rail":{"type":"string","enum":["legacy","delegation"],"description":"Which rail this agent's account is on (#1306). `delegation` means spend is gated by the agent's active budget delegations, enforced on-chain by the caveat enforcers. `legacy` is the RETIRED Safe / AllowanceModule rail (#1986) and gates nothing any more — every payment entry point answers 410 for such an account, so read it as \"this account cannot pay until it re-onboards via POST /accounts/hybrid\", not as a second live policy primitive. Reporting only; the enum keeps both values because a retired-rail account can still read its own identity here."},"parent_sub_budgets":{"type":"array","items":{"type":"object","required":["sub_budget_id","parent_agent_id","parent_agent_name","token_address","period_amount_atomic","expires_at","status","is_expired"],"properties":{"sub_budget_id":{"type":"string","format":"uuid"},"parent_agent_id":{"type":"string","format":"uuid"},"parent_agent_name":{"type":"string"},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"recipient_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"period_amount_atomic":{"type":"string"},"expires_at":{"type":"integer"},"status":{"type":"string","enum":["open","closing"]},"is_expired":{"type":"boolean"}},"additionalProperties":false},"description":"Sub-budgets agent A granted this agent (B) (#3330): period-scoped ERC-7710 children of A’s own budget delegation. `period_amount_atomic` and `expires_at` are the EFFECTIVE (narrower) limits; `is_expired` is derived. Enforcement stays on-chain through the [grant, parent-child, budget] redemption chain."}},"additionalProperties":false},"AllowanceSummary":{"type":"object","required":["agent_id","account_address","delegate_address","chain_id","allowances","superseded_by"],"properties":{"agent_id":{"type":"string","format":"uuid"},"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer"},"superseded_by":{"$ref":"#/components/schemas/AgentSupersession"},"allowances":{"type":"array","items":{"type":"object","required":["id","token_address","token_symbol","configured_amount","reset_period_min","onchain"],"properties":{"id":{"type":"string","format":"uuid"},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"token_symbol":{"type":"string"},"configured_amount":{"type":"string","pattern":"^(0|[0-9]+\\.[0-9]{2,6})$","description":"HUMAN-DECIMAL token amount — whole token units, NOT the atomic integer (25 USDC is \"25.00\", a zero budget is \"0\"). Projected from the agent's active delegation by rails/delegation-budget-view.ts via formatTokenValue(budget_atomic, decimals), whose output is always \"0\" or <integer>.<2–6 fraction digits> — so this pattern REJECTS an atomic value such as \"500\" (#2408). \"0\" is the one value both shapes share. Do not BigInt() this value: it is the shape that made #2283 a production bug. To compare it against an atomic price, scale it by the token's decimals first (#2295)."},"reset_period_min":{"type":"integer"},"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"#3518: this budget delegation's hash — the identifier a payment authorization records as budget_delegation_hash and the key `reserved_haven_atomic` sums children under."},"recipient_address":{"type":["string","null"],"pattern":"^0x[0-9a-f]{40}$","description":"#3518: the recipient pin — null for an open budget; a recipient-scoped budget pays ONLY this address (a payment's selection prefers the pin matching its payee)."},"merchant_id":{"type":["string","null"],"format":"uuid","description":"#3518: the merchant this budget was issued for (#3331), null for every other budget. Never set without a recipient pin."},"reserved_haven_atomic":{"type":"string","description":"#3518: Haven-side reservation — the sum of this budget's OPEN, unexpired task- and sub-budget children's caps (`agent_task_budgets` + `agent_sub_budgets`, joined by delegation_hash), in ATOMIC units. Reported BESIDE `onchain.remaining` and never folded into it: the on-chain figure stays authoritative, a reservation releases on close/expire without any chain event, and \"0\" covers both no-reservation and a failed read (the sum is best-effort)."},"funds_cover_remaining":{"type":["boolean","null"],"description":"#3731: whether the account's balance can back THIS row's whole remaining period budget. One `balanceOf` read per distinct token; each row is compared ALONE (several rows for one token do not add up, so two `true` rows do not mean both are backed at once). `false` means the account cannot back the whole remaining budget — a heads-up to mention to the user, NOT a refusal and NOT proof the next payment fails: a budget larger than the balance is a normal setup (an owner may top up weekly). `null` means unverifiable: the chain read failed, or `onchain.remaining` was not read live (`remaining_is_from_chain` false — the fallback figure cannot answer a holdings question). Absent when `onchain.remaining` is 0 (the compare would be vacuously true). Only the boolean rides the wire; the balance itself never does."},"onchain":{"type":"object","required":["amount","spent","remaining","effective_spent","reset_time_min","last_reset_min","nonce","is_reset_pending"],"properties":{"amount":{"type":"string","description":"The configured period budget in ATOMIC units — the same budget as the sibling `configured_amount`, which states it in whole token units. `spent`, `remaining` and `effective_spent` are atomic too (#2295)."},"spent":{"type":"string"},"remaining":{"type":"string"},"effective_spent":{"type":"string"},"reset_time_min":{"type":"integer"},"last_reset_min":{"type":"integer"},"nonce":{"type":"integer"},"is_reset_pending":{"type":"boolean"},"remaining_is_from_chain":{"type":"boolean","description":"Delegation rail only (#1319, provenance for #1145's fallback): true when `remaining` came from a live ERC20PeriodTransferEnforcer read, false when the read failed and this is the fallback full configured budget. #2105: it is now effectively always present, because this whole summary is a delegation-rail response — since #2020 the retired Safe / AllowanceModule rail answers 410 here rather than a summary, so there is no longer a second rail for the field to be absent on. Reporting only — the on-chain policy is the actual gate; this only says how fresh this number is."}},"additionalProperties":false}},"additionalProperties":false}}},"additionalProperties":false},"BudgetPrecheckRequest":{"type":"object","description":"The merchant quote facts the guided prepare asks Haven to pre-check (#3054). camelCase like the route family. `resourceUrl` is the merchant resource being bought — the refusal dedupe window's discriminating column — never this endpoint's own URL.","required":["token","amountAtomic"],"properties":{"chainId":{"type":"integer","description":"Advisory: the compare is scoped to the authenticated agent's own chain."},"token":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The quote's asset contract address — the SELECTED settlement option's asset."},"amountAtomic":{"type":"string","pattern":"^[0-9]+$","description":"The amount that would be authorized, in ATOMIC units, as a non-negative integer string."},"merchantTo":{"type":"string","description":"The merchant payTo address from the selected option, carried onto the refusal row. #3518: it scopes the compare the way the payment selects its budget — a recipient-pinned budget for this payee wins, a pin to another payee is excluded, and the open budget covers the rest. Absent: only the open budget is eligible; when the agent holds only merchant-locked budgets for the token the answer is 409 budget_requires_recipient (nothing recorded), never a budget refusal. #3492/#3527: when `idempotencyKey` is also present, this field additionally scopes the settled-replay match below — a replay answer requires it to equal the stored row's payee."},"resourceUrl":{"type":"string","description":"The merchant resource being bought. Lands on the refusal row's dedupe key when the pre-check refuses. #3492/#3527: when `idempotencyKey` is also present, this field additionally scopes the settled-replay match below — required, and must equal the stored row's resource. The two settlement schemes persist a DIFFERENT value into that stored column at authorize time (erc7710: the caller's resourceUrl; eip3009: the merchant's own `paymentRequired.resource.url`), so the CALLER is responsible for sending the value that matches what was stored for the scheme it is replaying."},"idempotencyKey":{"type":"string","minLength":1,"maxLength":128,"description":"#3492/#3527: the x402 idempotency key of the quote this pre-check describes. When it resolves to an already-SETTLED erc7710 OR eip3009 payment matching this same quote (confirmed, a tx_hash, settlement_scheme erc7710 or eip3009, and the SAME token/amountAtomic/merchantTo/resourceUrl this request names — both of the latter two are REQUIRED for a replay match, no task- or sub-budget pin), the pre-check answers sufficient — with `replay: true` — WITHOUT comparing against the now-lower remaining budget and WITHOUT recording a payment_refusals row: the money already moved (on eip3009, its funding leg did — the same fact the x402 replay guard already treats as replayable for every scheme), so re-refusing it as over-budget would be a false ledger row. Any other shape (no row, a pending child, a key collision on a different quote, or a task/sub-budget-scoped row) leaves today's compare unchanged. Omitted: unchanged behavior."}},"additionalProperties":false},"BudgetPrecheckResponse":{"type":"object","description":"The sufficient branch of the server-side budget pre-check (#3054). The insufficient answer is not this schema — it is the 403 delegation_budget_exceeded refusal, which also lands a payment_refusals row with source \"hosted_prepare\".","required":["sufficient","remaining_atomic"],"properties":{"sufficient":{"type":"boolean","description":"Always true on this schema — insufficiency refuses 403 instead."},"remaining_atomic":{"type":"string","description":"The remaining period budget for the requested token, in ATOMIC units, after deciding this quote fits."},"remaining_is_from_chain":{"type":"boolean","description":"#1319 provenance, same semantics as the allowances read's flag: true when the remaining figure came from a live ERC20PeriodTransferEnforcer read, false when it fell back to the configured budget. Absent when no budget row existed for the token (nothing was read)."},"budget_id":{"type":"string","format":"uuid","description":"#3518: the budget row the remaining figure describes — the payment-selection mirror's winner (recipient match for merchantTo, else the open budget), not the first per-token row. Absent when no row matched."},"budget_delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"#3518: the selected budget's delegation hash — the same identifier a payment authorization records as budget_delegation_hash, so a caller can verify report and payment name the same budget."},"budget_recipient_address":{"type":["string","null"],"pattern":"^0x[0-9a-f]{40}$","description":"#3518: the selected budget's recipient pin — null for an open budget, the merchant payee for a pinned/merchant-locked one."},"budget_merchant_id":{"type":["string","null"],"format":"uuid","description":"#3518: the merchant the selected budget was issued for (#3331), null for every other budget."},"replay":{"type":"boolean","description":"#3492/#3527: present and true only when sufficiency was decided because `idempotencyKey` resolved to an already-settled erc7710 OR eip3009 replay of this exact quote — NOT because `remaining_atomic` covers `amountAtomic` (it may not, on this branch: the settlement already spent it). Absent on every other sufficient answer."}},"additionalProperties":false},"BalanceCoverageResponse":{"type":"object","description":"The #3126 sufficiency answer — whether HELD funds cover the checked amount. Deliberately NOT a balance: no field carries the account's balance, and nothing is named like the authority figures (remaining/available). `covered` speaks only of holdings; `budget_remaining_atomic` is the PERMITTED figure the allowances read reports.","required":["covered","chain_id","token_address","token_symbol","checked_amount_atomic","budget_remaining_atomic"],"properties":{"covered":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true: the chain says the agent's account holds at least checked_amount_atomic of token. false: the chain read succeeded and reports LESS — tell the user the funds are missing rather than retrying. null: the chain read FAILED — unverifiable, never treated as absence; coverage_error carries why."},"coverage_error":{"type":"string","description":"Present only when covered is null: why the chain read could not answer."},"chain_id":{"type":"integer"},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"token_symbol":{"type":"string"},"checked_amount_atomic":{"type":"string","description":"The amount the coverage question was asked about, in ATOMIC units."},"budget_remaining_atomic":{"type":"string","description":"Context, AUTHORITY not holdings: the agent's remaining spend authority for the requested token, in ATOMIC units — the same derivation GET /machine-payments/allowances reports as onchain.remaining (#1090 derivation, #1145 enforcer read). Zero when no active budget row names the token. Compare it with covered, never instead of it."},"budget_remaining_is_from_chain":{"type":"boolean","description":"#1319 provenance, same semantics as the allowances read's flag: true when the budget figure came from a live ERC20PeriodTransferEnforcer read, false when it fell back to the configured budget. Absent when no budget row existed for the token (nothing was read)."},"budget_recipient_addresses":{"type":"array","items":{"type":"string"},"description":"#3518: present only when budget_remaining_atomic is \"0\" because the agent has no OPEN budget for the token but holds live merchant-locked budgets — their recipients, lowercase. Those budgets pay only these addresses; haven_get_allowances reports their remaining figures."}},"additionalProperties":false},"MachinePaymentReceipt":{"type":"object","required":["id","payment_id","rail","proof_status","tx_hash","chain_id","resource_url","token_symbol","token_address","amount_raw","amount_human","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"payment_id":{"type":"string","format":"uuid"},"payment_intent_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"approval_request_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Retained for wire compatibility; ALWAYS null. #2055 dropped `approval_requests`, so no receipt can be anchored to an approval any more."},"rail":{"type":"string"},"settlement_scheme":{"type":["string","null"],"description":"Which settlement branch ran (eip3009 | erc7710), from the intent (#946). Null on legacy-rail receipts."},"budget_delegation_hash":{"type":["string","null"],"description":"The metering budget delegation, uniform across schemes (#1059). Null on the legacy rail and on intents predating migration 053."},"proof_status":{"type":"string","enum":["payment_confirmed","merchant_response_observed","protocol_receipt_attached"]},"tx_hash":{"type":"string","description":"Deprecated (#2998) — kept for wire compatibility. Meaning depends on `settlement_scheme`: the account → delegate funding transaction on eip3009, the (only) settlement transaction on erc7710. Prefer `funding_tx_hash` / `settlement_tx_hash`, which name which is which."},"funding_tx_hash":{"type":["string","null"],"description":"The account → delegate funding transaction, relayed by Haven (#2998). Set on eip3009 and on scheme-less retired-x402-rail rows; null on erc7710 (no funding leg) and on scheme-less retired mpp-rail rows (one direct account → merchant transaction, which is the settlement)."},"settlement_tx_hash":{"type":["string","null"],"description":"The delegate → merchant settlement transaction (#2998). On erc7710 this is `tx_hash` itself (the one transaction). On eip3009 this is, first, a merchant settlement the agent reported and Haven verified on-chain (#3475); otherwise the merchant-reported `protocol_receipt_payload.transaction` when it is a non-zero 0x-prefixed 32-byte hash, else null — the merchant has not reported a settlement yet, or reported the zero-hash \"delivered, not settled\" marker. Trust level differs: on erc7710, and for the verified eip3009 report, Haven verified this hash on-chain; the eip3009 fallback is the merchant's claim as relayed (PAYMENT-RESPONSE), NOT verified on-chain by Haven. On scheme-less retired mpp-rail rows it is `tx_hash` itself."},"chain_id":{"type":"integer"},"resource_url":{"type":"string","format":"uri"},"merchant_address":{"anyOf":[{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},{"type":"null"}]},"payer_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The treasury account — same as `parties.treasury_account`; prefer `parties`."},"settlement_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"token_symbol":{"type":"string"},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"amount_raw":{"type":"string"},"amount_human":{"type":"string"},"challenge_id":{"type":["string","null"]},"idempotency_key":{"type":["string","null"]},"merchant_status":{"type":["integer","null"]},"delivery_quality":{"type":["string","null"],"enum":["ok","unusable","partial",null]},"delivery_note":{"type":["string","null"]},"delivery_reported_at":{"type":["string","null"],"format":"date-time"},"confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"parties":{"$ref":"#/components/schemas/Parties"},"scope":{"$ref":"#/components/schemas/ListScope"}},"additionalProperties":true},"MachinePaymentEvidenceRequest":{"type":"object","required":["paymentId","rail","txHash"],"properties":{"paymentId":{"type":"string","format":"uuid"},"rail":{"type":"string"},"txHash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"resourceUrl":{"type":"string"},"merchantStatus":{"type":"integer","minimum":100,"maximum":599},"challengePayload":{"type":"object","additionalProperties":true},"selectedPayment":{"type":"object","additionalProperties":true},"paymentProofHeaderName":{"type":"string"},"paymentProofHeader":{"type":"string"},"protocolReceiptHeaderName":{"type":"string"},"protocolReceiptHeader":{"type":"string"},"protocolReceiptPayload":{"type":"object","additionalProperties":true},"deliveryReference":{"type":"string","maxLength":512}},"additionalProperties":false},"MachinePaymentDeliveryQualityRequest":{"type":"object","required":["quality"],"properties":{"quality":{"type":"string","enum":["ok","unusable","partial"]},"note":{"type":"string"}},"additionalProperties":false},"MachinePaymentReconciliationEventRequest":{"type":"object","required":["paymentId","rail","eventType"],"properties":{"paymentId":{"type":"string","format":"uuid"},"rail":{"type":"string"},"eventType":{"type":"string","enum":["merchant_retry_rejected_after_payment"]},"txHash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"reason":{"type":"string"},"details":{"type":"object","additionalProperties":true}},"additionalProperties":false},"MachinePaymentReconciliationEventResponse":{"type":"object","required":["event_id","status","payment_id","rail","event_type","created_at"],"properties":{"event_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["open","resolved"]},"payment_id":{"type":"string","format":"uuid"},"rail":{"type":"string"},"event_type":{"type":"string"},"created_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"SweepAuthorization":{"type":"object","description":"EIP-3009 TransferWithAuthorization fields for a delegate → the agent's account (Haven wallet) USDC sweep.","required":["from","to","value","validAfter","validBefore","nonce","token","chainId"],"properties":{"from":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"value":{"type":"string","description":"Atomic USDC amount."},"validAfter":{"type":"string","description":"Unix seconds the authorization becomes valid."},"validBefore":{"type":"string","description":"Unix seconds the authorization expires."},"nonce":{"type":"string","description":"0x-prefixed 32-byte hex nonce.","pattern":"^0x[0-9a-fA-F]{64}$"},"token":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chainId":{"type":"integer","examples":[8453]}},"additionalProperties":false},"SweepExpectedAuth":{"type":"object","description":"Haven's binding over the sweep authorization context, verified by the edge signer.","required":["version","message","signature","signer"],"properties":{"version":{"type":"integer","enum":[1]},"message":{"type":"string"},"signature":{"type":"string"},"signer":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]}},"additionalProperties":false},"SweepPrepareResponse":{"type":"object","required":["authorization","expected_auth","asset","amount","amount_atomic","chain_id"],"properties":{"authorization":{"$ref":"#/components/schemas/SweepAuthorization"},"expected_auth":{"$ref":"#/components/schemas/SweepExpectedAuth"},"asset":{"type":"string","examples":["USDC"]},"amount":{"type":"string"},"amount_atomic":{"type":"string"},"chain_id":{"type":"integer","examples":[8453]},"sign_instructions":{"type":"string"}},"additionalProperties":false},"SweepSubmitRequest":{"type":"object","required":["authorization","signature"],"properties":{"authorization":{"$ref":"#/components/schemas/SweepAuthorization"},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$","description":"Delegate EIP-712 signature over the authorization."}},"additionalProperties":false},"SweepSubmitResponse":{"type":"object","required":["tx_hash","asset","amount","amount_atomic","from_address","to_address","chain_id","explorer_url"],"properties":{"tx_hash":{"type":"string"},"asset":{"type":"string","examples":["USDC"]},"amount":{"type":"string"},"amount_atomic":{"type":"string"},"from_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"to_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chain_id":{"type":"integer","examples":[8453]},"explorer_url":{"type":"string"},"idempotent_replay":{"type":"boolean"}},"additionalProperties":false},"TransactionBase":{"description":"Fields shared by every transaction representation. The per-account page items (`GET /transactions/{accountAddress}`) are exactly this shape; the aggregated feed adds account scope on top (`Transaction`). Flat (not `allOf`-composed with `Transaction`, #2885) so `additionalProperties: false` closes properly — see `transactionBaseProperties` above.","type":"object","required":["hash","type","from","to","value","valueFormatted","asset","decimals","direction","timestamp","blockNumber","isError"],"properties":{"hash":{"type":"string"},"type":{"type":"string","enum":["native","erc20","internal"]},"from":{"type":"string","description":"Counterparty address, or the empty string when the explorer reported none."},"to":{"type":"string","description":"Counterparty address, or the empty string when the explorer reported none."},"value":{"type":"string"},"valueFormatted":{"type":"string"},"asset":{"type":"string","description":"Token ticker where known; falls back to the raw contract address for unknown tokens."},"decimals":{"type":"integer"},"direction":{"type":"string","enum":["in","out"]},"timestamp":{"type":"integer"},"timestampSource":{"type":"string","enum":["block","confirmed_at","created_at"],"description":"#3132: which column produced `timestamp` — never a silent substitution. 'block' on explorer-derived rows; on an x402-synthesized row 'confirmed_at' when the intent carries one, else 'created_at' (the intent's creation time, NOT a settlement time). Read `confirmedAt` for the recorded confirmation time."},"confirmedAt":{"type":["string","null"],"format":"date-time","description":"#3132: the recorded confirmation time of an x402-synthesized row, null when the intent has none — the same nullable value `GET /receipts` reports as `confirmed_at`. Absent on explorer-derived rows."},"blockNumber":{"type":["integer","null"],"description":"On-chain block, or null when the row has none recorded. Null for x402-synthesized rows: they are built from a payment intent and no block number is stored (#3129). Was 0 for those rows until #3129 — a zero that meant \"unknown\" but read as block zero."},"isError":{"type":"boolean"},"tokenAddress":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"tokenSymbol":{"type":"string"},"source":{"type":"string","description":"Origin of the row. Known values: 'direct', 'x402', 'mpp_demo', 'mpp_crypto', 'spt', 'stripe_deposit'. Open set — new payment rails add values."},"x402ResourceUrl":{"type":["string","null"]},"x402MerchantAddress":{"type":["string","null"]},"paymentId":{"type":"string"},"paymentProofStatus":{"type":["string","null"]},"paymentFlowStatus":{"type":["string","null"],"enum":["paid","confirming_merchant","needs_attention",null]},"paymentAttentionReason":{"type":["string","null"],"enum":["merchant_retry_rejected_after_payment",null]},"activityType":{"type":"string","enum":["delegate_sweep"]},"agentName":{"type":"string"},"fundingTxHash":{"type":["string","null"],"description":"#3763: the EIP-3009 funding leg (same value as `hash`) — null on rows that are not synthesized x402 payments."},"settlementTxHash":{"type":["string","null"],"description":"#3763: the merchant settlement transaction when one is recorded (#3475); null otherwise — \"not recorded\", never \"failed\". Null on non-x402 rows."},"deliveryReference":{"type":["string","null"],"description":"#3778: the non-secret delivery pointer reported with an accepted x402 outcome (merchant, product, value, order id); null when none was reported. Never a redemption code or other credential — those are refused at write."},"initiatedBy":{"type":"string","enum":["agent","human","unknown"],"description":"Who initiated the transaction, recorded by the backend. `agent`: agent-attributed rows (confirmed x402 intents, delegate sweeps, raw transfers matched to a confirmed intent). `human`: reserved — nothing populates it today. `unknown`: outbound raw transfer with no matched intent. Absent for inbound (`direction: in`) rows."},"settlementScheme":{"type":["string","null"],"enum":["eip3009","erc7710",null],"description":"Which settlement branch actually moved the money: `erc7710` (direct settlement, account → merchant, no funding leg) or `eip3009` (funded transfer — the budget delegation funds the delegate EOA, which then signs the standard EIP-3009 header). This is the settlement SCHEME and is three-way distinct from its neighbours: `source` is the payment PROTOCOL (x402, mpp_crypto, …), and the account's `execution_rail` is the ACCOUNT ARCHITECTURE (delegation vs the legacy AllowanceModule). Do not collapse them. Null when no scheme was recorded — non-machine transfers, and legacy-rail rows, which are structurally EIP-3009 but never stamp the key. Null-in-null-out: nothing is inferred or backfilled."},"amountSek":{"type":["string","null"]},"fxRateSek":{"type":["string","null"]},"fxSource":{"type":["string","null"]},"convertedAmount":{"type":["string","null"]},"convertedCurrency":{"type":"string","enum":["SEK","USD","EUR"],"description":"The currency `convertedAmount` is denominated in — the user’s `currency_preference`, or SEK when none is set. SEK mirrors `amountSek`; USD/EUR are struck from the row’s book-time rate map (`machine_payment_evidence.fx_rates`, migration 082) and are null when no rate was captured there."},"convertedFxRate":{"type":["string","null"]},"approxAmount":{"type":["string","null"],"description":"#3824: the token amount at today’s rate in `approxCurrency`, for rows without book-time fiat. Null when no usable price exists — never \"0\"."},"approxCurrency":{"type":"string","enum":["SEK","USD","EUR"],"description":"#3824: the currency `approxAmount` is denominated in — the user’s `currency_preference`, or SEK when none is set."},"fxRates":{"type":["object","null"],"additionalProperties":{"type":"number"},"description":"Book-time token→currency rates frozen at settlement (`machine_payment_evidence.fx_rates`, migration 082), one per supported ledger currency with a usable quote. Null on rows settled before migration 082 and on rows with no evidence row."},"accounting":{"$ref":"#/components/schemas/TransactionAccounting"}}},"TransactionAccounting":{"description":"Accounting-feed state for one transaction (#2870), read from the sync ledger — no live provider call. Present on a row only when the feed is available to the account, the user has a provider connection, and the payment has a sync row; absent otherwise.","type":"object","required":["provider","status","externalRef","error"],"properties":{"provider":{"type":"string","description":"Ledger provider key, e.g. 'fortnox'.","examples":["fortnox"]},"status":{"type":"string","enum":["pending","pushed","failed","skipped"]},"externalRef":{"type":["string","null"],"description":"Provider-side reference once pushed ('fortnox:supplierinvoice:<n>'); null otherwise."},"error":{"type":["string","null"],"description":"Failure or skip reason; on a pushed row, a non-fatal note (#498). Null when clean."}},"additionalProperties":false},"Transaction":{"description":"Aggregated-feed transaction (`GET /transactions`): the shared base plus Safe/account scope. Flat, not `allOf`-composed (#2885) — see `transactionBaseProperties` above for why.","type":"object","required":["hash","type","from","to","value","valueFormatted","asset","decimals","direction","timestamp","blockNumber","isError","chainId","accountId","accountAddress","accountName"],"properties":{"hash":{"type":"string"},"type":{"type":"string","enum":["native","erc20","internal"]},"from":{"type":"string","description":"Counterparty address, or the empty string when the explorer reported none."},"to":{"type":"string","description":"Counterparty address, or the empty string when the explorer reported none."},"value":{"type":"string"},"valueFormatted":{"type":"string"},"asset":{"type":"string","description":"Token ticker where known; falls back to the raw contract address for unknown tokens."},"decimals":{"type":"integer"},"direction":{"type":"string","enum":["in","out"]},"timestamp":{"type":"integer"},"timestampSource":{"type":"string","enum":["block","confirmed_at","created_at"],"description":"#3132: which column produced `timestamp` — never a silent substitution. 'block' on explorer-derived rows; on an x402-synthesized row 'confirmed_at' when the intent carries one, else 'created_at' (the intent's creation time, NOT a settlement time). Read `confirmedAt` for the recorded confirmation time."},"confirmedAt":{"type":["string","null"],"format":"date-time","description":"#3132: the recorded confirmation time of an x402-synthesized row, null when the intent has none — the same nullable value `GET /receipts` reports as `confirmed_at`. Absent on explorer-derived rows."},"blockNumber":{"type":["integer","null"],"description":"On-chain block, or null when the row has none recorded. Null for x402-synthesized rows: they are built from a payment intent and no block number is stored (#3129). Was 0 for those rows until #3129 — a zero that meant \"unknown\" but read as block zero."},"isError":{"type":"boolean"},"tokenAddress":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"tokenSymbol":{"type":"string"},"source":{"type":"string","description":"Origin of the row. Known values: 'direct', 'x402', 'mpp_demo', 'mpp_crypto', 'spt', 'stripe_deposit'. Open set — new payment rails add values."},"x402ResourceUrl":{"type":["string","null"]},"x402MerchantAddress":{"type":["string","null"]},"paymentId":{"type":"string"},"paymentProofStatus":{"type":["string","null"]},"paymentFlowStatus":{"type":["string","null"],"enum":["paid","confirming_merchant","needs_attention",null]},"paymentAttentionReason":{"type":["string","null"],"enum":["merchant_retry_rejected_after_payment",null]},"activityType":{"type":"string","enum":["delegate_sweep"]},"agentName":{"type":"string"},"fundingTxHash":{"type":["string","null"],"description":"#3763: the EIP-3009 funding leg (same value as `hash`) — null on rows that are not synthesized x402 payments."},"settlementTxHash":{"type":["string","null"],"description":"#3763: the merchant settlement transaction when one is recorded (#3475); null otherwise — \"not recorded\", never \"failed\". Null on non-x402 rows."},"deliveryReference":{"type":["string","null"],"description":"#3778: the non-secret delivery pointer reported with an accepted x402 outcome (merchant, product, value, order id); null when none was reported. Never a redemption code or other credential — those are refused at write."},"initiatedBy":{"type":"string","enum":["agent","human","unknown"],"description":"Who initiated the transaction, recorded by the backend. `agent`: agent-attributed rows (confirmed x402 intents, delegate sweeps, raw transfers matched to a confirmed intent). `human`: reserved — nothing populates it today. `unknown`: outbound raw transfer with no matched intent. Absent for inbound (`direction: in`) rows."},"settlementScheme":{"type":["string","null"],"enum":["eip3009","erc7710",null],"description":"Which settlement branch actually moved the money: `erc7710` (direct settlement, account → merchant, no funding leg) or `eip3009` (funded transfer — the budget delegation funds the delegate EOA, which then signs the standard EIP-3009 header). This is the settlement SCHEME and is three-way distinct from its neighbours: `source` is the payment PROTOCOL (x402, mpp_crypto, …), and the account's `execution_rail` is the ACCOUNT ARCHITECTURE (delegation vs the legacy AllowanceModule). Do not collapse them. Null when no scheme was recorded — non-machine transfers, and legacy-rail rows, which are structurally EIP-3009 but never stamp the key. Null-in-null-out: nothing is inferred or backfilled."},"amountSek":{"type":["string","null"]},"fxRateSek":{"type":["string","null"]},"fxSource":{"type":["string","null"]},"convertedAmount":{"type":["string","null"]},"convertedCurrency":{"type":"string","enum":["SEK","USD","EUR"],"description":"The currency `convertedAmount` is denominated in — the user’s `currency_preference`, or SEK when none is set. SEK mirrors `amountSek`; USD/EUR are struck from the row’s book-time rate map (`machine_payment_evidence.fx_rates`, migration 082) and are null when no rate was captured there."},"convertedFxRate":{"type":["string","null"]},"approxAmount":{"type":["string","null"],"description":"#3824: the token amount at today’s rate in `approxCurrency`, for rows without book-time fiat. Null when no usable price exists — never \"0\"."},"approxCurrency":{"type":"string","enum":["SEK","USD","EUR"],"description":"#3824: the currency `approxAmount` is denominated in — the user’s `currency_preference`, or SEK when none is set."},"fxRates":{"type":["object","null"],"additionalProperties":{"type":"number"},"description":"Book-time token→currency rates frozen at settlement (`machine_payment_evidence.fx_rates`, migration 082), one per supported ledger currency with a usable quote. Null on rows settled before migration 082 and on rows with no evidence row."},"accounting":{"$ref":"#/components/schemas/TransactionAccounting"},"chainId":{"type":"integer"},"accountId":{"type":"string","format":"uuid"},"accountAddress":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"accountName":{"type":"string"},"agentId":{"type":"string","format":"uuid"},"scope":{"$ref":"#/components/schemas/ListScope"}}},"ListScope":{"description":"#3132 (owner decision 3 on #3130): what population a list row came from and what narrowed it, as two values — one value cannot say both. `source: 'wallet'` is the aggregated feed (every account's explorer window plus synthesized confirmed intents; sweeps and funding legs included); `'agent'` is the receipts view (this agent's evidence rows only). `filter` names the IDENTITY-axis narrowing applied on top (whose money / which wallet: agent, account, both), or null; token, direction and chain narrowing are deliberately not named here. `agentId=user` counts as agent-axis narrowing and selects outbound rows with NO agent attribution. `agentId` on the wallet feed NARROWS a wallet-scoped query; it does not make it the receipts view.","type":"object","required":["source","filter"],"properties":{"source":{"type":"string","enum":["wallet","agent"]},"filter":{"type":["string","null"],"enum":["agent","account","account+agent",null]}},"additionalProperties":false},"TransactionsPageResponse":{"description":"Per-account paginated transaction list (`GET /transactions/{accountAddress}`). Items carry no account scope — the account is the path parameter.","type":"object","required":["transactions","total","page","limit","pages"],"properties":{"transactions":{"type":"array","items":{"$ref":"#/components/schemas/TransactionBase"}},"total":{"type":"integer"},"page":{"type":"integer","minimum":1},"limit":{"type":"integer"},"pages":{"type":"integer","description":"0 when total is 0."}},"additionalProperties":false},"BalanceItem":{"type":"object","required":["symbol","address","balance","formatted","decimals"],"properties":{"symbol":{"type":"string"},"address":{"type":["string","null"],"description":"Token contract address; null for the chain-native token (exactly one entry)."},"balance":{"type":"string","description":"Raw base units. On a failed read, the last successfully read balance is served instead, marked by balanceFreshness; '0' only when no balance has ever been read for this token."},"formatted":{"type":"string"},"decimals":{"type":"integer"},"balanceFreshness":{"$ref":"#/components/schemas/BalanceFreshness"}},"additionalProperties":false},"BalanceFreshness":{"type":"object","description":"Present only when this entry's balance read FAILED (#3295). Absent on a clean read. The balance string it marks is still additive: the last-known value when one exists (status stale), else the filler '0' (status unavailable).","oneOf":[{"type":"object","required":["status","asOf"],"properties":{"status":{"type":"string","enum":["stale"]},"asOf":{"type":"string","format":"date-time","description":"When the served value was last successfully read from the chain."}},"additionalProperties":false},{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["unavailable"]}},"additionalProperties":false}]},"BalancesResponse":{"type":"object","required":["balances"],"properties":{"balances":{"type":"array","items":{"$ref":"#/components/schemas/BalanceItem"},"description":"Native token first, then ERC-20s in registry order. Never empty."}},"additionalProperties":false},"FundingToken":{"type":"object","required":["symbol","address","decimals","balance_human","minimum_useful_human"],"properties":{"symbol":{"type":"string"},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The token contract. (A funding token is always an ERC-20 — the chain-native asset is reported under `native`, not here.)"},"decimals":{"type":"integer"},"balance_human":{"type":"string","description":"Human-decimal balance via formatTokenValue — '0' or <int>.<2–6 fraction digits>. On a failed read (#3317), the last successfully read balance is served instead, marked by balanceFreshness; '0' only when no balance has ever been read for this token."},"minimum_useful_human":{"type":["string","null"],"description":"Documented per-token constant from @haven_ai/core — the smallest amount worth moving for this token (one small x402 payment plus headroom). A CONSTANT, not a policy: not a spend limit, not a minimum balance check. null when no constant is documented for the symbol; then `funded` ignores the token."},"balanceFreshness":{"$ref":"#/components/schemas/BalanceFreshness"}},"additionalProperties":false},"FundingResponse":{"type":"object","required":["account_address","chain","tokens","native","funded"],"properties":{"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Where the human sends funds — the linked Safe address."},"chain":{"type":"object","required":["id","name","explorer_url"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"explorer_url":{"type":"string"}},"additionalProperties":false},"tokens":{"type":"array","items":{"$ref":"#/components/schemas/FundingToken"},"description":"The chain's ERC-20s in registry order (native excluded — see `native`)."},"native":{"type":"object","required":["symbol","balance_human","needed"],"properties":{"symbol":{"type":"string"},"balance_human":{"type":"string","description":"On a failed read (#3317), the last successfully read native balance is served instead, marked by balanceFreshness; '0' only when no balance has ever been read."},"needed":{"type":"boolean","description":"Always false: gas is relay-sponsored (UserOps), so the funding instruction never asks for ETH."},"balanceFreshness":{"$ref":"#/components/schemas/BalanceFreshness"}},"additionalProperties":false},"faucet_url":{"type":"string","description":"Present ONLY on testnets, from the chain registry — where a HUMAN gets dev funds. Haven never calls a faucet. Absent (not null) on mainnets."},"funded":{"type":"boolean","description":"True when ANY token's KNOWN balance ≥ its `minimum_useful_human` constant — a fresh read, or the stale last-known balance a failed read serves (#3317). A failed read with no last-known value can never make this true (or false): unknown is not unfunded. Native gas is not part of the question."},"balanceFreshness":{"$ref":"#/components/schemas/BalanceFreshness"}},"additionalProperties":false},"PortfolioBreakdown":{"type":"object","required":["symbol","balance","formatted","usdValue","eurValue"],"properties":{"symbol":{"type":"string"},"balance":{"type":"string","description":"Raw base units. On a failed read, the last successfully read balance is served instead, marked by balanceFreshness; '0' only when no balance has ever been read for this token."},"formatted":{"type":"string"},"usdValue":{"type":"number","description":"When the price feed fails, valued at the last good price this server instance has seen for the token; 0 only if it has none (#3297)."},"eurValue":{"type":"number"},"sekValue":{"type":"number","description":"Same one price read as usd/eur (#3127 round 2). When the price feed fails, the last good price this server instance has seen; 0 only if it has none (#3297)."},"balanceFreshness":{"$ref":"#/components/schemas/BalanceFreshness"}},"additionalProperties":false},"PortfolioResponse":{"type":"object","required":["totalUsd","totalEur","breakdown"],"properties":{"totalUsd":{"type":"number"},"totalEur":{"type":"number"},"totalSek":{"type":"number"},"breakdown":{"type":"array","items":{"$ref":"#/components/schemas/PortfolioBreakdown"}}},"additionalProperties":false},"TransactionFilterOptionsResponse":{"type":"object","required":["accounts","agents","tokens"],"properties":{"accounts":{"type":"array","items":{"type":"object","required":["id","name","address","chainId"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"chainId":{"type":"integer"}},"additionalProperties":false}},"agents":{"type":"array","description":"ALL agents including revoked — unlike the dashboard preview.","items":{"type":"object","required":["id","name","status"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","pending_approval","revoked"]}},"additionalProperties":false}},"tokens":{"type":"array","items":{"type":"object","required":["key","symbol","address","chainId","isNative"],"properties":{"key":{"type":"string","description":"'<chainId>:native' or '<chainId>:<lowercased address>'."},"symbol":{"type":"string"},"address":{"type":["string","null"],"description":"null iff isNative."},"chainId":{"type":"integer"},"isNative":{"type":"boolean"}},"additionalProperties":false}}},"additionalProperties":false},"DashboardAgentAllowance":{"type":"object","required":["tokenSymbol","allowanceAmount"],"properties":{"tokenSymbol":{"type":"string"},"allowanceAmount":{"type":"string","pattern":"^(0|[0-9]+\\.[0-9]{2,6})$","description":"HUMAN-DECIMAL token amount — whole token units, NOT the atomic integer (25 USDC is \"25.00\", a zero budget is \"0\"). Projected from the agent's active delegation by rails/delegation-budget-view.ts via formatTokenValue(budget_atomic, decimals), whose output is always \"0\" or <integer>.<2–6 fraction digits> — so this pattern REJECTS an atomic value such as \"500\" (#2408). \"0\" is the one value both shapes share. Do not BigInt() this value: it is the shape that made #2283 a production bug. To compare it against an atomic price, scale it by the token's decimals first (#2295)."}},"additionalProperties":false},"DashboardAgentPreview":{"type":"object","required":["id","name","status","accountId","accountName","accountChainId","allowances","budgets","receivedSubBudgets","stats"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","pending_approval"],"description":"Revoked agents are excluded from the overview query. #3803: pending_approval joins — the redesigned overview must surface an agent still waiting for its connection."},"accountId":{"type":["string","null"],"format":"uuid"},"accountName":{"type":["string","null"]},"accountChainId":{"type":["integer","null"]},"allowances":{"type":"array","items":{"$ref":"#/components/schemas/DashboardAgentAllowance"},"deprecated":true},"setupStatus":{"type":"string","description":"Present only when status is 'pending_approval': the agent's newest connection setup status (e.g. 'awaiting_connection', 'expired', 'failed')."},"budgets":{"type":"array","items":{"$ref":"#/components/schemas/DashboardAgentBudget"},"description":"#3803: every live budget from the active delegation set — expired rows follow the #3802 predicate and are absent."},"receivedSubBudgets":{"type":"array","items":{"$ref":"#/components/schemas/DashboardReceivedSubBudget"},"description":"#3803: sub-budget grants this agent RECEIVED, so an agent whose only authority is a received sub-budget does not read \"No budget\"."},"stats":{"$ref":"#/components/schemas/DashboardAgentStats"}},"additionalProperties":false},"DashboardAgentBudget":{"type":"object","description":"#3803 — one delegation-rail budget with its identity and live window, for the caption the client renders in the user currency (see spotRates). budgetAtomic is the atomic integer; never parse the human `allowances[].allowanceAmount` for arithmetic.","required":["id","delegationHash","chainId","tokenAddress","tokenSymbol","decimals","budgetAtomic","periodSeconds","startDate","expiresAt","periodEnd"],"properties":{"id":{"type":"string","format":"uuid"},"delegationHash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"chainId":{"type":"integer"},"tokenAddress":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"tokenSymbol":{"type":"string"},"decimals":{"type":"integer"},"budgetAtomic":{"type":"string","description":"Base-10 atomic integer (VARCHAR(78) on the wire source)."},"periodSeconds":{"type":"integer"},"startDate":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"periodEnd":{"type":"string","format":"date-time","description":"End of the CURRENT budget period, computed from startDate + periodSeconds at read time."}},"additionalProperties":false},"DashboardReceivedSubBudget":{"type":"object","description":"#3803 — a sub-budget grant the agent received from a parent agent.","required":["parentAgentId","parentAgentName","open"],"properties":{"parentAgentId":{"type":"string","format":"uuid"},"parentAgentName":{"type":"string"},"open":{"type":"boolean","description":"true iff the grant's stored status is 'open'."}},"additionalProperties":false},"DashboardSpendWindow":{"type":"object","description":"#3803 — one window of the 7/30-day block. gross is the booked (or re-priced) fiat; net is gross with the #3755 delegate_sweeps ratio netting applied. Both are USD/EUR/SEK triples.","required":["gross","net","approx","payments","distinctMerchants","budgetStops"],"properties":{"gross":{"type":"object","required":["usd","eur","sek"],"properties":{"usd":{"type":"number"},"eur":{"type":"number"},"sek":{"type":"number"}},"additionalProperties":false},"net":{"type":"object","required":["usd","eur","sek"],"properties":{"usd":{"type":"number"},"eur":{"type":"number"},"sek":{"type":"number"}},"additionalProperties":false},"approx":{"type":"boolean","description":"Owner decision 1: true when any row's booked fiat was NULL and was priced at today's rate — the client marks the figure ≈ so a re-priced total is never shown as booked."},"payments":{"type":"integer","description":"Confirmed payment rows in the window — activity, never netted."},"distinctMerchants":{"type":"integer","description":"DISTINCT merchants over the window — merchant = the x402 resource host, else the recipient address. A user-level figure, never a sum of per-agent counts."},"budgetStops":{"type":"integer","description":"Refusal rows whose reason was delegation_budget_exceeded or delegation_expired — a budget stopped a payment."}},"additionalProperties":false},"DashboardAgentSpendWindow":{"type":"object","description":"#3803 — one window of a PER-AGENT stats block. Same gross/net/approx/payments definitions as the user-level DashboardSpendWindow; per-agent refusals instead of the user-level distinctMerchants/budgetStops rollups.","required":["gross","net","approx","payments","refusals"],"properties":{"gross":{"type":"object","required":["usd","eur","sek"],"properties":{"usd":{"type":"number"},"eur":{"type":"number"},"sek":{"type":"number"}},"additionalProperties":false},"net":{"type":"object","required":["usd","eur","sek"],"properties":{"usd":{"type":"number"},"eur":{"type":"number"},"sek":{"type":"number"}},"additionalProperties":false},"approx":{"type":"boolean","description":"Owner decision 1: true when any row's booked fiat was NULL and was priced at today's rate."},"payments":{"type":"integer","description":"Confirmed payment rows in the window — activity, never netted."},"refusals":{"$ref":"#/components/schemas/DashboardRefusalBuckets"}},"additionalProperties":false},"DashboardRefusalBuckets":{"type":"object","description":"#3803 — refusal ROW counts per bucket (not distinct reasons — #3815). relayer_budget is Haven’s own cap and is never shown as the user’s budget.","required":["budget","scope","failed","haven"],"properties":{"budget":{"type":"integer","description":"delegation_budget_exceeded + delegation_expired."},"scope":{"type":"integer","description":"no_delegation_for_target."},"failed":{"type":"integer","description":"onchain_revert."},"haven":{"type":"integer","description":"relayer_budget."}},"additionalProperties":false},"DashboardAgentStats":{"type":"object","description":"#3803 — per-agent 7/30-day figures. The spend windows carry the same definition as the user-level block; refusals are row counts per bucket over the same window.","required":["d7","d30","lastPaymentAt","lastCounterparty"],"properties":{"d7":{"$ref":"#/components/schemas/DashboardAgentSpendWindow"},"d30":{"$ref":"#/components/schemas/DashboardAgentSpendWindow"},"lastPaymentAt":{"type":["string","null"],"format":"date-time","description":"ALL-TIME latest confirmed payment (not windowed); null when the agent never paid."},"lastCounterparty":{"type":["object","null"],"required":["source","x402ResourceUrl","to","merchantName"],"properties":{"source":{"type":["string","null"]},"x402ResourceUrl":{"type":["string","null"]},"to":{"type":["string","null"]},"merchantName":{"type":["string","null"]}},"additionalProperties":false}},"additionalProperties":false},"DashboardAccountOverview":{"type":"object","description":"#3803 — per-account USDC, funding and pace. No new chain reads: the balance comes from the portfolio the overview already fetches.","required":["accountId","chainId","isTestnet","usdcBalanceAtomic","usdcDecimals","funded","needs_backup_recommendation","usdcPace7dAtomic"],"properties":{"accountId":{"type":"string","format":"uuid"},"chainId":{"type":"integer"},"isTestnet":{"type":"boolean","description":"The chain registry’s own testnet marker (faucet presence)."},"usdcBalanceAtomic":{"type":["string","null"],"description":"The chain-registry USDC balance in base units. NULL when the balance read is unavailable — unknown, never 0."},"usdcDecimals":{"type":["integer","null"],"description":"The registry USDC decimals; null when the chain has no registry USDC."},"usdcBalanceFreshness":{"$ref":"#/components/schemas/BalanceFreshness","description":"Present only when the USDC balance read was stale or unavailable (#3295 marker)."},"funded":{"type":["boolean","null"],"description":"The account's own USDC > 0. Null when the balance is unknown — never derived from a 0."},"needs_backup_recommendation":{"type":"boolean","description":"Per account (the session payload answered only accounts[0] before #3803)."},"usdcPace7dAtomic":{"type":"string","description":"Σ confirmed − min(swept, confirmed) per (agent, token, chain) over the last 7 days, for this account’s agents on this chain in the registry USDC. Swept = delegate_sweeps status submitted with a tx_hash."}},"additionalProperties":false},"DashboardOverviewResponse":{"type":"object","required":["totals","change","actionableApprovals","pendingApprovals","onboardingProgress","agents","agentCount","accounts","spotRates","spend"],"properties":{"totals":{"type":"object","required":["usd","eur"],"properties":{"usd":{"type":"number"},"eur":{"type":"number"},"sek":{"type":"number"}},"additionalProperties":false},"change":{"type":"object","required":["available","usdAmount","eurAmount","usdPercent","eurPercent"],"properties":{"available":{"type":"boolean","description":"true iff a yesterday snapshot existed to diff against."},"usdAmount":{"type":["number","null"],"description":"Null when balanceFreshness below is unavailable: the totals are understated by an unknown amount, so no swing may be claimed."},"eurAmount":{"type":["number","null"],"description":"Null when balanceFreshness below is unavailable, as usdAmount."},"sekAmount":{"type":["number","null"],"description":"Null when yesterday’s snapshot predates migration 090 (no SEK baseline stored) or when balanceFreshness below is unavailable — the client reports the change as unavailable rather than reading a fabricated swing."},"usdPercent":{"type":"number","description":"0 when unavailable or the previous total was 0."},"eurPercent":{"type":"number"},"sekPercent":{"type":"number","description":"0 when the SEK baseline is missing or the previous total was 0."},"balancesFreshness":{"$ref":"#/components/schemas/BalanceFreshness","description":"Present only when at least one of the totals' balance reads failed (#3295): stale with the oldest served as-of time, or unavailable when some token has no known value. Absent on a clean read."}},"additionalProperties":false},"actionableApprovals":{"type":"integer","description":"Always 0 since #2055 — the approval queue died with the Safe rail and its table is dropped; the field survives for wire compatibility."},"pendingApprovals":{"type":"integer","description":"Duplicate of actionableApprovals; always 0 since #2055, kept for compatibility."},"onboardingProgress":{"type":"object","required":["hasFirstAgentPayment"],"properties":{"hasFirstAgentPayment":{"type":"boolean"}},"additionalProperties":false},"agents":{"type":"array","items":{"$ref":"#/components/schemas/DashboardAgentPreview"},"description":"#3803: EVERY delegation-rail agent in active/paused/pending_approval — the former at-most-6 cap moved to the client (#3809)."},"activity":{"type":"array","maxItems":8,"items":{"$ref":"#/components/schemas/DashboardActivityGroup"},"description":"Grouped activity rows, newest first. Every count is a FLOOR (`countIsFloor`) when the explorer window was truncated."},"agentCount":{"type":"object","description":"#3803 — the overview’s agents[] by status, so tiles do not derive counts from a client-side slice.","required":["active","paused","pending_approval"],"properties":{"active":{"type":"integer"},"paused":{"type":"integer"},"pending_approval":{"type":"integer"}},"additionalProperties":false},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/DashboardAccountOverview"},"description":"#3803 — one entry per delegation-rail account, in the accounts list order."},"spotRates":{"type":"object","description":"#3803 — today’s rate per BUDGET token symbol in the user’s preferred currency (see the transaction feed’s resolution); null when the price read failed or quoted zero. The client has no rate of its own, so the budget captions are struck here.","additionalProperties":{"type":["number","null"]}},"spend":{"type":"object","description":"#3803 — the 7/30-day block. Windows are [now − N×24h, now) on confirmed_at — the same definition as /analytics/overview. Rows are confirmed, delegation-rail agents only; revoked agents’ in-window spend is included. Not counted: fees, deposits, other tokens.","required":["scope","d7","d30","topMerchant7d","failedIntents7d","balance_by_day"],"properties":{"scope":{"type":"string","enum":["mainnet","testnet"],"description":"Owner decision 3: mainnet accounts only whenever the user has one; a testnet-only user sees test totals labelled 'Test network'."},"d7":{"$ref":"#/components/schemas/DashboardSpendWindow"},"d30":{"$ref":"#/components/schemas/DashboardSpendWindow"},"topMerchant7d":{"type":["object","null"],"description":"The 7-day top merchant by USD booked sum, with RAW identity fields; null on a no-spend window.","required":["key","x402ResourceUrl","to","merchantName"],"properties":{"key":{"type":"string","description":"The merchant identity: the x402 resource host, else the recipient address."},"x402ResourceUrl":{"type":["string","null"],"description":"One representative x402 resource URL of this merchant’s rows."},"to":{"type":["string","null"],"description":"One representative recipient address."},"merchantName":{"type":["string","null"],"description":"Contact name, else receipt name, for the merchant address; null when neither exists."}},"additionalProperties":false},"failedIntents7d":{"type":"integer","description":"Payment intents with status failed in the 7-day window."},"balance_by_day":{"type":"array","description":"30 days of portfolio totals, shared with analytics’ BALANCE_BY_DAY_SQL. Days with no snapshot are ABSENT, not zero; totalSek is null on pre-migration-090 days.","items":{"type":"object","required":["snapshotDate","totalUsd","totalEur","totalSek"],"properties":{"snapshotDate":{"type":"string","description":"YYYY-MM-DD (UTC)."},"totalUsd":{"type":"number"},"totalEur":{"type":"number"},"totalSek":{"type":["number","null"]}},"additionalProperties":false}}},"additionalProperties":false}},"additionalProperties":false},"DashboardActivityGroup":{"type":"object","required":["count","sumAtomic","tokenSymbol","decimals","latestAt","agentId","agentName","source","x402ResourceUrl","to","merchantName","activityType","direction","status"],"properties":{"count":{"type":"integer","description":"Rows in the group. A FLOOR, not a total, when `countIsFloor` is true — the client renders \"×40+\"."},"countIsFloor":{"type":"boolean","description":"Present ONLY when the explorer window that produced the feed was truncated: the count is a floor over an unknown total. A count is never presented as exact when it is not."},"sumAtomic":{"type":"string","description":"Sum of the member rows' atomic `value` strings. One token per group, so the units agree."},"tokenSymbol":{"type":"string","description":"Ticker of the group’s token."},"decimals":{"type":"integer","description":"Decimals of the group’s token."},"latestAt":{"type":"string","format":"date-time","description":"ISO 8601 instant of the newest member row."},"agentId":{"type":["string","null"],"description":"Agent attribution of the newest member; null when the row has no agent (deposits, unattributed transfers)."},"agentName":{"type":["string","null"],"description":"Agent name of the newest member."},"source":{"type":["string","null"],"description":"The newest member's source, e.g. `x402`."},"x402ResourceUrl":{"type":["string","null"],"description":"The newest member’s x402 resource URL, when it is an x402 payment."},"to":{"type":"string","description":"The newest member’s raw `to` field — the merchant address on outbound payments, the account’s own address on deposits."},"merchantName":{"type":["string","null"],"description":"Contact or receipt-merchant name resolved for the counterparty, when one exists; null otherwise (always null on deposits)."},"activityType":{"type":["string","null"],"description":"The activity class, e.g. `delegate_sweep`; null for plain transfers and x402 payments."},"direction":{"type":"string","enum":["in","out"]},"status":{"type":"string","enum":["confirmed","pending","failed"],"description":"The DERIVED outcome: isError → failed; a paymentFlowStatus of confirming_merchant/needs_attention or a non-terminal paymentProofStatus → pending; else confirmed."},"convertedAmount":{"type":"string","description":"The book-time fiat sum — present ONLY when every member row carries a stored `convertedAmount` capture."},"convertedCurrency":{"type":"string","enum":["SEK","USD","EUR"],"description":"The currency `convertedAmount` is denominated in (the user’s preference)."},"approxAmount":{"type":["string","null"],"description":"The serve-time fiat sum at today’s rate — present INSTEAD of convertedAmount when any member lacked book-time fiat. Null when no usable price exists; never 0."},"approxCurrency":{"type":"string","enum":["SEK","USD","EUR"],"description":"The currency `approxAmount` is denominated in (the user’s preference)."}},"additionalProperties":false},"DashboardBudgetRemainingEntry":{"type":"object","required":["agent_id","chain_id","delegation_hash","token_address","token_symbol","token_decimals","budget_atomic","read_at","period_end","remaining_atomic","remaining_from_chain","used_atomic","sub_budget_spend"],"properties":{"agent_id":{"type":"string","format":"uuid"},"chain_id":{"type":"integer"},"delegation_hash":{"type":"string"},"token_address":{"type":"string"},"token_symbol":{"type":"string"},"token_decimals":{"type":"integer"},"budget_atomic":{"type":"string"},"read_at":{"type":["string","null"],"format":"date-time","description":"Null when the read is unknown."},"period_end":{"type":"string","format":"date-time"},"remaining_atomic":{"type":["string","null"],"description":"Null when the read is unknown — never \"0\", never the full budget."},"remaining_from_chain":{"type":"boolean"},"used_atomic":{"type":["string","null"],"description":"Null when the read is unknown."},"sub_budget_spend":{"type":"array","items":{"type":"object","required":["agent_id","spent_atomic"],"properties":{"agent_id":{"type":"string","format":"uuid"},"spent_atomic":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"DashboardBudgetRemainingResponse":{"type":"object","required":["budgets"],"properties":{"budgets":{"type":"array","items":{"$ref":"#/components/schemas/DashboardBudgetRemainingEntry"}}},"additionalProperties":false},"AgentRekeyResidual":{"type":"object","description":"Reported whether or not it is recoverable — nothing about a stranded residual fails quietly.","required":["atomic","recoverable_after_rekey"],"properties":{"atomic":{"type":"string"},"token_address":{"type":"string","nullable":true},"disposition":{"type":"string","nullable":true},"recoverable_after_rekey":{"type":"boolean"},"note":{"type":"string"}}},"AgentRekeyRevokePrepare":{"oneOf":[{"type":"object","description":"The treasury EOA owner signs signing_payload (EIP-712).","required":["signature_scheme","signing_payload","user_operation","treasury_address","delegation_hashes","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["eip712_userop"]},"signing_payload":{"type":"object","additionalProperties":true},"user_operation":{"type":"object","additionalProperties":true},"treasury_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegation_hashes":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}},"instructions":{"type":"string"}}},{"type":"object","description":"An account passkey signs user_op_hash via WebAuthn.","required":["signature_scheme","user_op_hash","user_operation","treasury_address","delegation_hashes","instructions"],"properties":{"signature_scheme":{"type":"string","enum":["webauthn_userop"]},"user_op_hash":{"type":"string"},"user_operation":{"type":"object","additionalProperties":true},"treasury_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegation_hashes":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}},"instructions":{"type":"string"}}},{"type":"object","description":"Nothing to revoke on-chain — an agent that never held a budget, one already revoked, or one whose previous re-key was abandoned after its revoke landed (#1868). No signature is needed and the re-key advances straight to the metered stage. When an abandoned predecessor froze a carry after its own on-chain revoke and no grant has been made since, that measurement is INHERITED rather than forfeited: carry holds the frozen entries, tx_hash is the predecessor’s revoke transaction, and carry_inherited_from_rekey_id names the abandoned re-key. Otherwise the carry is empty.","required":["revoked","stage","agent_has_no_authority","next_step"],"properties":{"revoked":{"type":"boolean","enum":[true]},"tx_hash":{"type":"string","nullable":true,"description":"Null on the empty walk; the ABANDONED predecessor’s revoke transaction when its carry was inherited."},"delegation_hashes":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}},"stage":{"type":"string"},"carry":{"type":"array","description":"Empty on the plain short-circuit; the inherited frozen measurement when an abandoned predecessor’s carry was adopted (#1868).","items":{"type":"object","required":["delegation_hash","remaining_atomic","from_chain"],"properties":{"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"remaining_atomic":{"type":"string"},"from_chain":{"type":"boolean"}}}},"carry_inherited_from_rekey_id":{"type":"string","format":"uuid","description":"Present only when the carry was inherited: the abandoned re-key whose frozen measurement this re-key adopted (#1868)."},"agent_has_no_authority":{"type":"boolean"},"next_step":{"type":"string"}}}]},"AgentRekeyPreflight":{"type":"object","required":["rekey_id","stage","old_delegate_address","new_delegate_address","residual","delegations_to_revoke","next_step"],"properties":{"rekey_id":{"type":"string","format":"uuid"},"stage":{"type":"string","enum":["preflight"]},"old_delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"new_delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"residual":{"$ref":"#/components/schemas/AgentRekeyResidual"},"delegations_to_revoke":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."}},"next_step":{"type":"string"},"ordering_note":{"type":"string"}}},"AgentRekeyIssuedDelegation":{"type":"object","required":["delegation_hash","carry_role","token_address","budget_atomic","signing_payload"],"properties":{"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"carry_role":{"type":"string","enum":["carry","steady","reanchor"]},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"recipient_address":{"type":"string","nullable":true},"budget_atomic":{"type":"string"},"period_seconds":{"type":"integer"},"start_date":{"type":"integer"},"expires_at":{"type":"integer"},"signing_payload":{"type":"object","additionalProperties":true}}},"AgentRekeyIssueResponse":{"type":"object","required":["stage","delegate_account_address","delegations"],"properties":{"stage":{"type":"string","enum":["issued"]},"delegate_account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"delegations":{"type":"array","items":{"$ref":"#/components/schemas/AgentRekeyIssuedDelegation"}},"skipped":{"type":"array","items":{"type":"object","properties":{"delegation_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The delegation's stable identity (#827) — keccak of the unsigned delegation."},"reason":{"type":"string"}}}},"carry_note":{"type":"string"},"next_step":{"type":"string"}}},"AgentRekeyCompleteResponse":{"type":"object","required":["completed","stage","agent_id","new_delegate_address","api_key","api_key_prefix","old_api_key_revoked"],"properties":{"completed":{"type":"boolean"},"stage":{"type":"string","enum":["completed"]},"agent_id":{"type":"string","format":"uuid"},"new_delegate_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"api_key":{"type":"string","description":"Shown ONCE. Never stored in plaintext and never logged."},"api_key_prefix":{"type":"string"},"old_api_key_revoked":{"type":"boolean"},"invalidated_intents":{"type":"integer"},"superseded_delegations":{"type":"integer"},"residual_on_old_delegate":{"type":"object","properties":{"atomic":{"type":"string"},"recoverable":{"type":"boolean"},"note":{"type":"string"}}}}},"ReceiveLedgerResponse":{"type":"object","description":"The account receive ledger (#3333): the persisted inbound index, the matched balance, and the saved off-ramp destination.","required":["account_address","chain_id","usdc_address","balance_atomic","balance_formatted","off_ramp_destination","transfers"],"properties":{"account_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The receiving account address."},"chain_id":{"type":"integer"},"usdc_address":{"type":["string","null"],"description":"The chain registry USDC contract, or null when the chain has none."},"balance_atomic":{"type":"string","description":"SUM of matched (balance_consumed) inbound rows, atomic units, as a numeric string. Unmatched rows are unearned and never counted."},"balance_formatted":{"type":"string","description":"The balance in USDC human decimals (6)."},"off_ramp_destination":{"oneOf":[{"$ref":"#/components/schemas/OffRampDestination"},{"type":"null"}],"description":"The owner-saved off-ramp destination, or null when none is saved yet."},"transfers":{"type":"array","items":{"$ref":"#/components/schemas/ReceiveLedgerTransfer"}}},"additionalProperties":false},"ReceiveLedgerTransfer":{"type":"object","required":["tx_hash","payer_address","amount_raw","amount_formatted","block_time","match_kind","matched_payment_intent_id","matched_receipt_id","balance_consumed","earned"],"properties":{"tx_hash":{"type":"string"},"payer_address":{"type":"string"},"amount_raw":{"type":"string","description":"Atomic units, numeric string."},"amount_formatted":{"type":"string","description":"USDC human decimals (6)."},"block_time":{"type":"string","format":"date-time"},"match_kind":{"oneOf":[{"type":"string","enum":["x402_payto","receipt"]},{"type":"null"}],"description":"How the row is linked to evidence: an x402 settlement the account was payTo for, or a supplied receipt. Null = unmatched = unearned."},"matched_payment_intent_id":{"type":["string","null"],"description":"The x402 settlement intent id, present only for x402_payto. Stored for audit; never a grant."},"matched_receipt_id":{"type":["string","null"],"description":"The linked receipt document id, present only for the receipt kind."},"balance_consumed":{"type":"boolean","description":"True once the row counts in balance_atomic."},"earned":{"type":"boolean","description":"match_kind !== null. Unmatched = unearned; nothing is delivered on an unearned row."}},"additionalProperties":false},"OffRampDestination":{"type":"object","required":["destination_address","destination_kind","label","updated_at"],"properties":{"destination_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The deposit address at the venue (Safello, Coinbase, or a custody account)."},"destination_kind":{"type":"string","enum":["safello","coinbase","custody_deposit"]},"label":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}},"additionalProperties":false},"OffRampDestinationInput":{"type":"object","required":["destination_address"],"properties":{"destination_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The deposit address. The zero address is refused."},"destination_kind":{"type":"string","enum":["safello","coinbase","custody_deposit"],"description":"Defaults to custody_deposit when omitted (handler fallback)."}},"additionalProperties":false},"OffRampPrepareInput":{"type":"object","required":["amount_atomic"],"properties":{"amount_atomic":{"type":"string","pattern":"^[0-9]+$","description":"The amount to move, atomic units. The recipient and token are NOT in the request: they are the saved destination and the chain registry USDC."}},"additionalProperties":false},"OffRampPrepareResponse":{"type":"object","required":["prepared","submit"],"properties":{"prepared":{"type":"object","additionalProperties":true,"description":"The prepared UserOperation (same shape the owner send prepare returns) — what the OWNER signs with the account own signer."},"submit":{"$ref":"#/components/schemas/OffRampSubmitInstructions"}},"additionalProperties":false},"OffRampSubmitInstructions":{"type":"object","required":["endpoint","token_address","to","destination_kind","amount_atomic","signature_required_from"],"properties":{"endpoint":{"type":"string","description":"The submit route; it re-derives the calldata and refuses a user_operation that does not contain it."},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"to":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The saved destination — echoed so the owner signs what they verified."},"destination_kind":{"type":"string"},"amount_atomic":{"type":"string","pattern":"^[0-9]+$"},"signature_required_from":{"type":"string","enum":["owner"],"description":"Only the account owner signs; no agent key can authorize this transfer."}},"additionalProperties":false},"ReceiptDropInput":{"type":"object","required":["tx_hash","amount_raw","payer_address","signature"],"properties":{"tx_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"The on-chain transfer the receipt is for — the match key."},"amount_raw":{"type":"string","pattern":"^[0-9]+$","description":"Atomic units; must equal the persisted transfer amount."},"payer_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"The payer the transfer names; the signature must recover to it."},"signature":{"type":"string","pattern":"^0x[0-9a-fA-F]+$","description":"EIP-191 personal signature over `haven:receipt-drop\\ntx:<tx_hash>\\namount_raw:<amount_raw>`."}},"additionalProperties":false},"ReceiptDropResponse":{"type":"object","required":["matched","match_kind","transfer_id"],"properties":{"matched":{"type":"boolean","enum":[true]},"match_kind":{"type":"string","enum":["x402_payto","receipt"]},"transfer_id":{"type":"string","format":"uuid"}},"additionalProperties":false},"InboundIngestInput":{"type":"object","required":["tx_hash","payer_address","amount_raw"],"properties":{"tx_hash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$"},"payer_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"]},"amount_raw":{"type":"string","pattern":"^[0-9]+$","description":"Atomic units."},"token_address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","examples":["0x1111111111111111111111111111111111111111"],"description":"Optional; defaults to the chain registry USDC — the receive index tracks USDC only."},"block_time":{"type":"string","format":"date-time","description":"Defaults to now."},"block_number":{"type":"string","pattern":"^[0-9]+$"}},"additionalProperties":false},"InboundIngestResponse":{"type":"object","required":["ingested","id"],"properties":{"ingested":{"type":"boolean","description":"false when the (chain, tx hash) was already indexed — idempotent re-ingest."},"id":{"type":["string","null"],"format":"uuid","description":"The row id, or null when it already existed."}},"additionalProperties":false},"TransactionsResponse":{"type":"object","required":["transactions","total","offset","limit","hasMore","partialFailure","failedAccountIds","truncated"],"properties":{"transactions":{"type":"array","items":{"$ref":"#/components/schemas/Transaction"}},"total":{"type":"integer"},"offset":{"type":"integer"},"limit":{"type":"integer"},"hasMore":{"type":"boolean"},"partialFailure":{"type":"boolean"},"failedAccountIds":{"type":"array","items":{"type":"string","format":"uuid"}},"truncated":{"type":"boolean","description":"At least one account's explorer read stopped at the pagination budget with the source still offering more, so these rows and `total` are a capped view rather than the full history (#2884). Independent of `partialFailure`."}},"additionalProperties":false}}}}