How the Autumn Webhook Handler Reconciles Paid Invoice Events with OAuth Organization Boundaries
When Open SEO receives an Autumn billing webhook, it verifies the Svix signature, extracts the organization ID from the customer_id field, and performs an idempotent sync that updates the local billing state and records referral sales—all scoped strictly to the OAuth organization boundary without requiring additional authentication tokens.
The every-app/open-seo repository implements a deterministic reconciliation pipeline that maps external billing events to internal OAuth-scoped resources. This architecture treats the Autumn customer_id as the canonical organization identifier, ensuring that paid invoice events automatically align with the correct multi-tenant boundary.
Webhook Verification and Payload Validation
Every incoming POST request to the Autumn webhook endpoint undergoes strict verification before any business logic executes.
Svix Signature Verification
The handler rejects unauthorized requests at the network edge using verifySvixSignature. Located in src/server/billing/autumn-webhook.ts (lines 27-35), this function validates cryptographic signatures generated by Svix against a configured secret. Invalid signatures immediately return a 401 response, preventing spoofed events from reaching the reconciliation logic.
Zod Schema Validation
After signature verification, the raw payload undergoes structured validation using autumnWebhookPayloadSchema (lines 10-15). This permissive Zod schema ensures type safety while extracting the critical type field. The validation guarantees that downstream functions receive predictable data shapes even if Autumn introduces new webhook event types.
Mapping Autumn Customers to OAuth Organizations
The reconciliation hinges on a deliberate architectural decision: the Autumn customer_id is the Open SEO organization ID.
When processing billing.updated events, the handler calls getCustomerId (lines 86-89 in autumn-webhook.ts) to extract data.customer_id from the payload. This string value serves as both the external billing identifier and the internal OAuth organization boundary (context.organizationId). If the field is missing or empty, the function returns null and the handler responds with a 400 error, halting processing.
This 1-to-1 mapping eliminates the need for lookup tables or secondary queries to resolve organization scope. The webhook inherently knows which tenant's resources to update because the customer identifier carries the OAuth context.
Idempotent Reconciliation of Billing State
Once the organization ID is isolated, the system performs an atomic synchronization to ensure the local database reflects the source of truth in Autumn.
Synchronizing Customer Status
The syncAutumnCustomerStatus(customerId) function (implemented in src/server/billing/customer-status-sync.ts) retrieves the current customer state using autumn.customers.getOrCreate from the Autumn SDK. This fetch captures the latest subscription status, balance, and entitlements for that specific organization.
Deriving the Billing Snapshot
The raw Autumn customer object transforms into a BillingCustomerStatusSnapshot via deriveBillingCustomerStatusSnapshot. This normalized structure is then upserted into the billingCustomerStatus database table. Because the operation uses an upsert pattern keyed by the organization ID, repeated webhook deliveries or out-of-order processing result in eventual consistency without duplicate records.
After the database commit, syncBillingStatusToLoops(snapshot) propagates the updated status to the Loops marketing platform, ensuring external analytics remain synchronized with the organization's current billing tier.
Recording Paid Invoices as Dub Sales
The final reconciliation step connects billing events to the referral tracking system. The handler invokes trackDubSalesForOrganization(customerId) (sourced from src/server/referrals/dub-sale.ts), which converts paid Autumn invoices into Dub /track/sale events.
This routine uses the organization ID as the unique key (organizationId) when emitting sale events. Because the organization ID and Autumn customer ID are identical, the conversion automatically respects OAuth boundaries—revenue attribution occurs strictly within the organization that generated the payment. The implementation relies on the referral pin stored via src/server/referrals/dub.ts during initial account creation.
Why This Architecture Preserves OAuth Boundaries
Three design principles ensure the webhook handler maintains security and consistency across multi-tenant boundaries:
-
Organization-centric identifier – By mandating that Autumn’s
customer_idmatch the Open SEO organization ID, the system creates an immutable link between external billing records and internal OAuth scopes. No user-specific authentication tokens are required because the organization ID itself enforces the boundary. -
Idempotent sync – The
syncAutumnCustomerStatusroutine overwrites existing database rows for the organization, making the handler safe for retries and duplicate deliveries. This guarantees that the local state eventually converges with Autumn's records regardless of network latency or webhook redelivery. -
Zero user context – Webhooks carry no authenticated user session; they carry only the organization identifier. This design ensures that billing updates affect only the organization's resources (subscription status, referral credits) without risking cross-tenant data leakage.
Summary
- The Autumn webhook handler in
src/server/billing/autumn-webhook.tsverifies Svix signatures and validates payloads using Zod schemas before processing. - The
customer_idfield in webhook payloads directly maps to the Open SEO organization ID, serving as the OAuth boundary key. syncAutumnCustomerStatusperforms idempotent upserts to thebillingCustomerStatustable, ensuring local data reflects the true Autumn subscription state.trackDubSalesForOrganizationconverts paid invoices into Dub referral events scoped to the organization ID.- This architecture reconciles external billing events with internal OAuth boundaries without requiring per-user tokens or complex lookup logic.
Frequently Asked Questions
How does the webhook verify it came from Autumn?
The handler uses verifySvixSignature in src/server/billing/autumn-webhook.ts to cryptographically validate the Svix signature headers against a configured secret. Invalid signatures trigger an immediate 401 response, rejecting the request before any payload parsing occurs.
What happens if the customer_id is missing from the payload?
The getCustomerId helper returns null when data.customer_id is absent or empty. The webhook handler checks this return value and responds with a 400 status code and an error message, preventing unscoped billing events from processing.
Is the synchronization idempotent?
Yes. The syncAutumnCustomerStatus function upserts records into the billingCustomerStatus table using the organization ID as the primary key. Repeated webhook deliveries simply overwrite the existing row with the latest Autumn customer data, ensuring eventual consistency without creating duplicates.
Why doesn't the webhook need an OAuth token?
The webhook carries the organization identifier (customer_id) in its signed payload. Because this ID is cryptographically tied to the OAuth scope through the 1-to-1 mapping with context.organizationId, and because the Svix signature proves the message originated from Autumn, no additional bearer token is required. The organization boundary is enforced by the identifier itself and the webhook's authenticity.
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 →