Skip to main content

Overview

Autumn sends webhooks to notify your application of important events like:
  • Subscription changes (new, upgrade, downgrade, cancel)
  • Usage threshold alerts
  • Payment events
  • Balance updates
Webhooks are powered by Svix and include built-in signature verification, automatic retries, and a testing playground.

Setup

1. Create a Webhook Endpoint

Create an endpoint in your application to receive webhooks:
// app/api/webhooks/autumn/route.ts
import { headers } from 'next/headers';
import { Webhook } from 'svix';

export async function POST(req: Request) {
  const payload = await req.text();
  const headersList = headers();
  
  const svixId = headersList.get('svix-id');
  const svixTimestamp = headersList.get('svix-timestamp');
  const svixSignature = headersList.get('svix-signature');
  
  // Verify webhook signature
  const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);
  
  let event;
  try {
    event = wh.verify(payload, {
      'svix-id': svixId!,
      'svix-timestamp': svixTimestamp!,
      'svix-signature': svixSignature!,
    });
  } catch (err) {
    console.error('Webhook verification failed:', err);
    return new Response('Invalid signature', { status: 400 });
  }
  
  // Handle the event
  switch (event.type) {
    case 'customer.products.updated':
      await handleProductUpdate(event.data);
      break;
    case 'customer.threshold_reached':
      await handleThresholdReached(event.data);
      break;
  }
  
  return new Response('Webhook processed', { status: 200 });
}

2. Configure Webhook in Dashboard

  1. Go to the Autumn dashboard → SettingsWebhooks
  2. Click Add Endpoint
  3. Enter your webhook URL (e.g., https://api.yourapp.com/webhooks/autumn)
  4. Select which events to receive
  5. Save and copy the Signing Secret to your environment variables
For local development, use tools like ngrok or Svix Play to expose your local server.

Event Types

customer.products.updated

Sent when a customer’s product subscription changes. Scenarios:
  • new - Customer subscribed to a new product
  • upgrade - Customer upgraded to a higher tier
  • downgrade - Customer downgraded to a lower tier
  • cancel - Customer canceled their subscription
Payload:
{
  type: "customer.products.updated",
  data: {
    scenario: "new" | "upgrade" | "downgrade" | "cancel",
    customer: {
      id: string,
      email: string,
      name: string,
      // ... full customer object
    },
    updated_product: {
      id: string,
      name: string,
      status: "active" | "scheduled" | "canceling",
      // ... full product object
    },
    entity?: {
      id: string,
      name: string
    }
  }
}
Example Handler:
async function handleProductUpdate(data: CustomerProductsUpdatedData) {
  const { scenario, customer, updated_product } = data;
  
  switch (scenario) {
    case 'new':
      // Customer subscribed to new product
      await sendWelcomeEmail(customer.email, updated_product.name);
      await syncToDatabase({ customerId: customer.id, product: updated_product });
      break;
      
    case 'upgrade':
      // Customer upgraded
      await sendUpgradeEmail(customer.email, updated_product.name);
      await unlockPremiumFeatures(customer.id);
      break;
      
    case 'downgrade':
      // Customer downgraded
      await sendDowngradeEmail(customer.email, updated_product.name);
      await restrictFeatures(customer.id, updated_product);
      break;
      
    case 'cancel':
      // Customer canceled
      await sendCancellationEmail(customer.email);
      await scheduleDataRetention(customer.id);
      break;
  }
}

customer.threshold_reached

Sent when a customer reaches a usage threshold (e.g., 80% of their limit). Payload:
{
  type: "customer.threshold_reached",
  data: {
    customer: {
      id: string,
      email: string,
      name: string
    },
    feature: {
      id: string,
      name: string
    },
    threshold: number,        // Threshold percentage (e.g., 80)
    usage: number,            // Current usage
    limit: number,            // Total limit
    remaining: number         // Amount remaining
  }
}
Example Handler:
async function handleThresholdReached(data: ThresholdReachedData) {
  const { customer, feature, threshold, usage, limit, remaining } = data;
  
  // Send warning email
  await sendEmail({
    to: customer.email,
    subject: `${feature.name} usage at ${threshold}%`,
    body: `
      You've used ${usage} of ${limit} ${feature.name}.
      ${remaining} remaining.
      
      Upgrade now to get more: https://yourapp.com/upgrade
    `
  });
  
  // Send in-app notification
  await createNotification({
    customerId: customer.id,
    title: `${feature.name} limit approaching`,
    message: `${remaining} ${feature.name} remaining`,
    action: { label: "Upgrade", url: "/upgrade" }
  });
}

Security

Verify Webhook Signatures

Always verify webhook signatures to ensure requests are from Autumn:
import { Webhook } from 'svix';

const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);

try {
  const event = wh.verify(payload, headers);
  // Event is verified, safe to process
} catch (err) {
  // Invalid signature - reject the request
  throw new Error('Invalid webhook signature');
}

Use HTTPS

Webhook endpoints must use HTTPS in production. Svix will reject HTTP endpoints.

Implement Idempotency

Webhooks may be delivered more than once. Use the svix-id header as an idempotency key:
const eventId = headers['svix-id'];

// Check if already processed
if (await hasProcessedEvent(eventId)) {
  return new Response('Already processed', { status: 200 });
}

// Process event
await handleEvent(event);

// Mark as processed
await markEventProcessed(eventId);

Testing

Local Testing with Svix Play

  1. Go to Svix Play
  2. Generate a webhook URL
  3. Add it as a webhook endpoint in Autumn dashboard
  4. Trigger events (attach, track, etc.) and see them arrive in real-time

Send Test Events

Use the Autumn dashboard to send test webhook events:
  1. Go to SettingsWebhooks
  2. Click on your endpoint
  3. Click Send Example and select an event type

CLI Testing

# Install Svix CLI
npm install -g svix-cli

# Listen for webhooks locally
svix listen http://localhost:3000/webhooks/autumn

Retries and Reliability

Autumn automatically retries failed webhook deliveries:
  • Retry Schedule: Exponential backoff (1min, 5min, 30min, 2hr, 5hr, 10hr, 10hr)
  • Success Criteria: HTTP 200-299 response
  • Timeout: 15 seconds per attempt
  • Total Attempts: Up to 7 retries over ~24 hours

Return Proper Status Codes

// ✅ Success - webhook processed
return new Response('OK', { status: 200 });

// ✅ Will retry - temporary error
return new Response('Database unavailable', { status: 503 });

// ❌ Won't retry - permanent error
return new Response('Invalid event type', { status: 400 });

Debugging

View Webhook Logs

  1. Go to SettingsWebhooks in Autumn dashboard
  2. Click on your endpoint
  3. View delivery attempts, status codes, and response bodies

Common Issues

  • Check that you’re using the correct webhook secret
  • Ensure you’re passing the raw request body (not parsed JSON)
  • Verify all three Svix headers are present: svix-id, svix-timestamp, svix-signature
  • Verify your endpoint is publicly accessible (use ngrok for local dev)
  • Check that the endpoint uses HTTPS (required in production)
  • Ensure you’ve selected the correct events in webhook configuration
  • Check webhook logs in dashboard for delivery failures
  • This is normal behavior - implement idempotency using svix-id header
  • Check your handler is returning 200 on success

Best Practices

1

Process webhooks asynchronously

Return 200 immediately and process events in a background job:
app.post('/webhooks/autumn', async (req, res) => {
  const event = await verifyWebhook(req);
  
  // Queue for processing
  await queue.add('process-webhook', event);
  
  // Return immediately
  res.status(200).send('Queued');
});
2

Implement idempotency

Use svix-id to prevent duplicate processing:
const eventId = headers['svix-id'];
if (await cache.get(eventId)) {
  return res.status(200).send('Already processed');
}
await processEvent(event);
await cache.set(eventId, true, 86400); // 24hr TTL
3

Monitor webhook health

Track webhook failures and alert on repeated errors:
if (failureCount > 3) {
  await alertOncall('Webhook failures detected');
}
4

Version your webhook handlers

Plan for schema changes by versioning handlers:
const handlers = {
  'customer.products.updated': {
    v1: handleProductUpdateV1,
    v2: handleProductUpdateV2,
  }
};

Next Steps

API Reference

Explore all available endpoints

SDK Documentation

Learn how to use Autumn SDKs

Build docs developers (and LLMs) love