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.

NewAgentPay now ships an official MCP server for Claude Desktop, Cursor, and any MCP-compatible host.
Read MCP docs →

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.

bash
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.

POST/api/agent/transactions

Request body

paramtyperequireddescription
vendorstringyesDomain or merchant identifier (e.g. openai.com)
amount_centsintegeryesCharge amount, in cents (e.g. 1200 = $12.00)
idempotency_keystringnoRe-sending the same key returns the prior result
metadataobjectnoFree-form JSON, stored on the transaction

Example

bash
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)

json
{
  "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)

json
{
  "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.

bash
curl https://agentpay.run/api/agent/wallet \
  -H "Authorization: Bearer $AGENTPAY_API_KEY"
json
{
  "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/wallet with the same JSON body shape as a full-scope key
  • return 403 on POST /api/agent/transactions with { "error": "This API key is read-only..." }

Policy evaluation order

  1. Wallet active? Paused wallets reject everything.
  2. Per-transaction cap. If set, charges over the cap are denied.
  3. Budget remaining. If set, charges that would push the wallet over budget are denied.
  4. Vendor allowlist. If set, only allowlisted vendors are approved. Empty allowlist = allow any vendor.
  5. 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

eventfires when
anomaly.createdVelocity spike, novel vendor, high-value charge, or vendor cap nearing the limit
transaction.approvedEvery charge that passed policy
transaction.deniedEvery charge rejected by policy (vendor allowlist, budget, vendor cap, etc.)
wallet.auto_pausedAgentPay 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.

headervalue
X-AgentPay-Signaturesha256=<hex> over `${ts}.${body}`
X-AgentPay-TimestampUnix seconds at the time of sign — reject if >5min skew
X-AgentPay-EventEvent type (e.g. anomaly.created) — also in body.event
X-AgentPay-DeliveryDelivery ID — stable across retries, use for dedupe
X-AgentPay-AttemptAttempt 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):

json
{
  "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)

ts
// 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)

python
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 ack

5. 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: verify callback; Next.js: route handler reads req.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_typeseveritytrigger
velocity_spikehigh5+ transactions in a 60-second window
high_value_chargemediumSingle charge consumes ≥50% of remaining budget
vendor_cap_nearmedium / highApproved 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_vendorlowFirst transaction with a vendor this wallet has paid

Code samples

Node.js / TypeScript

ts
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

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.