Listen for Salable Events

You want your application to react when a Subscription is created, updated, or cancelled; usage is finalised; or tokens are accrued, without constant polling. A Webhook delivers those events to an endpoint you control automatically. This guide creates a webhook, subscribes it to events, verifies deliveries, and confirms the integration with a ping event.

Create a webhook and choose its events

Send POST /api/webhooks with a name, endpoint URL, and the events the webhook should subscribe to. The 201 response carries the signing secret you'll use to verify every delivery.

FieldDescription
nameA label for the webhook, 1–120 characters.
urlThe public HTTPS endpoint that receives deliveries.
eventsOne or more event types to subscribe to. Use the underscore form (subscription_created).
descriptionAn optional description. Up to 255 characters.
import { Salable } from '@salable/sdk';
const salable = new Salable(process.env.SALABLE_SECRET_KEY);
 
const webhook = await salable.api.webhooks.post({
    name: 'Subscription events',
    url: 'https://api.acme.com/webhooks/salable',
    events: ['subscription_created', 'subscription_updated', 'subscription_cancelled']
});

The response includes a signing secret (prefixed whsec_) that signs every delivery for this webhook; use it to verify events are genuine. Keep it server-side as an environment variable and treat it as you would any other secret. The GET /api/webhooks response includes it again, so you can retrieve it later if required. To change which events the webhook receives, send PATCH /api/webhooks/{id} with a new events array.

Note Each organisation can have up to 10 webhooks.

Important You subscribe to events using the underscore form (subscription_created), but events are delivered using dot notation for the type property (subscription.created); ensure you handle this.

Receive and verify events

Every event is sent as a POST request to your endpoint URL with signature headers you should use to verify the request before trusting the payload. Once processed return any 2xx status to confirm a successful delivery. A non-2xx status or taking longer than 15 seconds to respond is a failed delivery. Any failed non-ping events are automatically retried up to 10 times following an exponential backoff.

Generate a signature using the raw request body, exactly as sent in the request. Use this signature to compare against the one included in the x-salable-signature header of the request to confirm authenticity.

Important The below code is only an example. The exact implementation for verifying a delivery depends on your framework and runtime.

import { createHmac, timingSafeEqual } from 'node:crypto';
 
export default async function handleSalableWebhook(req, res) {
    if (req.method !== 'POST') return res.status(405).end();
 
    const signature = req.headers['x-salable-signature'];
    const timestamp = req.headers['x-salable-timestamp'];
    const secret = process.env.SALABLE_WEBHOOK_SECRET;
 
    if (typeof signature !== 'string' || typeof timestamp !== 'string' || !secret) {
        return res.status(401).end();
    }
 
    // Reject deliveries older than 5 minutes to block replays.
    const sentAt = Date.parse(timestamp);
    if (!Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) > 5 * 60 * 1000) {
        return res.status(401).end();
    }
 
    // `rawBody` must be the exact, unparsed body delivered to your endpoint.
    const rawBody = req.rawBody;
    const expected = createHmac('sha256', secret).update(timestamp).update('.').update(rawBody).digest();
    const received = Buffer.from(signature, 'hex');
    if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
        return res.status(401).end();
    }
 
    const event = JSON.parse(rawBody.toString('utf8'));
 
    // Process the verified `event` with your application's idempotent handler.
    return res.status(204).end();
}

Process events safely

Keep these recommendations in mind when processing events:

  • Webhook deliveries can be repeated, so track the envelope's id and discard events you've already processed.
  • Return a 2xx response within the 15-second timeout.
  • Queue time-consuming work to run after you respond, rather than processing it inline.

Related The signature verification reference covers the exact headers, signing scheme, and replay window in full. For retry behaviour, auto-disabling, and the delivery dashboard, see Implementing Webhook Handlers.

Testing your webhook

To test your implementation before processing real events, send POST /api/webhooks/{id}/ping. It queues a ping event which you should process like any other event and respond with a 2xx status to confirm delivery.

Important If the ping event returns an error response, investigate the issue before processing real events.

import { Salable } from '@salable/sdk';
const salable = new Salable(process.env.SALABLE_SECRET_KEY);
 
await salable.api.webhooks.byId('wh_01HXACME').ping.post();
  • Webhooks The full event reference, signature and retry details, auto-disabling, and the delivery dashboard.
  • Core Concepts How Webhooks, events, and signing secrets relate in the Salable model.