API documentation

AI Payment Guard is an authorization layer that sits between your AI agents and any payment rail (card, bank transfer, crypto). The agent asks for permission first — AI Payment Guard evaluates policies, scores risk, and optionally routes to a human approver. Only after an explicit APPROVED decision does the agent proceed to its payment provider.

We never touch your money. We decide whether your agent is allowed to.

Agent → POST /payment-intents → AI Payment Guard
↓ evaluate policies + risk score
← APPROVED / REJECTED / PENDING_HUMAN_REVIEW
If APPROVED:
Agent calls its payment provider (Stripe, bank API, crypto wallet…)
Agent confirms: POST /payment-intents/:id/execute
If PENDING_HUMAN_REVIEW:
↓ human decides in the dashboard
Agent gets notified → then proceeds to execute

Base URL

https://api.your-domain.com/api/v1

Authentication

All requests require a Bearer token in the Authorization header. Each agent has its own key, generated from the dashboard and shown only once. Keys can be rotated at any time from Settings → Agents → Rotate key.

Authorization: Bearer pg_live_a4f2c1e9d8b7a3...

1. Submit a payment intent

POST /api/v1/payment-intents

The single call your agent makes before touching any money. Returns a decision in one round-trip if a policy matches; otherwise holds the request for human review.

Required headers

  • Authorization: Bearer <key>
  • Idempotency-Key: <unique string, 8–200 chars> — replaying the same key returns the original decision without creating a duplicate.
  • Content-Type: application/json

Body

{
  "amount_minor":  24900,            // integer, smallest currency unit (e.g. cents)
  "currency":      "EUR",            // ISO-4217, 3 uppercase letters
  "beneficiary": {
    "name":               "AWS",
    "account_identifier": "DE12500105170648489890",  // IBAN, wallet address, account id…
    "category":           "infrastructure"           // optional
  },
  "category":   "infrastructure",    // matched against CATEGORY_BLOCKLIST policies
  "memo":       "Monthly invoice",   // optional — improves human-review context
  "metadata":   { "invoice_id": "INV-042" },  // optional, stored as-is

  // ── Notification options (pick one or none) ───────────────────────────
  "callback_url": "https://your-agent.example/hooks/payment-guard"
  //  AI Payment Guard will POST to this URL when a human decides.
  //  Signed with HMAC-SHA256 (see "Receiving notifications" below).
}

Examples

curl -X POST https://api.your-domain.com/api/v1/payment-intents \
  -H "Authorization: Bearer $PAYMENT_GUARD_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 24900,
    "currency": "EUR",
    "beneficiary": {
      "name": "AWS",
      "account_identifier": "DE12500105170648489890"
    },
    "category": "infrastructure",
    "memo": "Monthly invoice"
  }'

Response (201 Created)

{
  "id":              "8a4f3b1e-...",
  "status":          "APPROVED" | "REJECTED" | "PENDING_HUMAN_REVIEW",
  "decision":        "APPROVED" | "REJECTED" | "REQUIRES_HUMAN_APPROVAL",
  "decisionReason":  "amount_lt_500000_EUR",
  "matchedPolicyId": "p1_...",
  "requiresApproval": false,
  "expiresAt":       "2026-05-04T12:15:00.000Z"  // set when APPROVED; null otherwise
}

2. Handle the decision

DecisionStatusWhat the agent should do
APPROVEDAPPROVEDCall your payment provider, then confirm with POST /payment-intents/:id/execute within 15 minutes.
REJECTEDREJECTEDFinal. Stop the payment — do not retry.
REQUIRES_HUMAN_APPROVALPENDING_HUMAN_REVIEWWait for a human to decide. Choose one of the three notification strategies below.

Waiting for human approval

When the decision is REQUIRES_HUMAN_APPROVAL, a human must approve or reject the intent in the dashboard before the agent can proceed. There are three ways to know when that happens.

A

Polling — simplest, no infrastructure needed

Poll GET /api/v1/payment-intents/:id every few seconds until status leaves PENDING_HUMAN_REVIEW. Works anywhere, no public URL required.

import time, requests

def wait_for_decision(intent_id: str, poll_interval: int = 5, timeout: int = 600):
    deadline = time.time() + timeout
    while time.time() < deadline:
        resp = requests.get(
            f"https://api.your-domain.com/api/v1/payment-intents/{intent_id}",
            headers={"Authorization": f"Bearer {os.environ['PAYMENT_GUARD_KEY']}"},
            timeout=10,
        ).json()
        status = resp["status"]
        if status == "APPROVED":
            return "approved"
        if status in ("REJECTED", "CANCELLED"):
            return status.lower()
        time.sleep(poll_interval)
    raise TimeoutError("still pending")
B

Callback URL — best for serverless & async agents

Pass a callback_url when submitting the intent. AI Payment Guard will POST to that URL the moment a human decides — signed with HMAC-SHA256 so you can verify authenticity. Ideal for Lambda functions, Cloud Run jobs, or any agent that cannot hold an open connection.

curl -X POST https://api.your-domain.com/api/v1/payment-intents \
  -H "Authorization: Bearer $PAYMENT_GUARD_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 50000,
    "currency": "EUR",
    "beneficiary": { "name": "Stripe", "account_identifier": "acct_1A2B3C" },
    "category": "saas",
    "callback_url": "https://your-agent.example/hooks/payment-guard"
  }'
Callback payload: { event, payment_intent_id, status, decision_reason, resolved_at, comment?, reject_reason? }
C

SSE /wait — best for synchronous agents

Open a long-lived GET connection to /api/v1/payment-intents/:id/wait. The server holds it open, polls for status changes every 2 seconds, and pushes a Server-Sent Event the moment the human decides. No public URL needed — the agent just blocks on the stream. Max wait: 10 minutes (configurable via ?timeout_ms=).

# Blocks until the human decides (or 10 min timeout)
curl -N https://api.your-domain.com/api/v1/payment-intents/$INTENT_ID/wait \
  -H "Authorization: Bearer $PAYMENT_GUARD_KEY"

# Output:
# : heartbeat
# : heartbeat
# event: intent.approved
# data: {"id":"...","status":"APPROVED","decision_reason":"...","expires_at":"..."}
Events: intent.approved · intent.rejected · intent.expired · timeout. Heartbeat comments every 15s to keep proxies alive.

Authorization window (15 minutes)

Every APPROVED intent carries an expires_at timestamp set 15 minutes after approval. If the agent calls POST /payment-intents/:id/execute after that window, the endpoint returns 410 Gone with code intent_expired and the intent is automatically cancelled.

// 410 Gone
{
  "error": {
    "code":       "intent_expired",
    "message":    "Authorization expired at 2026-05-04T12:15:00.000Z",
    "expiredAt":  "2026-05-04T12:15:00.000Z"
  }
}

If you need more time, cancel the original intent and re-submit with a new idempotency key. The 15-minute window is intentional — stale authorizations are a common attack vector.

3. Confirm execution

POST /api/v1/payment-intents/:id/execute

Call this after your payment provider has processed the transaction. This closes the audit trail and marks the intent as EXECUTED. Only valid while status is APPROVED and expires_at has not passed.

curl -X POST https://api.your-domain.com/api/v1/payment-intents/$INTENT_ID/execute \
  -H "Authorization: Bearer $PAYMENT_GUARD_KEY"

Other endpoints

  • GET /api/v1/payment-intents/:id — current state + expires_at.
  • GET /api/v1/payment-intents?limit=50 — list intents for this agent (max 200).
  • POST /api/v1/payment-intents/:id/cancel — cancel while PENDING_HUMAN_REVIEW or APPROVED.

Org-level webhooks

Register endpoints in Settings to receive all events for your organization (useful for logging pipelines, Slack alerts, etc.). Each delivery is HMAC-SHA256 signed.

Event types

  • intent.approved — auto-approved by policy or human approved
  • intent.rejected — auto-rejected by policy or human rejected
  • intent.requires_human — held for human review
  • intent.executed — agent confirmed execution

Signature verification

import { verifyWebhookSignature } from "@payment-guard/node";

const rawBody = await request.text();
const signature = request.headers.get("x-paymentguard-signature")!;
const event = verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET!);

if (event.type === "intent.approved") {
  // log, alert, trigger downstream workflow…
}

Errors

StatusCodeMeaning
400validation_errorBody did not match the schema.
400missing_idempotency_keyHeader absent or malformed.
401agent_auth_failedInvalid or inactive Bearer token.
404not_foundIntent not found or does not belong to your org.
409invalid_stateAction not allowed in current status.
410intent_expiredAuthorization window (15 min) elapsed — intent cancelled.
500internal_errorUnexpected server-side failure.
// All errors follow this shape:
{
  "error": {
    "code":    "intent_expired",
    "message": "Authorization expired at 2026-05-04T12:15:00.000Z",
    "details": { … }   // optional
  }
}

Node.js SDK

npm install @payment-guard/node
import { PaymentGuard } from "@payment-guard/node";

const client = new PaymentGuard({ apiKey: process.env.PAYMENT_GUARD_KEY! });

const intent = await client.paymentIntents.submit({
  amount_minor: 24_900,
  currency: "EUR",
  beneficiary: { name: "AWS", account_identifier: "DE12500105170648489890" },
  category: "infrastructure",
  memo: "Monthly invoice",
});

if (intent.decision === "REJECTED") {
  console.log("Blocked:", intent.decisionReason);
  return;
}

if (intent.decision === "REQUIRES_HUMAN_APPROVAL") {
  // SSE — blocks until human decides (Option C)
  await client.paymentIntents.waitForDecision(intent.id, { timeoutMs: 600_000 });
}

// ← decision is now APPROVED
// Call your payment provider here (Stripe, Wise, Coinbase, etc.)

await client.paymentIntents.execute(intent.id);

Questions? support@payment-guard.example