Events and webhooks
Every lifecycle event SalesDogg records, how webhooks are signed and retried, and how to verify them.
Every change to a customer's purchases is recorded as an event and sent to your webhook endpoints. List them with npx -y @salesdogg/cli events list --app-user-id user_1.
Event types
| Type | Meaning |
|---|---|
initial_purchase |
First paid period of a subscription. |
trial_started |
A free trial began. |
trial_converted |
The first payment after a trial went through. |
renewal |
A new period was paid. |
cancellation |
Auto-renew was turned off. Access continues until the paid period ends. |
uncancellation |
Auto-renew was turned back on. |
billing_issue |
A renewal payment failed. |
billing_recovered |
A failed renewal was paid after all. |
paused |
The subscription was paused. |
resumed |
A paused subscription resumed. |
product_change |
The customer moved to another product. |
expiration |
The subscription ended and its access was removed. |
refund |
A payment was refunded. |
non_renewing_purchase |
A one-time purchase. |
transfer |
A purchase moved to another customer (restore behavior transfer). |
grant_created |
Access was granted without a purchase. |
grant_revoked |
A grant was revoked. |
test |
Sent by webhooks test. |
Register an endpoint
npx -y @salesdogg/cli webhooks create --url https://example.com/webhooks/salesdogg
npx -y @salesdogg/cli webhooks test <endpoint_id>
The response includes the endpoint's signing secret, shown once. Endpoints must be public https URLs; addresses inside private networks are refused, and redirects are not followed. Pass --event-types to receive only some events.
--format revenuecat sends RevenueCat's webhook body instead of SalesDogg's event object, for receivers written for RevenueCat; add --authorization-header to send a fixed Authorization header. Events with no RevenueCat counterpart (billing_recovered, grants) are not sent in that format.
Verify the signature
Each delivery carries a SalesDogg-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, keyed with the endpoint's secret. Verify it against the raw body, before parsing:
import { verifyWebhook } from "@salesdogg/sdk";
export async function POST(request: Request) {
const event = await verifyWebhook(
await request.text(),
request.headers.get("salesdogg-signature"),
process.env.SALESDOGG_WEBHOOK_SECRET!,
); // throws if the signature is wrong or older than 5 minutes
// handle event.type
return new Response(null, { status: 200 });
}
Delivery and retries
Respond with a 2xx status. A failed delivery is retried up to 8 attempts in all, with the wait growing from 30 seconds and capped at 24 hours. npx -y @salesdogg/cli webhooks deliveries lists attempts and their results. Deliveries are claimed with row locks, so with several API instances each one is still sent once.
Handle events idempotently by id: a retry after a timeout can deliver an event your handler already processed.