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 task-level walkthrough, see
Configure Analytics.
This is the inbound direction: your server → Encore. For the outbound direction (Encore
calling your server when a user completes an offer), see
Offer Completed Webhook. Different headers, a different
signing string, and an unrelated purpose; don’t mix the two.
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: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.Structure
Field 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.
Configure Analytics shows
this end to end.
Responses
object
object
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).object
X-Timestamp outside the ±300 second window. Auth failures fail loud (unlike post-auth errors).
Note the shape: error responses use Encore’s platform error body, a top-level error
message with no success field. Only 200 responses carry the success envelope.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
- Configure Analytics: the quickstart step that uses this contract.
- identify(): set a stable user id so the app account id is consistent across devices.