Webhook Integration Guide

Receive real-time notifications when governance events happen — transaction approvals, rejections, and budget threshold alerts. All webhooks are HMAC-SHA256 signed for verification.

Why Use Webhooks?

Without webhooks, you'd have to poll the API or check the dashboard manually. Webhooks push events to your server in real-time, enabling:

  • Slack/Discord alerts when agents spend money or get rejected
  • Proactive budget management — top up before agents get blocked
  • Audit logging to your own systems
  • Automated workflows triggered by spending events

Event Types

EventFires WhenUse Case
transaction.approvedA spend is approvedLog to Slack, update internal dashboards, trigger workflows
transaction.rejectedA spend is rejectedAlert on anomalous behavior, debug mandate rules
mandate.budget.warningBudget usage reaches 80%Notify team to top up before agents are blocked
mandate.budget.exhaustedBudget usage reaches 100%Know immediately when an agent can no longer spend

Events fire on all payment paths/evaluate, x402, ACP, and Stripe CC.

Setup

Option A: Dashboard

  1. Go to app.quetra.dev SettingsWebhooks
  2. Click Configure and enter your endpoint URL
  3. Click Save — events will start firing immediately

Option B: REST API

curl -X PATCH https://gateway.quetra.dev/api/v1/organization \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl": "https://your-app.com/webhooks/quetra"}'

Signing & Verification

Every webhook includes an X-Quetra-Signature header containing an HMAC-SHA256 hash of the request body, signed with your webhook secret. Always verify this server-side to ensure the payload is genuinely from QuetraAI.

Generate a Signing Secret

Via dashboard: SettingsWebhooks Rotate Secret. Or via API:

curl -X POST https://gateway.quetra.dev/api/v1/organization/webhook-secret/rotate \
  -H "Authorization: Bearer sk_your_api_key"

Verify in Your Server

import { createHmac } from "crypto";

function verifyWebhook(
  body: string,
  signature: string,
  secret: string
): boolean {
  const expected = createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return signature === expected;
}

// In your webhook handler:
app.post("/webhooks/quetra", (req, res) => {
  const signature = req.headers["x-quetra-signature"];
  const body = JSON.stringify(req.body);

  if (!verifyWebhook(body, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  // Process the event
  const { event, data } = req.body;
  console.log(`${event}: agent ${data.agentId} spent ${data.amount}`);

  res.status(200).send("OK");
});

Example Payload

{
  "event": "transaction.approved",
  "timestamp": "2026-04-01T12:00:00Z",
  "data": {
    "transactionId": "tx_abc123",
    "agentId": "a1b2c3d4-...",
    "mandateId": "m5e6f7g8-...",
    "amount": 500,
    "vendor": "api.example.com",
    "category": "research",
    "remainingBudget": 4500,
    "decision": "approved"
  }
}

Budget Alert Payloads

// mandate.budget.warning (≥80%)
{
  "event": "mandate.budget.warning",
  "timestamp": "2026-04-01T12:05:00Z",
  "data": {
    "mandateId": "m5e6f7g8-...",
    "agentId": "a1b2c3d4-...",
    "budgetTotal": 5000,
    "budgetSpent": 4200,
    "percentUsed": 84
  }
}

// mandate.budget.exhausted (≥100%)
{
  "event": "mandate.budget.exhausted",
  "timestamp": "2026-04-01T12:10:00Z",
  "data": {
    "mandateId": "m5e6f7g8-...",
    "agentId": "a1b2c3d4-...",
    "budgetTotal": 5000,
    "budgetSpent": 5000,
    "percentUsed": 100
  }
}

Delivery & Retries

  • Delivery is asynchronous via Cloudflare Workers waitUntil() — it never blocks the evaluation response
  • 4 retry attempts with exponential backoff on failure
  • 5-second timeout per delivery attempt
  • All delivery attempts (success and failure) are logged in the Webhook Delivery Log on the Settings page
  • Your endpoint must return 2xx to acknowledge receipt

Troubleshooting

Not receiving events

  • Verify webhook URL is saved in Settings
  • Check your endpoint is publicly accessible (not localhost)
  • Review the Webhook Delivery Log for failed attempts

Signature verification failing

  • Ensure you're hashing the raw request body (not a parsed/re-serialized version)
  • Verify the secret matches what was returned from the rotate endpoint
  • Check that you haven't rotated the secret since the event was sent

Related