SalesDogg

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.