How the Autumn Webhook Handles Subscription Billing Events in Open-SEO
The Autumn webhook validates Svix-signed POST requests at /api/autumn/webhook, parses subscription billing events using a Zod schema, and synchronizes customer states via syncAutumnCustomerStatus, which upserts deterministic snapshots to the database and pushes updates to Loops CRM.
The Autumn webhook acts as the secure bridge between the Autumn subscription billing platform and the Open-SEO application state. Located in the every-app/open-seo repository, this endpoint ensures that every subscription billing event—whether plan changes, renewals, or cancellations—immediately reflects in the local database and downstream CRM systems.
Webhook Endpoint Configuration
The webhook is exposed at the path /api/autumn/webhook, exported as the constant AUTUMN_WEBHOOK_PATH from src/server/billing/autumn-webhook.ts. The route handler is registered to process all HTTP methods at this endpoint, though it strictly enforces POST-only access for security.
// Register the webhook route (simplified)
import { handleAutumnWebhookRequest } from "@/server/billing/autumn-webhook";
app.all(AUTUMN_WEBHOOK_PATH, (req) => handleAutumnWebhookRequest(req));
Request Validation and Security
The webhook implements multiple layers of validation to ensure request authenticity. It first rejects any non-POST requests with a 405 Method Not Allowed response. For valid POST requests, the raw request body is verified using Svix's HMAC signature mechanism with the secret stored in the AUTUMN_WEBHOOK_SECRET environment variable.
If signature verification fails, the webhook immediately returns a 401 Unauthorized status with a JSON error payload. This prevents attackers from injecting fake billing events into the system.
Payload Schema Validation
After security verification, the request body is parsed and validated against the autumnWebhookPayloadSchema defined using Zod. This schema expects an object containing a type string and an optional data object. Any malformed JSON or schema violations result in a 400 Bad Request response, ensuring only well-formed events enter the processing pipeline.
{
"type": "billing.updated",
"data": {
"customer_id": "org_12345",
"plan_id": "premium",
"status": "active"
}
}
Handling billing.updated Events
When the payload type equals billing.updated, the webhook extracts the customer_id from payload.data. If this identifier is missing, the endpoint returns a 400 error, as the customer context is required for all subsequent operations.
The core subscription billing event processing is delegated to the syncAutumnCustomerStatus function, imported from src/server/billing/customer-status-sync.ts. This function encapsulates the entire state synchronization logic, keeping the webhook handler thin and focused on HTTP concerns.
Synchronizing the Customer Billing Status
Inside syncAutumnCustomerStatus, the system first retrieves the current customer record from Autumn using the SDK's customers.getOrCreate method. The retrieved data is then transformed into a BillingCustomerStatusSnapshot via the deriveBillingCustomerStatusSnapshot helper.
This snapshot is upserted into the billingCustomerStatus database table using an idempotent ON CONFLICT DO UPDATE clause, ensuring that duplicate or out-of-order webhook deliveries never create inconsistent data. Finally, the snapshot is pushed to Loops for CRM-side bookkeeping, maintaining alignment between the billing system and marketing automation platform.
// Manually invoke the sync logic (useful for testing)
import { syncAutumnCustomerStatus } from "@/server/billing/customer-status-sync";
await syncAutumnCustomerStatus("org_12345");
Error Handling and Observability
All synchronous operations are wrapped in try-catch blocks to prevent unhandled exceptions from crashing the webhook server. When errors occur during customer status synchronization, the system logs the full error context—including the underlying database driver cause—and reports the failure to PostHog via the captureServerError utility.
The webhook responds with a 500 Internal Server Error status when processing fails, triggering Svix's automatic retry mechanism while alerting the development team through the observability pipeline.
Idempotency and Retry Behavior
The webhook is designed to be idempotent by nature. Because syncAutumnCustomerStatus computes deterministic snapshots and uses database upserts, re-playing or receiving out-of-order events simply re-applies the same customer state without side effects. No dedicated deduplication table is required, as the database constraints handle duplicate prevention implicitly.
When the webhook returns any non-2xx status code, Svix automatically retries delivery according to its exponential backoff schedule, ensuring eventual consistency even during temporary outages or deployment windows.
Summary
- The Autumn webhook at
/api/autumn/webhookaccepts only POST requests and validates Svix HMAC signatures usingAUTUMN_WEBHOOK_SECRET. - Incoming payloads are parsed against the
autumnWebhookPayloadSchemaZod validator, rejecting malformed data with 400 responses. - Subscription billing events of type
billing.updatedtriggersyncAutumnCustomerStatus, which fetches customer data via the Autumn SDK and derives aBillingCustomerStatusSnapshot. - Customer snapshots are upserted idempotently into the
billingCustomerStatustable and forwarded to Loops CRM for synchronization. - Errors are captured via
captureServerErrorto PostHog, with 500 responses triggering automatic Svix retries.
Frequently Asked Questions
How does the Autumn webhook verify the authenticity of incoming requests?
The webhook verifies requests using Svix's HMAC signature validation. It reads the raw request body and validates it against the secret stored in the AUTUMN_WEBHOOK_SECRET environment variable. Requests with invalid signatures receive a 401 Unauthorized response, ensuring only legitimate Autumn webhooks are processed.
What happens if the customer_id is missing from a billing.updated event?
If the payload.data object lacks a customer_id when processing a billing.updated event, the webhook immediately returns a 400 Bad Request error. This validation occurs in src/server/billing/autumn-webhook.ts before any database operations, preventing orphaned records or failed sync attempts.
Is the Autumn webhook idempotent, and how does it handle duplicate deliveries?
Yes, the webhook is idempotent by design. The syncAutumnCustomerStatus function uses deterministic snapshot generation and database upserts with ON CONFLICT DO UPDATE clauses. Duplicate or out-of-order webhook deliveries simply overwrite the existing record with the same computed state, eliminating the need for a separate deduplication table while maintaining data consistency.
Which database table stores the synchronized billing customer status?
The billing customer status is stored in the billingCustomerStatus table. This table receives upserts from the syncAutumnCustomerStatus function defined in src/server/billing/customer-status-sync.ts, ensuring the Open-SEO application always maintains the latest subscription state from Autumn.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →