Sellvik / developers
Webhooks

Registering webhooks

Panel vs API; what makes a good endpoint URL.

Two ways to register

  • Panel. Settings → Webhooks → New. Easiest for a single integration; the secret is shown once on screen and you copy it into your endpoint's config.
  • API. POST /v1/admin/webhooks. Reproducible across environments; pair with Terraform or a setup script.

Both end up writing the same row.

What you need

  1. A public HTTPS URL. Localhost works only via a tunnel (ngrok, cloudflared) — see Testing locally.
  2. The list of events you want. See Events for the catalogue.
  3. A place to store the returned secret (env var, secret manager).

URL hygiene

  • Use a dedicated path. /sellvik-webhook, not /. Makes traffic obvious in your access logs.
  • Don't put the secret in the URL. It's already in the payload signature; putting it in the path leaks it to every reverse proxy in the chain.
  • Respond fast (< 10s). If your handling is slow, ack first, work later: enqueue the event to a queue and return 200 immediately.
  • Idempotent handler. Process the same event.id twice without breaking state. See Events → Idempotency.
  • Stable URL. Webhook deliveries follow redirects up to two hops; beyond that they fail. Don't put a redirector in front of the webhook endpoint.

One-shot registration script

// scripts/setup-webhooks.ts
import "dotenv/config"

const SELLVIK_ADMIN_KEY = process.env.SELLVIK_ADMIN_KEY!
const ENDPOINT = process.env.WEBHOOK_URL! // e.g. https://erp.example.com/sellvik-webhook

const res = await fetch("https://api.sellvik.app/api/v1/admin/webhooks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${SELLVIK_ADMIN_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "ERP sync",
    url: ENDPOINT,
    events: [
      "order.created",
      "order.fulfilled",
      "order.cancelled",
      "inventory.low_stock",
    ],
  }),
})

if (!res.ok) {
  console.error(await res.text())
  process.exit(1)
}

const { id, secret } = await res.json()
console.log(`Created webhook ${id}`)
console.log(`Secret (store now, never shown again):\n${secret}`)

One subscription per integration

Don't register many narrow webhooks if a single broad one works. The dispatcher is fan-out per event; subscribing the same URL to 8 events across 8 webhooks is 8x the delivery rate vs 1 webhook with 8 events.

When to split:

  • Different integrations consuming different event groups (a fulfillment service vs an analytics pipe).
  • Different reliability requirements (a critical pipe vs an experimental one) so failures in one don't surface as webhook.failed for the other.

Updating events

PATCH /v1/admin/webhooks/{id} accepts a new events array — the secret and URL stay; only the subscription changes. Use this when you want to listen for additional events without rotating credentials.

On this page