·6 min read·The PayGraph Team

Designing approval webhooks for AI agent spending

A practical guide to agent approval webhooks: payload schema, idempotency, timeouts, retries, and reference implementations for Slack and generic HTTP endpoints.

Approval webhooks are the seam between an AI agent that wants to spend money and a human who decides whether it should. Get the seam right and the agent ships. Get it wrong and you have an outage, a duplicate charge, or a payment that hangs forever.

What is an agent approval webhook?

An agent approval webhook is an HTTP callback fired by your spending control layer when a proposed transaction needs a human decision. The webhook delivers a structured payload describing the transaction, waits for an approve-or-deny response, and then either releases the payment or aborts it.

PayGraph is an open-source SDK for policy-controlled spending, approvals, and audit logs for AI agents. Approval webhooks are one of its core primitives — the mechanism that connects a paused agent to a Slack channel, a PagerDuty rotation, or a custom HTTP endpoint owned by your finance team.

What should the payload look like?

A clean approval payload is self-contained. The approver should never need to query a second system to make a decision. That means including the agent's identity, the transaction, the policy rule that triggered the approval, and the deadline.

{
  "approval_id": "apr_01HF8K9X2N4P5Q6R7S8T9V0W1X",
  "created_at": "2026-04-28T14:32:11Z",
  "expires_at": "2026-04-28T14:47:11Z",
  "agent": {
    "id": "agent_billing_assistant",
    "run_id": "run_01HF8K9X2N4P5Q6R7S8T9V0W1Y",
    "operator": "ops-team@acme.com"
  },
  "transaction": {
    "amount_usd": 1850.00,
    "vendor": "AWS",
    "category": "cloud-infrastructure",
    "memo": "Top up reserved instance budget for Q2",
    "rail": "stripe-issuing"
  },
  "policy": {
    "rule": "require_approval_above_usd",
    "threshold_usd": 1000,
    "daily_spent_usd": 2400,
    "daily_cap_usd": 5000
  },
  "callback_url": "https://api.paygraph.dev/v1/approvals/apr_01HF8K9X2N4P5Q6R7S8T9V0W1X",
  "signature": "t=1714316531,v1=5257a869e7..."
}

Three fields do most of the work. approval_id is the idempotency key. expires_at is the deadline after which the approval auto-denies. signature is an HMAC over the raw body so the receiver can verify the webhook came from your control plane.

How do you handle idempotency?

Webhooks fail. Networks blip, receivers 500, load balancers retry. Without idempotency, a single approval request can fire three times and produce three Slack messages, three pages, and one very confused approver.

The contract is simple. approval_id is unique per approval and stable across retries. The receiver stores the first one it sees and treats subsequent deliveries with the same ID as duplicates. The same ID is used when posting the decision back to the callback URL, so a double-click on the "approve" button doesn't release the payment twice.

FieldPurposeStable across retries?
approval_idIdempotency keyYes
created_atOriginal event timeYes
signatureHMAC over bodyYes (body is identical)
delivery_idPer-attempt IDNo — changes each retry
attemptRetry counterNo

Receivers should key on approval_id, not delivery_id. PayGraph also stamps every retry into the audit log so you can see exactly how many attempts each decision took.

What are the timeout and retry semantics?

Two timeouts matter, and they're different things. The HTTP delivery timeout governs how long the control plane waits for the receiver to acknowledge the webhook. The approval timeout governs how long the agent waits for a human decision before failing closed.

Sensible defaults:

  • HTTP delivery timeout: 5 seconds to acknowledge with a 2xx. The receiver acks fast, then does the slow work (posting to Slack, paging on-call) async.
  • Delivery retries: exponential backoff at 1s, 5s, 30s, 2m, 10m. After five failed attempts, mark the approval as undeliverable and fail the transaction closed.
  • Approval timeout: 15 minutes by default, configurable per policy rule. A $50 ad spend can wait 2 minutes; a $50,000 wire can wait an hour.
  • Decision retries: if posting the decision back to the callback URL fails, retry on the same schedule. The decision is the durable artifact, not the delivery.

The default failure mode is deny, never approve. An approval webhook that times out, fails to deliver, or returns an ambiguous response must result in a denied transaction. This is the same failure-closed principle that governs human-in-the-loop agent payments more broadly — when in doubt, the money does not move.

How do you implement it against Slack and a generic HTTP endpoint?

Slack is the most common target. The pattern: receive the webhook, verify the signature, post a message with approve/deny buttons to a channel, and use Slack's interactivity payload to call back to PayGraph.

from paygraph import PolicyEngine, Policy, ApprovalWebhook
from slack_sdk import WebClient
 
policy = Policy(
    require_approval_above_usd=1000,
    daily_cap_usd=5000,
)
 
slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
 
@ApprovalWebhook.handler(secret=os.environ["PAYGRAPH_WEBHOOK_SECRET"])
def on_approval_request(event):
    if event.is_duplicate(key=event.approval_id):
        return  # already posted
 
    slack.chat_postMessage(
        channel="#agent-approvals",
        text=f"Approval needed: ${event.transaction.amount_usd} to {event.transaction.vendor}",
        blocks=build_approval_blocks(event),
        metadata={"approval_id": event.approval_id},
    )

The handler verifies the HMAC signature, dedupes on approval_id, and posts to Slack. The button click handler calls event.approve() or event.deny(reason=...), which posts back to the callback URL.

A generic HTTP endpoint follows the same shape without Slack. Your finance dashboard, a custom approval queue, or PagerDuty all fit the same contract: receive, verify, dedupe, surface to a human, post the decision back.

A few rules earn their keep in production:

  1. Verify the signature on every request. A webhook receiver that skips signature verification is a backdoor into your payment rail.
  2. Acknowledge fast, work async. Return 200 immediately, then do the Slack post or the page from a background worker.
  3. Store the full payload. When finance asks why a $1,850 charge was approved at 2 AM, you want the original webhook body, not a summary.
  4. Fail closed on ambiguity. A 500 from your handler, a malformed decision, or a missing reason field — all deny.

The fourth rule is where most teams get bitten. Approval flows that fail open look fine in testing and disastrous in incident review. The same fail-closed default underpins how an agent's tool permissions are scoped at the layer below.

Where to start

  • GitHub: github.com/paygraph-ai/paygraph — MIT-licensed SDK with built-in approval webhook delivery, retries, and signature verification.
  • Docs: docs.paygraph.dev — full payload schema, signature format, Slack and PagerDuty reference handlers, retry configuration.
  • Discord: discord.gg/PPVZWSMdEm — ask the team about webhook patterns for your specific approval workflow.

If your agent can spend money and your approval flow is a Slack DM with no idempotency key, that's the next thing to fix. PayGraph ships the schema, the retries, and the audit trail in the box.