API Reference
Integrate AgentPay in five minutes
One scoped key per agent. Issue a charge request, get an approve/deny verdict back. The policy engine runs server-side; your agent only sees the result.
Authentication
Every request to the AgentPay API authenticates with a bearer token — the API key issued when you create a wallet. Keys are shown once at creation; rotate them anytime from the wallet detail page.
Authorization: Bearer ap_test_xxxxxxxxxxxxxxxxxxxxxxxx
Test keys are prefixed ap_test_; production keys with ap_live_. Do not commit either to source control.
Charge an agent wallet
Send a single POST to the charge endpoint. The policy engine evaluates per-transaction cap, monthly budget, and vendor allowlist before approving. Approved charges are committed to the ledger and reduce the wallet's remaining budget.
/api/agent/transactionsRequest body
| param | type | required | description |
|---|---|---|---|
vendor | string | yes | Domain or merchant identifier (e.g. openai.com) |
amount_cents | integer | yes | Charge amount, in cents (e.g. 1200 = $12.00) |
idempotency_key | string | no | Re-sending the same key returns the prior result |
metadata | object | no | Free-form JSON, stored on the transaction |
Example
curl -X POST https://agentpay.run/api/agent/transactions \
-H "Authorization: Bearer $AGENTPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"vendor": "openai.com",
"amount_cents": 1200,
"idempotency_key": "msg_4197"
}'Approved response (200)
{
"transaction_id": 4197,
"status": "approved",
"policy_matched": "vendor_allowlist",
"denial_reason": null,
"vendor": "openai.com",
"amount_cents": 1200,
"remaining_budget_cents": 348800,
"anomalies_flagged": 0,
"created_at": "2026-05-01T15:42:11.000Z"
}Denied response (402)
{
"transaction_id": 4198,
"status": "denied",
"policy_matched": "vendor_allowlist",
"denial_reason": "Vendor \"evil.com\" is not on the allowlist",
"vendor": "evil.com",
"amount_cents": 9900,
"remaining_budget_cents": 348800,
"anomalies_flagged": 0,
"created_at": "2026-05-01T15:42:14.000Z"
}Read your wallet
GET /api/agent/walletreturns the policy and spend state for the wallet bound to your API key — useful for orchestrators that want to decide whether a call is worth making before issuing the charge. Read-only, doesn't consume rate limit, doesn't write to the ledger.
curl https://agentpay.run/api/agent/wallet \ -H "Authorization: Bearer $AGENTPAY_API_KEY"
{
"wallet_id": 42,
"name": "Research bot",
"is_active": true,
"api_key_prefix": "ap_test_aaaa",
"api_key_scope": "read_only",
"budget_limit_cents": 50000,
"spent_cents": 1200,
"remaining_budget_cents": 48800,
"per_transaction_limit_cents": 5000,
"vendor_whitelist": ["openai.com", "github.com"],
"vendor_caps": { "openai.com": 20000 },
"rate_limit_per_minute": 60,
"pause_on_high_severity_alert": false,
"last_used_at": "2026-05-01T15:42:11.000Z"
}Read-only API keys
Each wallet can have multiple keys with different scopes. The default primary key is full-scope (read + charge). Mint additional keys at read_onlyscope from the wallet detail page when you need to share budget visibility with an embedded client / third-party plugin / edge runtime that shouldn't be able to charge. Read-only keys:
- succeed on
GET /api/agent/walletwith the same JSON body shape as a full-scope key - return
403onPOST /api/agent/transactionswith{ "error": "This API key is read-only..." }
Policy evaluation order
- Wallet active? Paused wallets reject everything.
- Per-transaction cap. If set, charges over the cap are denied.
- Budget remaining. If set, charges that would push the wallet over budget are denied.
- Vendor allowlist. If set, only allowlisted vendors are approved. Empty allowlist = allow any vendor.
- Per-vendor cap.If a cap is configured for the charge's vendor, the approved spend on that wallet+vendor in the last 30 days plus this charge must stay under the cap. Vendors without a cap entry fall through to
default_allow.
Each check returns the matching policy in policy_matched so you can debug exactly why a charge was denied. Possible values: wallet_inactive, amount_invalid, per_transaction_limit, budget_limit, vendor_allowlist, vendor_cap, default_allow.
Webhooks
Webhooks deliver events from AgentPay to an HTTPS endpoint you own. Each delivery is a single signed JSON POST. Acknowledge with a 2xx; anything else triggers retry with exponential backoff.
1. Create the webhook
On /dashboard/settings, click New webhook. Paste your endpoint URL, pick the events you want, optionally scope to a single wallet (otherwise it fires for all your wallets). The dialog shows a whsec_… secret once — copy it and set it on your handler as AGENTPAY_WEBHOOK_SECRET. You can rotate or test the webhook anytime from the row.
2. Pick the events that matter
| event | fires when |
|---|---|
anomaly.created | Velocity spike, novel vendor, high-value charge, or vendor cap nearing the limit |
transaction.approved | Every charge that passed policy |
transaction.denied | Every charge rejected by policy (vendor allowlist, budget, vendor cap, etc.) |
wallet.auto_paused | AgentPay flipped a wallet to is_active=false after a high-severity anomaly. Wire to PagerDuty / Slack / OpsGenie for paging. |
3. Know the request shape
Every delivery carries these headers. Verify X-AgentPay-Signature against the raw body (see step 4) before you act on the payload.
| header | value |
|---|---|
X-AgentPay-Signature | sha256=<hex> over `${ts}.${body}` |
X-AgentPay-Timestamp | Unix seconds at the time of sign — reject if >5min skew |
X-AgentPay-Event | Event type (e.g. anomaly.created) — also in body.event |
X-AgentPay-Delivery | Delivery ID — stable across retries, use for dedupe |
X-AgentPay-Attempt | Attempt number (1, 2, 3, …) — >1 means we retried |
The body is the same JSON the OpenAPI spec describes for that event type. Smallest example (anomaly.created):
{
"event": "anomaly.created",
"alert": {
"id": 17,
"wallet_id": 42,
"transaction_id": 4197,
"alert_type": "velocity_spike",
"severity": "high",
"message": "5 transactions in the last 60 seconds — possible runaway agent.",
"created_at": "2026-05-04T12:34:56.000Z"
},
"wallet": { "id": 42, "name": "Research bot" }
}4. Verify the signature
Sign the raw request body bytes, not a re-serialized JSON object. If your framework auto-parses JSON, opt out (Next.js await req.text(); Express express.json({ verify }) to capture rawBody; FastAPI await request.body()).
Node.js (Next.js App Router route handler)
// app/api/webhooks/agentpay/route.ts
import { createHmac, timingSafeEqual } from "crypto";
export async function POST(req: Request) {
const rawBody = await req.text(); // raw bytes — DO NOT use req.json()
const ts = Number(req.headers.get("x-agentpay-timestamp"));
const sig = req.headers.get("x-agentpay-signature") ?? "";
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) {
return new Response("stale timestamp", { status: 401 });
}
const expected =
"sha256=" +
createHmac("sha256", process.env.AGENTPAY_WEBHOOK_SECRET!)
.update(`${ts}.${rawBody}`)
.digest("hex");
if (
expected.length !== sig.length ||
!timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(rawBody);
switch (event.event) {
case "anomaly.created": /* alert your team */ break;
case "transaction.approved": /* update your spend ledger */ break;
case "transaction.denied": /* surface to the agent or operator */break;
case "wallet.auto_paused": /* page on-call */ break;
}
return new Response("ok"); // 2xx tells AgentPay "delivered, don't retry"
}Python (FastAPI)
import hmac, hashlib, json, os, time
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/webhooks/agentpay")
async def agentpay_webhook(req: Request):
raw_body = await req.body() # raw bytes — DO NOT json()
ts = int(req.headers.get("x-agentpay-timestamp", "0"))
sig = req.headers.get("x-agentpay-signature", "")
if not ts or abs(time.time() - ts) > 300:
raise HTTPException(401, "stale timestamp")
secret = os.environ["AGENTPAY_WEBHOOK_SECRET"]
expected = "sha256=" + hmac.new(
secret.encode(),
f"{ts}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, sig):
raise HTTPException(401, "bad signature")
event = json.loads(raw_body)
# event["event"] in {"anomaly.created", "transaction.approved",
# "transaction.denied", "wallet.auto_paused"}
return {"ok": True} # 2xx ack5. Retry behavior
Anything other than 2xx, plus 429 specifically, queues a retry. Backoff schedule: 30s → 2m → 10m → 30m → 2h → 6h, then abandoned. Each retry carries the same X-AgentPay-Delivery ID and an incremented X-AgentPay-Attempt — dedupe on delivery ID if your handler isn't idempotent.
Common pitfalls
- JSON parsing changes the bytes. Re-serializing the parsed JSON (different key order, different whitespace, different numeric formatting) produces a different signature. Always sign over the raw bytes you received.
- Clock skew.If your handler's clock is more than 5 minutes off, every signature looks stale. NTP-sync the host.
- Body-parsing middleware silently consumes the stream. If you read the body in middleware, save it in a way the handler can still see (Express:
verifycallback; Next.js: route handler readsreq.text()first). - Returning early on success but late on retries.If you ack 2xx then crash, AgentPay won't retry — make sure your handler is idempotent (use
X-AgentPay-Delivery) so a retry doesn't double-process.
Anomaly alerts
Approved charges are also evaluated for anomalies and flagged on the wallet's alerts inbox. Medium- and high-severity alerts are pushed to the wallet owner's email automatically.
| alert_type | severity | trigger |
|---|---|---|
velocity_spike | high | 5+ transactions in a 60-second window |
high_value_charge | medium | Single charge consumes ≥50% of remaining budget |
vendor_cap_near | medium / high | Approved spend on a capped vendor crosses ≥80% (medium) or ≥95% (high) of its monthly cap. Lets you raise the cap before the next charge hard-fails. |
new_vendor | low | First transaction with a vendor this wallet has paid |
Code samples
Node.js / TypeScript
async function charge(vendor: string, amountCents: number) {
const res = await fetch("https://agentpay.run/api/agent/transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AGENTPAY_API_KEY!}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ vendor, amount_cents: amountCents }),
});
return res.json();
}Python
import os, requests
def charge(vendor: str, amount_cents: int):
return requests.post(
"https://agentpay.run/api/agent/transactions",
headers={"Authorization": f"Bearer {os.environ['AGENTPAY_API_KEY']}"},
json={"vendor": vendor, "amount_cents": amount_cents},
).json()OpenAPI spec
The public API is described as OpenAPI 3.1 at /api/openapi.json. Drop it into Postman, Insomnia, Stainless, Fern, or any OpenAPI generator to scaffold your own client.
Ready to provision a wallet?
Sign up, create a wallet with your spending policy, and copy the API key.