{"openapi":"3.1.0","info":{"title":"AgentPay API","version":"1.0.0","description":"AgentPay gives AI agents a wallet with spending rules. Every route mounted under /api/v1 is documented below. All endpoints are owner-authenticated via the `x-user-id` header (an integer matching `wallets.user_id` / `agent_accounts.user_id` — see the UserIdHeader security scheme). Use this document with Postman (Import → Raw text / File) or Insomnia to pre-fill every shipped AgentPay route with its request and response shapes."},"servers":[{"url":"/","description":"AgentPay server"}],"components":{"securitySchemes":{"UserIdHeader":{"type":"apiKey","in":"header","name":"x-user-id","description":"Per-request owner authentication. Send as an integer string matching the owning user row."}},"schemas":{"Error":{"type":"object","description":"Standard 4xx error envelope. `details` is the zod `error.flatten()` shape from routes that parse requests with zod (e.g. routes/wallets.js:298-300, routes/developer.js:30-32). The rule_* / limit / observed_value fields are populated only when an enforcement gate trips (routes/wallets.js:148-170 payout path, routes/subscriptions.js:101-133 subscription_cancel path).","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message."},"details":{"type":"object","description":"Optional zod validation flatten() payload.","additionalProperties":true},"rule_id":{"type":"integer","nullable":true},"rule_type":{"type":"string","nullable":true,"enum":["max_single_transaction","monthly_budget","daily_spend_threshold","blocked_category","allowed_category"]},"limit":{"type":["integer","string"],"nullable":true},"observed_value":{"type":["integer","string"],"nullable":true}}},"Wallet":{"type":"object","description":"agent_wallets row as returned by db/wallet.js getWalletById (the SELECT * at line 19). additionalProperties:true captures columns added in migrations not enumerated explicitly here (e.g. plaid_access_token via 1783722996794_add_plaid_columns_to_wallets.js — note that migration ALTERs the user-level wallets table; the live /api/v1 handlers read from agent_wallets via the SELECT * pattern).","properties":{"id":{"type":"integer"},"user_id":{"type":"integer"},"agent_name":{"type":"string"},"balance_cents":{"type":"integer"},"monthly_budget_cents":{"type":"integer"},"plaid_access_token":{"type":"string","nullable":true},"status":{"type":"string","description":"AgentPay-managed status (e.g. \"active\"/\"inactive\")."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"additionalProperties":true},"Transaction":{"type":"object","description":"transactions row shape returned by db/transaction.js getTransactionsByWalletIdPaginated (line 60 SELECT *). Mirrors the rows appended by routes/wallets.js topup/payout paths and services/refund-engine.js.","properties":{"id":{"type":"integer"},"wallet_id":{"type":"integer"},"amount_cents":{"type":"integer"},"merchant":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"policy_decision":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"TransactionListResponse":{"type":"object","description":"Paginated transaction listing response. Returned verbatim by GET /api/v1/wallets/{walletId}/transactions, sourced from db/transaction.js getTransactionsByWalletIdPaginated.","properties":{"transactions":{"type":"array","items":{"$ref":"#/components/schemas/Transaction"}},"next_cursor":{"type":"integer","nullable":true,"description":"Cursor (id) of the last row in `transactions`; null when no further page."},"has_more":{"type":"boolean"}}},"Policy":{"type":"object","description":"Projected policy_rules row returned by GET /api/v1/wallets/{walletId}/policies (routes/wallets.js:396-407). Mapping derives publishable fields from the storage shape consumed by services/policy-engine.js.","properties":{"rule_id":{"type":"integer"},"rule_type":{"type":"string","enum":["max_single_transaction","monthly_budget","daily_spend_threshold","blocked_category","allowed_category"]},"limit":{"type":"integer","nullable":true,"description":"Numeric cap when the rule is a numeric constraint (payout_cap / max_single_transaction); null for category-shaped rules."},"scope":{"type":"string","nullable":true,"description":"String scope for category-shaped rules; null for numeric rules."},"action":{"type":"string","enum":["block","flag"]},"status":{"type":"string","enum":["active","inactive"]},"created_at":{"type":"string","format":"date-time"}}},"PolicyListResponse":{"type":"object","description":"Bulk listing of policies for one wallet — same response shape as GET /api/v1/wallets/{walletId}/policies.","properties":{"policies":{"type":"array","items":{"$ref":"#/components/schemas/Policy"}},"next_cursor":{"type":"integer","nullable":true},"has_more":{"type":"boolean"}}},"CreatePolicyRequest":{"oneOf":[{"type":"object","description":"Numeric cap on a single payout. Mirrors the first branch of routes/wallets.js policyCreateSchema (zod literal \"payout_cap\").","required":["rule_type","limit"],"properties":{"rule_type":{"type":"string","enum":["payout_cap"]},"limit":{"type":"integer","exclusiveMinimum":0,"description":"Cap in cents (positive integer)."},"scope":{"type":"string","description":"Optional scope."},"action":{"type":"string","enum":["block","flag"],"default":"block"}}},{"type":"object","description":"Block a single merchant (mapped onto blocked_category in policy_rules). Mirrors the second branch of routes/wallets.js policyCreateSchema (zod literal \"merchant_block\").","required":["rule_type","limit"],"properties":{"rule_type":{"type":"string","enum":["merchant_block"]},"limit":{"type":"string","minLength":1,"description":"Merchant name."},"scope":{"type":"string"},"action":{"type":"string","enum":["block","flag"],"default":"block"}}},{"type":"object","description":"Block any charge carrying a given category/scope value. Mirrors the third branch of routes/wallets.js policyCreateSchema (zod literal \"recurring_block\").","required":["rule_type","scope"],"properties":{"rule_type":{"type":"string","enum":["recurring_block"]},"scope":{"type":"string","minLength":1,"description":"Non-empty scope."},"action":{"type":"string","enum":["block","flag"],"default":"block"}}}],"discriminator":{"propertyName":"rule_type","mapping":{"payout_cap":"#/components/schemas/CreatePolicyRequest","merchant_block":"#/components/schemas/CreatePolicyRequest","recurring_block":"#/components/schemas/CreatePolicyRequest"}}},"SubscriptionCandidate":{"type":"object","description":"subscription_candidates row returned by db/subscription-candidate.js getCandidatesByWalletId. Fields match the table created by 1783722996796_add_subscription_candidates.js (with status / cancelled_at added by 1784985720000_add_cancellation_fields_to_subscription_candidates.js).","properties":{"id":{"type":"integer"},"wallet_id":{"type":"integer"},"merchant":{"type":"string"},"average_amount_cents":{"type":"integer"},"detected_at":{"type":"string","format":"date-time","description":"Last charge observed during scanning."},"status":{"type":"string","enum":["active","cancelled"]},"cadence":{"type":"string","enum":["weekly","monthly","yearly"]}}},"SubscriptionCandidatesResponse":{"type":"object","properties":{"candidates":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionCandidate"}}}},"ScanResponse":{"type":"object","description":"Return value from services/subscription-scanner.js scanWallet: number of Plaid transactions read and number of high-confidence clusters surfaced.","properties":{"scanned":{"type":"integer"},"candidates":{"type":"integer"}}},"CancelSubscriptionResponse":{"type":"object","description":"Return value from services/subscription-canceller.js processSubscriptionCancellation: bundle of the updated candidate row, the appended ledger row (null when nothing was credited), the updated wallet row, and the credited cents amount.","properties":{"candidate":{"$ref":"#/components/schemas/SubscriptionCandidate"},"transaction":{"allOf":[{"$ref":"#/components/schemas/Transaction"}],"nullable":true},"wallet":{"$ref":"#/components/schemas/Wallet"},"credited_amount":{"type":"integer","description":"Pro-rated cents credited back to the wallet (0 if cycle elapsed)."}}},"Refund":{"type":"object","description":"refunds audit row written by db/refund.js recordRefundRecovery (mirrors the table created by 1783722996795_add_refunds_table.js).","properties":{"id":{"type":"integer"},"wallet_id":{"type":"integer"},"amount_cents":{"type":"integer"},"merchant":{"type":"string","nullable":true},"reason":{"type":"string","nullable":true},"status":{"type":"string","description":"Pending until recovery credits the wallet."},"transaction_id":{"type":"integer","nullable":true,"description":"Linked transactions.id on successful recovery."},"recovered_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"RefundListResponse":{"type":"object","properties":{"refunds":{"type":"array","items":{"$ref":"#/components/schemas/Refund"}}}},"RefundCreateRequest":{"type":"object","required":["amount_cents","merchant","reason"],"properties":{"amount_cents":{"type":"integer","exclusiveMinimum":0,"description":"Positive integer cents to credit back to the wallet."},"merchant":{"type":"string"},"reason":{"type":"string"}}},"RefundCreateResponse":{"type":"object","description":"Atomic recovery bundle returned by db/refund.js recordRefundRecovery: the refund audit row, the appended transactions row (category=\"refund\"), and the updated wallet row.","properties":{"refund":{"$ref":"#/components/schemas/Refund"},"transaction":{"$ref":"#/components/schemas/Transaction"},"wallet":{"$ref":"#/components/schemas/Wallet"}}},"PolicyAuditEntry":{"type":"object","description":"Single policy_audit row returned by routes/wallets.js:701-712 (GET /api/v1/wallets/{walletId}/policy-audit). Mirrors the row shape written by db/policy-audit.js (table created in 1785800000000_add_policy_audit.js, with the endpoint CHECK extended in 1787000000000_extend_policy_audit_endpoint.js).","properties":{"id":{"type":"integer"},"wallet_id":{"type":"integer"},"endpoint":{"type":"string","enum":["payout","subscription_cancel","policy_delete"]},"decision":{"type":"string","enum":["allow","reject"]},"rule_id":{"type":"integer","nullable":true},"observed_value":{"type":"string","nullable":true},"limit_value":{"type":"string","nullable":true},"actor":{"type":"string","nullable":true},"reason":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"PolicyAuditResponse":{"type":"object","properties":{"policy_audit":{"type":"array","items":{"$ref":"#/components/schemas/PolicyAuditEntry"}},"limit":{"type":"integer","minimum":1},"offset":{"type":"integer","minimum":0}}},"WeeklySummary":{"type":"object","description":"Weekly activity digest assembled by services/activity-summary.js buildWeeklySummary. Field set mirrors the JSON returned by routes/agents.js:202 (GET /api/v1/agents/{agentId}/weekly-summary).","properties":{"agent_id":{"type":"integer"},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"window_days":{"type":"integer"},"totals":{"type":"object","properties":{"total_spend_cents":{"type":"integer"},"refunds_recovered_cents":{"type":"integer"},"payouts_cents":{"type":"integer"},"top_ups_cents":{"type":"integer"},"net_position_change_cents":{"type":"integer"}}},"transaction_count":{"type":"integer"},"refund_count":{"type":"integer"},"wallet_count":{"type":"integer"},"has_activity":{"type":"boolean"},"wallets":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"agent_name":{"type":"string"},"balance_cents":{"type":"integer"},"totals":{"type":"object","properties":{"spend_cents":{"type":"integer"},"refunds_cents":{"type":"integer"},"payouts_cents":{"type":"integer"},"top_ups_cents":{"type":"integer"},"net_position_change_cents":{"type":"integer"}}},"transaction_count":{"type":"integer"},"refund_count":{"type":"integer"}}}}}},"WeeklySummaryResponse":{"type":"object","properties":{"summary":{"$ref":"#/components/schemas/WeeklySummary"}}},"CreateApiKeyRequest":{"type":"object","description":"Mirrors zod schema KEY_CREATE_SCHEMA at routes/developer.js:14-16.","required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":255}}},"CreateApiKeyResponse":{"type":"object","description":"Response shape from routes/developer.js:44-50. The plaintext `key` is returned exactly once on creation — only the bcrypt digest is persisted (db/api-keys.js insertApiKey).","properties":{"id":{"type":"integer"},"owner_id":{"type":"integer"},"label":{"type":"string"},"key":{"type":"string","minLength":43,"description":"Base64url-encoded 32 random bytes."},"created_at":{"type":"string","format":"date-time"}}},"PayoutRequest":{"type":"object","required":["amount_cents"],"properties":{"amount_cents":{"type":"integer","exclusiveMinimum":0,"description":"Positive integer cents to debit."}}},"PayoutResponse":{"type":"object","description":"Response shape from routes/wallets.js:207.","properties":{"wallet":{"$ref":"#/components/schemas/Wallet"},"payout_cents":{"type":"integer"},"transfer_id":{"type":"string","description":"Plaid transfer id."}}},"TopUpResponse":{"type":"object","description":"Response shape from routes/wallets.js:118.","properties":{"wallet":{"$ref":"#/components/schemas/Wallet"},"top_up_cents":{"type":"integer"}}}}},"security":[{"UserIdHeader":[]}],"paths":{"/api/v1/wallets/{walletId}/transactions":{"get":{"operationId":"listWalletTransactions","security":[{"UserIdHeader":[]}],"summary":"List transactions for a wallet","description":"Source: routes/wallets.js:604-636. Owner-only via the wallet owner check at lines 607-612 (DEMO_CURRENT_USER_ID=1).","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Paginated transactions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionListResponse"}}}},"400":{"description":"Invalid from / to date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/topup":{"post":{"operationId":"topUpWallet","security":[{"UserIdHeader":[]}],"summary":"Top up wallet from linked Plaid bank account","description":"Source: routes/wallets.js:89-123. Reads no body — accepts an empty {} so Postman shows a defined request shape.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Top-up succeeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopUpResponse"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Missing or zero-balance Plaid link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/payout":{"post":{"operationId":"payoutWallet","security":[{"UserIdHeader":[]}],"summary":"Debit wallet and push funds to linked Plaid bank account","description":"Source: routes/wallets.js:128-212. Routed through services/policy-engine.evaluate before the debit.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutRequest"}}}},"responses":{"200":{"description":"Payout succeeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutResponse"}}}},"400":{"description":"amount_cents missing or not positive.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Policy blocked the payout (rule_id / rule_type / limit / observed_value populated on the body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Insufficient wallet balance or no Plaid link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Underlying Plaid transfer failed; balance restored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/policies":{"post":{"operationId":"createPolicy","security":[{"UserIdHeader":[]}],"summary":"Create a spending policy rule","description":"Source: routes/wallets.js:264-334 (POST handler) / :290-334 actual route. Accepts the discriminated user-friendly schema and translates onto policy_rules columns read by services/policy-engine.js.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePolicyRequest"}}}},"responses":{"201":{"description":"Policy persisted.","content":{"application/json":{"schema":{"type":"object","properties":{"rule":{"$ref":"#/components/schemas/Policy"}}}}}},"400":{"description":"zod \"Invalid request\" with zod error.flatten() in details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listPolicies","security":[{"UserIdHeader":[]}],"summary":"List the wallet’s spending rules (paginated)","description":"Source: routes/wallets.js:336-414. Surfaces the rows services/policy-engine.js sees on each evaluation.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"schema":{"type":"integer"}},{"name":"rule_type","in":"query","required":false,"schema":{"type":"string","enum":["max_single_transaction","monthly_budget","daily_spend_threshold","blocked_category","allowed_category"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","inactive"]}}],"responses":{"200":{"description":"Paginated policy listing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyListResponse"}}}},"400":{"description":"Invalid cursor / rule_type / status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/policies/{policyId}":{"delete":{"operationId":"deletePolicy","security":[{"UserIdHeader":[]}],"summary":"Revoke a single spending policy rule","description":"Source: routes/wallets.js:416-469. Writes a policy_delete row to policy_audit before deletion.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}},{"name":"policyId","in":"path","required":true,"description":"Numeric policy_rules id.","schema":{"type":"integer","minimum":1}}],"responses":{"204":{"description":"Policy revoked.","content":{}},"400":{"description":"Invalid wallet id or policy id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet or policy not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/refunds":{"get":{"operationId":"listRefunds","security":[{"UserIdHeader":[]}],"summary":"List refund audit rows for a wallet","description":"Source: routes/wallets.js:593-601.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Refund audit list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundListResponse"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createRefund","security":[{"UserIdHeader":[]}],"summary":"Credit a recovered refund to the wallet","description":"Source: routes/wallets.js:569-590. Atomically increments balance, writes a transactions row (category=\"refund\"), and writes a refunds row (status=\"recovered\") via db/refund.js recordRefundRecovery.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundCreateRequest"}}}},"responses":{"201":{"description":"Recovery bundle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundCreateResponse"}}}},"400":{"description":"INVALID_AMOUNT from services/refund-engine.js.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"WALLET_NOT_FOUND from services/refund-engine.js.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/policy-audit":{"get":{"operationId":"listPolicyAudit","security":[{"UserIdHeader":[]}],"summary":"Owner-facing audit of unified policy decisions","description":"Source: routes/wallets.js:645-719. Reads from db/policy-audit.js listPolicyAuditByWalletId.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}},{"name":"rule_id","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"decision","in":"query","required":false,"schema":{"type":"string","enum":["allow","reject"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"minimum":0}}],"responses":{"200":{"description":"Paginated audit rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyAuditResponse"}}}},"400":{"description":"Invalid rule_id / decision / limit / offset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/subscriptions":{"get":{"operationId":"listSubscriptionCandidates","security":[{"UserIdHeader":[]}],"summary":"List detected subscription candidates","description":"Source: routes/subscriptions.js:34-45. Implicit owner check via DEMO_CURRENT_USER_ID=1 inside loadOwnedWallet.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Candidates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCandidatesResponse"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this wallet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Wallet not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/scan":{"post":{"operationId":"scanSubscriptions","security":[{"UserIdHeader":[]}],"summary":"Run the subscription scanner once","description":"Source: routes/subscriptions.js:48-65.","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"201":{"description":"Scanner result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResponse"}}}},"404":{"description":"WALLET_NOT_FOUND from services/subscription-scanner.js.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"NO_PLAID_LINK from services/subscription-scanner.js.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/wallets/{walletId}/subscriptions/{subscriptionId}/cancel":{"post":{"operationId":"cancelSubscription","security":[{"UserIdHeader":[]}],"summary":"Cancel a detected subscription candidate","description":"Source: routes/subscriptions.js:80-205. The policy gate at lines 97-100 evaluates services/policy-engine.evaluate on the candidate’s average_amount_cents and merchant (passed as `category`).","parameters":[{"name":"walletId","in":"path","required":true,"description":"Numeric wallet id (primary key of agent_wallets).","schema":{"type":"integer","minimum":1}},{"name":"subscriptionId","in":"path","required":true,"description":"Numeric subscription_candidates id (scoped to walletId).","schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Cancellation bundle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelSubscriptionResponse"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Policy blocked the cancellation (rule_id / rule_type / limit / observed_value populated on the body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"CANDIDATE_NOT_FOUND or WALLET_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"ALREADY_CANCELLED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"UNKNOWN_CADENCE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/agents/{agentId}/weekly-summary":{"get":{"operationId":"agentWeeklySummary","security":[{"UserIdHeader":[]}],"summary":"Rolling 7-day activity digest for an agent","description":"Source: routes/agents.js:181-207.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Numeric agent id (primary key of agent_accounts).","schema":{"type":"integer","minimum":1}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Weekly summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeeklySummaryResponse"}}}},"400":{"description":"Invalid from / to date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Caller does not own this agent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/developer/keys":{"post":{"operationId":"createApiKey","security":[{"UserIdHeader":[]}],"summary":"Issue a new developer API key for the owner","description":"Source: routes/developer.js:21-55. Returns the plaintext exactly once on creation; only the bcrypt digest is persisted.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}}},"responses":{"201":{"description":"Plaintext key returned once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyResponse"}}}},"400":{"description":"zod \"Invalid request\" with zod error.flatten() in details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}