{"openapi":"3.1.0","info":{"title":"AgentPay API","version":"0.1.0","summary":"Smart payment rails for AI agents.","description":"Provision per-agent wallets governed by a policy engine, charge against them via a single REST endpoint, receive signed webhooks for transactions and anomalies. See https://agentpay.run/docs for prose docs.","contact":{"name":"AgentPay","email":"hello@agentpay.run","url":"https://agentpay.run"},"license":{"name":"Proprietary","url":"https://agentpay.run/terms"}},"servers":[{"url":"https://agentpay.run","description":"Production"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"ap_test_… or ap_live_…","description":"API key issued when you create a wallet. Sent as `Authorization: Bearer <key>`."}},"schemas":{"ChargeRequest":{"type":"object","required":["vendor","amount_cents"],"properties":{"vendor":{"type":"string","description":"Domain or merchant identifier, e.g. `openai.com`.","example":"openai.com"},"amount_cents":{"type":"integer","minimum":1,"description":"Charge amount in USD cents.","example":1200},"idempotency_key":{"type":"string","description":"Re-sending with the same key on the same wallet returns the prior result.","example":"msg_4197","nullable":true},"metadata":{"type":"object","additionalProperties":true,"description":"Free-form JSON, stored on the transaction.","nullable":true}}},"ChargeResponse":{"type":"object","required":["transaction_id","status","policy_matched","denial_reason","vendor","amount_cents","remaining_budget_cents","anomalies_flagged","created_at"],"properties":{"transaction_id":{"type":"integer","example":4197},"status":{"type":"string","enum":["approved","denied"]},"policy_matched":{"type":"string","nullable":true,"description":"Which policy rule matched. One of: wallet_inactive, amount_invalid, per_transaction_limit, budget_limit, vendor_allowlist, vendor_cap, default_allow.","example":"vendor_allowlist","enum":["wallet_inactive","amount_invalid","per_transaction_limit","budget_limit","vendor_allowlist","vendor_cap","default_allow"]},"denial_reason":{"type":"string","nullable":true,"example":"Vendor \"evil.com\" is not on the allowlist"},"vendor":{"type":"string","example":"openai.com"},"amount_cents":{"type":"integer","example":1200},"remaining_budget_cents":{"type":"integer","example":348800,"description":"Budget remaining on the wallet after this charge. 0 if denied or no budget set."},"anomalies_flagged":{"type":"integer","example":0,"description":"How many anomaly alerts were created from this charge (velocity, novelty, value)."},"wallet_paused":{"type":"boolean","example":false,"description":"True if the wallet's pause_on_high_severity_alert flag was set and a high-severity anomaly fired on this charge — the wallet is now is_active=false and subsequent charges will 401 until you re-activate it."},"created_at":{"type":"string","format":"date-time"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"retry_after_seconds":{"type":"integer","description":"Set on 429 responses."},"limit_per_minute":{"type":"integer"},"details":{"type":"string"}}},"WalletSnapshot":{"type":"object","required":["wallet_id","name","api_key_prefix","is_active","budget_limit_cents","spent_cents","per_transaction_limit_cents","rate_limit_per_minute","pause_on_high_severity_alert","created_at"],"properties":{"wallet_id":{"type":"integer","example":42},"name":{"type":"string","example":"Research bot"},"api_key_prefix":{"type":"string","example":"ap_test_aaaa","description":"First 12 chars of the wallet's primary key. Useful for confirming WHICH wallet the call was made against without leaking the full secret."},"api_key_scope":{"type":"string","enum":["full","read_only"],"example":"full","description":"Scope of the API key the calling agent is authenticated with. 'full' can charge; 'read_only' can only call GET /api/agent/wallet."},"is_active":{"type":"boolean","example":true},"budget_limit_cents":{"type":"integer","example":50000,"description":"Monthly budget cap. 0 means unlimited."},"spent_cents":{"type":"integer","example":1200},"remaining_budget_cents":{"type":"integer","nullable":true,"example":48800,"description":"budget_limit_cents minus spent_cents, floored at 0. null when budget_limit_cents is 0 (unlimited)."},"per_transaction_limit_cents":{"type":"integer","example":5000},"vendor_whitelist":{"type":"array","items":{"type":"string"},"nullable":true,"description":"null = allow any vendor; otherwise only listed domains."},"vendor_caps":{"type":"object","additionalProperties":{"type":"integer"},"description":"Per-vendor monthly cap in USD cents (e.g. {\"openai.com\":20000}). Empty object = no per-vendor restrictions."},"rate_limit_per_minute":{"type":"integer","example":60},"pause_on_high_severity_alert":{"type":"boolean","example":false},"last_used_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"QuotaSnapshot":{"type":"object","required":["tier","wallets_used","wallets_cap","charges_today","charges_cap","charges_at_cap","wallets_at_cap"],"properties":{"tier":{"type":"string","enum":["free","team","business","enterprise"],"example":"free"},"wallets_used":{"type":"integer","example":1},"wallets_cap":{"type":"integer","nullable":true,"example":1,"description":"null on tiers with no wallet cap (team and above)."},"wallets_remaining":{"type":"integer","nullable":true,"example":0,"description":"null when wallets_cap is null."},"charges_today":{"type":"integer","example":47,"description":"Count of transactions for this account in the current UTC day (approved + denied)."},"charges_cap":{"type":"integer","nullable":true,"example":100,"description":"null on tiers with no daily charge cap."},"charges_remaining":{"type":"integer","nullable":true,"example":53},"audit_retention_days":{"type":"integer","nullable":true,"example":7,"description":"Days of audit_log retained before nightly prune. null on enterprise (no auto-prune)."},"charges_at_cap":{"type":"boolean","example":false},"wallets_at_cap":{"type":"boolean","example":true}}},"HealthResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"checks":{"type":"object","properties":{"app":{"type":"object","properties":{"ok":{"type":"boolean"}}},"db":{"type":"object","properties":{"ok":{"type":"boolean"},"latency_ms":{"type":"integer","nullable":true},"error":{"type":"string","nullable":true}}}}},"version":{"type":"string"},"commit":{"type":"string","nullable":true},"region":{"type":"string","nullable":true},"ts":{"type":"string","format":"date-time"},"duration_ms":{"type":"integer"}}}}},"paths":{"/api/agent/transactions":{"post":{"summary":"Charge an agent wallet","description":"Run a charge through the policy engine. Returns 200 for approved, 402 for policy denial, 401 for bad key, 429 for rate-limit, 5xx for transient errors. Approved + denied charges both land in the ledger.","operationId":"createTransaction","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChargeRequest"}}}},"responses":{"200":{"description":"Approved charge — ledger updated, budget reduced.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChargeResponse"}}},"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Per-minute charge limit configured on this wallet."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Charges remaining in the current window after this one."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix seconds when the limit window resets."}}},"400":{"description":"Bad request (missing vendor, non-positive amount, bad JSON).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Denied by policy. Body shape matches 200; status field is 'denied'.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChargeResponse"}}},"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"403":{"description":"API key is read-only. Charges require a full-scope key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded for this wallet. `Retry-After` header + retry_after_seconds in body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix seconds when the limit window resets."}}},"500":{"description":"Internal server error — log it and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/agent/wallet":{"get":{"summary":"Read the wallet for the calling API key","description":"Returns a read-only snapshot of the wallet bound to the bearer API key — budget, policy limits, active state. Use this for orchestrator-style flows where the agent decides whether a call is worth making before issuing the charge. Doesn't count against the rate limit and doesn't write to the ledger.","operationId":"getWallet","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Wallet snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletSnapshot"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/agent/quota":{"get":{"summary":"Read tier + remaining quota for the calling account","description":"Returns the calling account's pricing tier and how much wallet / daily-charge quota is left. Companion to GET /api/agent/wallet — agents can poll this to back off gracefully BEFORE hitting a 429 from POST /api/agent/transactions. Read-only-scope keys succeed; this is pure introspection.","operationId":"getQuota","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Quota snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaSnapshot"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/health":{"get":{"summary":"Health check","description":"Public endpoint for uptime monitors. Returns 200 with ok:true when the app and database are reachable, 503 otherwise.","operationId":"getHealth","responses":{"200":{"description":"Healthy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"Database round-trip failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}}},"webhooks":{"anomaly.created":{"post":{"summary":"Anomaly created on one of your wallets","description":"Fires when AgentPay detects a velocity spike, new vendor, or high-value charge. Verify with `X-AgentPay-Signature` (HMAC-SHA256 of `${ts}.${body}`) and reject anything older than 5 minutes via `X-AgentPay-Timestamp`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"event":{"const":"anomaly.created"},"alert":{"type":"object","properties":{"id":{"type":"integer","nullable":true},"wallet_id":{"type":"integer"},"transaction_id":{"type":"integer","nullable":true},"alert_type":{"type":"string","enum":["velocity_spike","new_vendor","high_value_charge"]},"severity":{"type":"string","enum":["low","medium","high"]},"message":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"wallet":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}}}}}}}},"responses":{"2XX":{"description":"Acknowledged. Anything 4xx (except 429) is treated as 'don't retry'; 5xx + 429 retry with backoff."}}}},"transaction.approved":{"post":{"summary":"Charge approved","responses":{"2XX":{"description":"Acknowledged."}}}},"transaction.denied":{"post":{"summary":"Charge denied by policy","responses":{"2XX":{"description":"Acknowledged."}}}},"wallet.auto_paused":{"post":{"summary":"Wallet automatically paused after a high-severity anomaly","description":"Fires when AgentPay flips a wallet to is_active=false because pause_on_high_severity_alert was on and a high-severity anomaly (velocity_spike, new_vendor, or critical-fraction high_value_charge) just fired. Wire this to PagerDuty / Slack so a human is paged for incident response.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"event":{"const":"wallet.auto_paused"},"wallet":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}}},"alert":{"type":"object","properties":{"type":{"type":"string"},"severity":{"const":"high"},"message":{"type":"string"},"transaction_id":{"type":"integer","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}},"responses":{"2XX":{"description":"Acknowledged."}}}}}}