POST /webhooks/encore is the server-to-server endpoint for forwarding your subscription
lifecycle events into Encore. Encore joins each event back to the web exposure it recorded
for the same user, which is what powers NCL (incrementality) measurement.
This page is the contract: endpoint, authentication, signing, payload schema, responses,
and idempotency. For the end-to-end Stripe walkthrough, see the
Subscription Webhook guide.
When to use this
Use this if you bill your own subscriptions (for example, Stripe) and want those events in Encore for incrementality measurement. Forward each subscription event from your server as it happens. If your subscriptions run through a source that already notifies us — RevenueCat, Superwall, or App Store / Play server notifications — point those at their dedicated receivers instead; do not also send them here.Endpoint
Authentication
Requests use the platform’s standard server-to-server HMAC. Send three headers:Your app’s publishable key (
pk_live_... or pk_test_...). Resolves which app the event
belongs to.HMAC-SHA256 over the composite signing string (below), keyed by your app’s secret key
(
sk_...), hex-encoded.Unix timestamp in seconds. Must be within ±300 seconds (5 minutes) of Encore’s
server time or the request is rejected with
401. Use the same value here that you sign.Signing a request
The signed string is not the body alone — it is the composite:<unix_timestamp>is the exact value you send inX-Timestamp.- The method is the literal
POST. - The path is the literal
/webhooks/encore— note this is the path the API serves internally, without the/encoreprefix that appears in the public URL. Sign/webhooks/encore, even though you POST to…/encore/webhooks/encore. <raw_body>is the exact bytes of the JSON body you transmit. Serialize once, then sign and send the identical bytes — do not re-serialize after signing (property reordering or whitespace changes will break the signature).
buildEncoreHeaders helper is the one signing implementation referenced everywhere in
these docs:
Payload
The request body is JSON.user.app_account_id, subscription.original_transaction_id,
event_id, and event_type are required on every event.
Unique id for this event. Encore uses it as the idempotency / dedup key, so retrying
with the same
event_id collapses to a single event — safe to retry.The Encore-standard verb describing what happened. One of:
did_subscribe, did_renew, did_cancel, did_resubscribe, did_expire,
did_enter_grace_period, did_enter_billing_retry, did_pause, did_change_product,
did_refund.The subscriber. Required on every event.
The Encore app account id for this user — the value you read on the client with
getAppAccountId() and carry through your billing provider. Required
on every event (did_subscribe, did_renew, did_cancel, … all of them). Omitting it
fails validation and the event is dropped. See How the join works.Optional. Your own user id, for cross-checking
identify().The subscription this event belongs to. Required on every event.
The subscription chain anchor — your biller’s subscription id (for example, a Stripe
subscription id). Required on every event. Stays constant across the entire lifecycle of
one subscription.
Your product identifier.
The plan or pricing tier.
Price in micros (1,000,000 micros = 1 unit of currency). Supplying price directly enables
revenue-lift measurement on day one.
ISO 4217 currency code (for example,
USD).true when this event starts a free trial.The billing source (for example,
stripe).ISO 3166-1 alpha-2 country code.
RFC3339 timestamp of when the event occurred (for example,
2026-06-24T18:30:00Z).
Optional — defaults to receipt time if absent.Optional. Your own wire payload, preserved verbatim. Encore stores it alongside the
canonical event for your reference.
Example payload
The canonical example — both required identifiers (user.app_account_id and
subscription.original_transaction_id) are present, as they must be on every event:
did_renew) has the same shape and still carries
user.app_account_id — only the lifecycle-specific fields differ.
How the join works
Encore linkssubscription.original_transaction_id (the chain anchor) to
user.app_account_id (the persistent person id) on every event, and that link is what
resolves a subscription back to the web exposure Encore recorded for the same person.
Because the link is (re)written on every event, user.app_account_id must be present on
every event — not just the first. An event missing it fails validation and is dropped, so
that subscription cannot be attributed. Persist the app account id when you first see a
subscription and attach it to every event you forward. The
Subscription Webhook guide shows this with
Stripe.
Responses
user.app_account_id). Encore returns 200 here deliberately, to prevent retry storms.Important: a 200 is not proof of durable storage. The "processing deferred" message
specifically means the event was acknowledged but may have been dropped. Treat only the
"Received <event_id>" message as a successful ingest; if you see "processing deferred",
inspect your payload (most often a validation failure).X-Timestamp outside the ±300s window. Auth failures fail loud (unlike post-auth errors).Idempotency
event_id is the dedup key end to end. Re-POSTing an event with the same event_id
collapses to a single stored event, so retries are safe.
getAppAccountId()
Returns the persistent Encore app account id for the current user — the value to carry through your billing provider and send asuser.app_account_id.
Signature
Return Value
Type:string | null
- Your own user id if
identify()was called. - An auto-generated anonymous id otherwise.
nullif the SDK has not initialized yet.
getAppAccountId() is the canonical accessor for this use case.
getCurrentUserId() returns the
same underlying id.Next steps
- Subscription Webhook guide — the end-to-end Stripe walkthrough that uses this contract.
- identify() — set a stable user id so the app account id is consistent across devices.