Amba

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

POST /webhooks/revenuecat?project_id=<uuid>
Authorization: Bearer <webhook_secret>
Content-Type: application/json
PieceWhereDescription
project_idquery stringThe Amba project the event belongs to.
Authorization: Bearer <secret>headerMust exactly equal the webhook_secret configured in the project's revenuecat integration. Constant-time compared.
JSON bodybodyRevenueCat's standard WebhookEvent payload. Forwarded unmodified to the Temporal workflow.

Response 200

{ "data": { "received": true } }

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_PROJECTproject_id query param missing.
  • 401 INVALID_SIGNATUREAuthorization header missing or wrong secret.
  • 404 NOT_CONFIGURED — project has no active revenuecat integration or no webhook_secret on 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:

# Called by RevenueCat — you don't call this directly.
curl -X POST '${BASE_URL}/webhooks/revenuecat' \
  -H 'Authorization: Bearer ${WEBHOOK_SECRET}' \
  -H 'Content-Type: application/json' \
  -d '{}'

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

POST /webhooks/superwall?project_id=<uuid>
x-superwall-signature: <hex-hmac-sha256-of-raw-body>
Content-Type: application/json
PieceWhereDescription
project_idquery stringAmba project.
x-superwall-signatureheaderHex-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 bodybodySuperwall 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

{ "data": { "received": true } }

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_PROJECTproject_id missing.
  • 400 INVALID_BODY — body is not valid JSON.
  • 401 INVALID_SIGNATUREx-superwall-signature missing or doesn't match the HMAC of the raw body.
  • 404 NOT_CONFIGURED — project has no active superwall integration or no webhook_secret.
  • 429 — rate-limited.

Called by Superwall — you can't invoke this directly. The curl below shows the expected shape for local testing.

Curl:

# Called by Superwall — you don't call this directly.
curl -X POST '${BASE_URL}/webhooks/superwall' \
  -H 'x-superwall-signature: ${HMAC_SHA256_HEX}' \
  -H 'Content-Type: application/json' \
  -d '{}'

Configuring the secret

Both providers take their shared secret from the project's project_integrations row:

SELECT config FROM project_integrations
 WHERE project_id = '<uuid>'
   AND provider   IN ('revenuecat', 'superwall')
   AND is_active  = true;

Use POST /admin/projects/:projectId/integrations to create or update the row. The webhook_secret lives inside config.webhook_secret.

On this page