Amba

Webhooks

How inbound webhooks authenticate — RevenueCat Bearer token, Superwall HMAC-SHA256, and the generic retry contract.

Amba accepts inbound webhooks from third-party platforms on /webhooks/:provider?project_id=.... Every endpoint verifies authenticity before parsing JSON, using timingSafeEqual so secret length cannot leak via timing side-channels. Unverified requests get 401.

Today there are two inbound webhook providers: RevenueCat (subscription events) and Superwall (paywall events). Each has its own auth mechanism mirroring the provider's wire format.

RevenueCat

RevenueCat authenticates webhooks with a bearer token you choose at integration-configure time.

Endpoint

POST https://api.amba.dev/webhooks/revenuecat?project_id=proj_xxx
Authorization: Bearer <webhook_secret>

The secret is what you passed as config.webhook_secret to POST /admin/integrations.

Verification (server-side)

const authHeader = c.req.header('Authorization') ?? '';
const expectedHeader = `Bearer ${expectedToken}`;
const a = Buffer.from(authHeader);
const b = Buffer.from(expectedHeader);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
  return c.json({ error: { code: 'INVALID_SIGNATURE', ... } }, 401);
}

Retry contract

On success Amba returns 200 { "data": { "received": true } }. If background processing can't accept the event, the endpoint returns 500 so RevenueCat retries per its webhook contract. Swallowing the failure as a 200 would cause silent event loss, so Amba deliberately surfaces the problem.

Superwall

Superwall signs the raw body with HMAC-SHA256 using a shared secret, and sends the hex digest in x-superwall-signature.

Endpoint

POST https://api.amba.dev/webhooks/superwall?project_id=proj_xxx
x-superwall-signature: <hex_digest>
Content-Type: application/json

Verification (server-side)

const rawBody = await c.req.text();
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const provided = Buffer.from(signatureHeader);
if (provided.length !== expected.length || !timingSafeEqual(provided, Buffer.from(expected))) {
  return c.json({ error: { code: 'INVALID_SIGNATURE', ... } }, 401);
}
// Parse JSON AFTER verification.
const body = JSON.parse(rawBody);

Reading the raw body before JSON parsing is critical — HMAC validates the bytes on the wire, not a re-serialized object. Parsing first would produce a different digest and invalidate every request.

Engagement mirror

Superwall events that include user_id + event are mirrored into your project's engagement_events table as superwall_<event> so paywall interactions feed into streaks, XP rules, and segment membership alongside first-party events.

Rate limits

Each provider has a per-project, per-second rate limit (bucket keyed by project_id query param):

ProviderLimit
RevenueCat100/sec
Superwall100/sec

Bursts above the limit return 429. Providers batch events before delivery, so sustained load above this ceiling is more likely replay / abuse than normal traffic.

Error codes

CodeHTTPMeaning
MISSING_PROJECT400project_id query param was not provided
NOT_CONFIGURED404No active integration row for this project
INVALID_SIGNATURE401Header missing or failed timingSafeEqual
INVALID_BODY400Malformed JSON (after signature verified)
WEBHOOK_PROCESSING_FAILED500Background processing could not accept the event (retry-safe for RC)

Configuration

Both providers are configured identically, via the admin API:

POST /admin/integrations
 
{
  "provider": "revenuecat",   // or "superwall"
  "config": { "webhook_secret": "<your_secret_here>" }
}

The API responds with a webhook_url containing the exact URL to register in the provider dashboard. Push providers (apns, fcm) don't have webhooks and get no webhook_url — they only send.

Rotating a secret

PATCH /admin/integrations/revenuecat
 
{ "config": { "webhook_secret": "<new_secret>" } }

The new secret takes effect immediately. Update the provider's webhook setting to match before rotating — mismatched secrets produce 401 INVALID_SIGNATURE until both sides agree.

Outbound webhooks

There is no first-class outbound webhook feature yet. Applications that need to dispatch webhooks from Amba events should subscribe to engagement events via a server-side consumer and dispatch themselves.

Next

On this page