API webhooks

Storlaunch sends webhook events to URLs you control so you can react to state changes in real time — products created, checkouts completed, subscriptions renewed.

This page is the API reference for webhooks: the envelope, the signature scheme, the retry policy. For the per-event catalogue, see Webhook events. For programmatic endpoint management, see Webhook endpoints.

The envelope

Every webhook is a POST to your endpoint with this body:

{
  "id": "evt_01HX...",
  "type": "checkout.completed",
  "createdAt": "2026-05-13T10:43:22Z",
  "accountId": "acc_01HX...",
  "data": { /* event-type-specific payload */ }
}
Field Description
id Unique event ID. Stable across retries; use it for idempotency on your side.
type Event type (checkout.completed, storlaunch.product.created.v1, ...).
createdAt ISO 8601 UTC timestamp when the event was emitted.
accountId The workspace this event is for.
data Event-specific payload — see the per-event reference pages.

Headers

Header Value
Content-Type application/json
X-Storlaunch-Signature HMAC signature header (see below).
X-Storlaunch-Event-Id Same as event.id — handy for log correlation.
X-Storlaunch-Delivery-Id Per-attempt ID; different on each retry of the same event.

Signature scheme

Every webhook is signed so you can verify it came from Storlaunch.

Header format

X-Storlaunch-Signature: t=1715593403,v1=ab12cd34...
  • t = Unix epoch seconds at signing time.
  • v1 = hex HMAC-SHA256(<webhook signing secret>, <t>.<raw body>).

Verification recipe

  1. Split the header on , then = to extract t and v1.
  2. Compute the expected signature using your webhook's signing secret (returned at endpoint create time).
  3. Compare in constant time.
  4. Reject events older than 5 minutes (now - t > 300) to prevent replay.
// Node
import crypto from 'node:crypto';

function verifyWebhook(rawBody, header, secret, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map(s => s.split('=')));
  const t = parseInt(parts.t, 10);
  if (Math.abs(Date.now() / 1000 - t) > tolerance) {
    throw new Error('Signature timestamp out of tolerance');
  }
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.${rawBody}`).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
    throw new Error('Signature mismatch');
  }
  return JSON.parse(rawBody);
}
# Python
import hmac, hashlib, time, json

def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300):
    parts = dict(s.split("=") for s in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > tolerance:
        raise ValueError("Signature timestamp out of tolerance")
    expected = hmac.new(secret.encode(), f"{t}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts["v1"]):
        raise ValueError("Signature mismatch")
    return json.loads(raw_body)
// Go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "errors"
    "fmt"
    "strings"
    "time"
)

func VerifyWebhook(rawBody []byte, header, secret string, tolerance time.Duration) error {
    parts := map[string]string{}
    for _, kv := range strings.Split(header, ",") {
        x := strings.SplitN(kv, "=", 2)
        if len(x) == 2 { parts[x[0]] = x[1] }
    }
    t, _ := time.Parse(time.RFC3339, parts["t"])
    if time.Since(t) > tolerance { return errors.New("signature timestamp out of tolerance") }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(fmt.Sprintf("%d.%s", t.Unix(), rawBody)))
    expected := hex.EncodeToString(mac.Sum(nil))
    if !hmac.Equal([]byte(expected), []byte(parts["v1"])) {
        return errors.New("signature mismatch")
    }
    return nil
}

Each SDK exports a verifyWebhook helper that does all this for you. See SDK.

Retry policy

We deliver events with at-least-once semantics:

  • Initial attempt — immediately after the event is emitted.
  • Retries — exponential backoff at T+1m, T+5m, T+30m, T+2h, T+8h, T+24h (six attempts after the initial).
  • Success is any 2xx response. 4xx and 5xx both trigger retry.
  • Disable threshold — 20 consecutive failures auto-disables the endpoint (you'll see active: false in GET /v1/payment/webhook-endpoints). Re-enable manually after fixing.

The total retry window is roughly 35 hours. Beyond that, the event is permanently dropped.

Subscribing to events

In Settings → Webhooks (or via Webhook endpoints API):

  1. Add your endpoint URL (HTTPS only).
  2. Pick the events you want. Leave empty to subscribe to everything.
  3. Copy the signing secret — you only see it once.

Adding an event type later affects only future deliveries; we don't backfill historical events.

Idempotency

Use event.id as your idempotency key. A typical handler:

// Node
app.post('/webhooks/storlaunch', async (req, res) => {
  const event = verifyWebhook(req.rawBody, req.headers['x-storlaunch-signature'], SECRET);
  const seen = await db.events.exists({ id: event.id });
  if (seen) return res.status(200).end();
  await processEvent(event);
  await db.events.insert({ id: event.id, type: event.type, receivedAt: Date.now() });
  res.status(200).end();
});

Replay protection

The signature scheme includes a timestamp; reject events older than 5 minutes. Otherwise an attacker who once captures a valid event could replay it indefinitely.

Test events

The dashboard's Webhooks → Send test event action delivers a synthetic event to your endpoint — useful for verifying signature handling before going live. The test event's id is prefixed evt_test_ so you can filter it out of production processing.

Next