Webhooks
Provider-verified callbacks for RevenueCat subscription events and Superwall paywall events.
Webhooks live at the top level (not under /client or /admin) because they're authenticated by provider-specific signatures, not by API keys or developer tokens. The project_id a given payload belongs to is supplied as a query-string parameter.
Both providers:
- Verify signatures with
timingSafeEqual(no length-based timing leaks). - Require the integration to be active in
project_integrations(provider +webhook_secret). - Rate-limit per project: 100 requests / second (bucket key =
project_id).
Source: apps/api/src/routes/webhooks/revenuecat.ts, apps/api/src/routes/webhooks/superwall.ts.
POST /webhooks/revenuecat
Receive and process a RevenueCat subscription event (initial purchase, renewal, cancellation, billing issue, etc.). Processing happens asynchronously via Temporal — the handler verifies the request, kicks off REVENUECAT_WEBHOOK, and returns.
Request
| Piece | Where | Description |
|---|---|---|
project_id | query string | The Amba project the event belongs to. |
Authorization: Bearer <secret> | header | Must exactly equal the webhook_secret configured in the project's revenuecat integration. Constant-time compared. |
| JSON body | body | RevenueCat's standard WebhookEvent payload. Forwarded unmodified to the Temporal workflow. |
Response 200
Retry semantics
RevenueCat treats any 2xx as "delivered" and never retries. Any 5xx triggers retries on RevenueCat's side. We return 500 if the Temporal workflow refuses to start so the event isn't silently dropped.
Errors
400 MISSING_PROJECT—project_idquery param missing.401 INVALID_SIGNATURE—Authorizationheader missing or wrong secret.404 NOT_CONFIGURED— project has no activerevenuecatintegration or nowebhook_secreton file.429— rate-limited (100/sec per project).500 WEBHOOK_PROCESSING_FAILED— Temporal workflow start failed. RevenueCat will retry.
Called by RevenueCat — you can't invoke this directly. The curl below shows the expected shape for local testing.
Curl:
POST /webhooks/superwall
Receive a Superwall paywall event. If the payload includes event + user_id, the handler writes a single engagement_events row (event_name = "superwall_<event>", full payload stored in properties). No Temporal workflow is started.
Request
| Piece | Where | Description |
|---|---|---|
project_id | query string | Amba project. |
x-superwall-signature | header | Hex-encoded HMAC-SHA256 of the raw request body using the project's webhook_secret. Verified BEFORE JSON parsing so malformed/forged payloads never reach any side effects. |
| JSON body | body | Superwall event object. Minimum fields the handler reads: event (string), user_id (app user id). Any other fields are stored verbatim in engagement_events.properties. |
Response 200
Retry semantics
Superwall retries on non-2xx. The handler returns 200 even if the engagement-event INSERT fails (non-fatal, logged) to avoid replay loops for write-level issues.
Errors
400 MISSING_PROJECT—project_idmissing.400 INVALID_BODY— body is not valid JSON.401 INVALID_SIGNATURE—x-superwall-signaturemissing or doesn't match the HMAC of the raw body.404 NOT_CONFIGURED— project has no activesuperwallintegration or nowebhook_secret.429— rate-limited.
Called by Superwall — you can't invoke this directly. The curl below shows the expected shape for local testing.
Curl:
Configuring the secret
Both providers take their shared secret from the project's project_integrations row:
Use POST /admin/projects/:projectId/integrations to create or update the row. The webhook_secret lives inside config.webhook_secret.