Webhooks

Webhooks let you receive real-time notifications when events occur in AutopayOS. Use them to update your systems, trigger workflows, or alert users.

Overview

Diagram
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  AutopayOS   │────▶│   Webhook    │────▶│ Your Server  │
│   Gateway    │     │   Delivery   │     │              │
└──────────────┘     └──────────────┘     └──────────────┘

When an event occurs (payment authorized, intent denied, etc.), AutopayOS sends an HTTP POST request to your configured endpoint.

Configure webhooks

Dashboard

  1. Go to Dashboard → Settings → Webhooks
  2. Click Add Endpoint
  3. Enter your endpoint URL
  4. Select events to subscribe to
  5. Copy the signing secret

API

Bash
curl https://api.autopayos.com/ap2/admin/webhooks \
  -H "Authorization: Bearer $AUTOPAYOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/autopayos",
    "events": [
      "payment.authorized",
      "payment.captured",
      "payment.failed",
      "intent.denied"
    ],
    "secret": "whsec_your_secret"
  }'

Event types

Payment events

EventDescription
payment.authorizedPayment was authorized by rail
payment.capturedFunds were captured
payment.failedPayment failed
payment.refundedPayment was refunded
payment.disputedChargeback initiated

Intent events

EventDescription
intent.issuedIntent was approved
intent.deniedIntent was rejected by policy
intent.expiredIntent exceeded time limit

Cart events

EventDescription
cart.validatedCart verification passed
cart.rejectedCart verification failed

Agent events

EventDescription
agent.createdNew agent registered
agent.revokedAgent was revoked

Policy events

EventDescription
policy.updatedPolicy was modified
policy.violationPolicy violation detected

Webhook payload

All webhooks follow this structure:

JSON
{
  "id": "evt_abc123xyz",
  "type": "payment.authorized",
  "created": "2025-12-17T10:00:00Z",
  "data": {
    "mandateId": "pm_xyz789",
    "amount": 38.85,
    "currency": "USD",
    "merchant": "amazon.com",
    "rail": "STRIPE",
    "stripePaymentIntentId": "pi_3abc123..."
  },
  "context": {
    "agentDid": "did:key:z6Mk...",
    "principalDid": "did:key:z6Mp...",
    "intentId": "intent_abc123"
  }
}
FieldDescription
idUnique event identifier
typeEvent type
createdISO 8601 timestamp
dataEvent-specific payload
contextRelated identifiers

Verify signatures

Always verify webhook signatures to ensure authenticity:

<tabs> <tab title="TypeScript">
TypeScript
import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expectedSignature}`)
  );
}

// Express example
app.post('/webhooks/autopayos', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-autopayos-signature'] as string;
  const payload = req.body.toString();

  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  
  // Process event
  handleWebhookEvent(event);

  res.status(200).send('OK');
});
</tab> <tab title="Python">
Python
import hmac
import hashlib
from flask import Flask, request

app = Flask(__name__)

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, f"sha256={expected}")

@app.route('/webhooks/autopayos', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Autopayos-Signature')
    payload = request.get_data()
    
    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401
    
    event = request.get_json()
    
    # Process event
    handle_webhook_event(event)
    
    return 'OK', 200
</tab> </tabs>

Handle events

Payment authorized

TypeScript
async function handlePaymentAuthorized(event: WebhookEvent) {
  const { mandateId, amount, merchant } = event.data;

  // Update order status
  await db.orders.update({
    where: { mandateId },
    data: { status: 'AUTHORIZED' },
  });

  // Notify user
  await sendNotification(event.context.principalDid, {
    title: 'Payment Authorized',
    body: `$${amount} payment to ${merchant} was authorized`,
  });
}

Payment captured

TypeScript
async function handlePaymentCaptured(event: WebhookEvent) {
  const { mandateId, amount } = event.data;

  await db.orders.update({
    where: { mandateId },
    data: {
      status: 'PAID',
      paidAt: new Date(),
    },
  });

  // Trigger fulfillment
  await fulfillmentService.startFulfillment(mandateId);
}

Payment failed

TypeScript
async function handlePaymentFailed(event: WebhookEvent) {
  const { mandateId, error } = event.data;

  await db.orders.update({
    where: { mandateId },
    data: {
      status: 'FAILED',
      failureReason: error.code,
    },
  });

  // Alert user
  await sendNotification(event.context.principalDid, {
    title: 'Payment Failed',
    body: `Your payment could not be processed: ${error.message}`,
  });
}

Intent denied

TypeScript
async function handleIntentDenied(event: WebhookEvent) {
  const { reasonCodes, request } = event.data;

  // Log for analytics
  await analytics.track('intent_denied', {
    agentDid: event.context.agentDid,
    reasonCodes,
    amount: request.maxAmount,
    merchant: request.vendorHints?.domain,
  });

  // Alert if anomaly
  if (reasonCodes.includes('ANOMALY_DETECTED')) {
    await alertSecurityTeam(event);
  }
}

Policy violation

TypeScript
async function handlePolicyViolation(event: WebhookEvent) {
  const { violationType, details } = event.data;

  await db.alerts.create({
    data: {
      type: 'POLICY_VIOLATION',
      agentDid: event.context.agentDid,
      details: JSON.stringify(details),
    },
  });

  // Review high-severity violations
  if (violationType === 'VELOCITY_BREACH') {
    await scheduleAgentReview(event.context.agentDid);
  }
}

Retry behavior

AutopayOS retries failed webhook deliveries:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
624 hours

After 6 failed attempts, the webhook is marked as failed and no further retries occur.

Successful delivery

A webhook is considered successful when your endpoint returns:

  • HTTP 200-299 status code
  • Within 30 seconds

Failed delivery

A webhook fails if:

  • HTTP 4xx or 5xx status code
  • Connection timeout (30 seconds)
  • SSL/TLS error
  • DNS resolution failure

Idempotency

Webhooks may be delivered multiple times. Use the event id for idempotency:

TypeScript
async function handleWebhookEvent(event: WebhookEvent) {
  // Check if already processed
  const existing = await db.processedEvents.findUnique({
    where: { eventId: event.id },
  });

  if (existing) {
    console.log('Event already processed, skipping');
    return;
  }

  // Process event
  await processEvent(event);

  // Mark as processed
  await db.processedEvents.create({
    data: { eventId: event.id, processedAt: new Date() },
  });
}

Testing webhooks

Stripe CLI style testing

Use the AutopayOS CLI to forward webhooks locally:

Bash
# Install CLI
npm install -g @autopayos/cli

# Forward webhooks to local server
autopayos webhooks listen --forward-to localhost:3000/webhooks/autopayos

Trigger test events

Bash
# Trigger a test event
autopayos webhooks trigger payment.authorized

# With custom data
autopayos webhooks trigger payment.authorized \
  --data '{"amount": 100, "currency": "USD"}'

Webhook logs

View recent webhook deliveries:

Bash
curl "https://api.autopayos.com/ap2/admin/webhooks/logs?limit=20" \
  -H "Authorization: Bearer $AUTOPAYOS_API_KEY"

Response:

JSON
{
  "logs": [
    {
      "id": "whl_abc123",
      "eventType": "payment.authorized",
      "url": "https://your-server.com/webhooks/autopayos",
      "status": 200,
      "duration": 145,
      "deliveredAt": "2025-12-17T10:00:00Z"
    }
  ]
}

Best practices

1. Respond quickly

Return 200 immediately, process asynchronously:

TypeScript
app.post('/webhooks/autopayos', async (req, res) => {
  // Verify signature
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  // Queue for processing
  await queue.add('webhook', req.body);

  // Return immediately
  res.status(200).send('OK');
});

// Process asynchronously
queue.process('webhook', async (job) => {
  await handleWebhookEvent(job.data);
});

2. Handle duplicates

TypeScript
const processedEvents = new Set<string>();

function handleEvent(event: WebhookEvent) {
  if (processedEvents.has(event.id)) {
    return; // Already processed
  }
  
  // Process...
  
  processedEvents.add(event.id);
}

3. Verify signatures

Always verify the X-Autopayos-Signature header.

4. Use HTTPS

Only use HTTPS endpoints in production.

5. Monitor delivery

Set up alerts for failed webhook deliveries.

Event reference

payment.authorized

JSON
{
  "type": "payment.authorized",
  "data": {
    "mandateId": "pm_xyz789",
    "intentId": "intent_abc123",
    "amount": 38.85,
    "currency": "USD",
    "merchant": "amazon.com",
    "rail": "STRIPE",
    "railTransactionId": "pi_3abc123..."
  }
}

payment.captured

JSON
{
  "type": "payment.captured",
  "data": {
    "mandateId": "pm_xyz789",
    "amount": 38.85,
    "currency": "USD",
    "capturedAt": "2025-12-17T10:00:15Z"
  }
}

intent.denied

JSON
{
  "type": "intent.denied",
  "data": {
    "mandateId": "intent_abc123",
    "reasonCodes": ["AMOUNT_OVER_CAP", "MERCHANT_NOT_ALLOWED"],
    "request": {
      "maxAmount": 500.00,
      "merchantDomain": "casino.com"
    },
    "policyId": "pol_xyz"
  }
}

Next steps