Getting started
Ship your first signed webhook delivery in under 10 minutes. Stay in Test until a real customer URL is ready.
What you will build
Loading diagram...
An application (one tenant), one endpoint URL, a reveal-once signing secret, a test API key, one emit, and a delivery you can inspect. Then verify the signature on the receiver.
sk_test_ never touches live endpoints. Switch to Live only when you intend real deliveries to customer servers.Step by step
- 1
Sign up and land in Test
Open /login. Sign in with Clerk (email, Google, or GitHub — enabled in the Clerk dashboard). A workspace is created on first visit. You land in the Test environment.
- 2
Create an application
Applications are your tenants (the customers who receive webhooks). In the dashboard open Applications → Create. You get an
app_id. Optionally mint a portal exchange link so that tenant can self-serve endpoints later. - 3
Add an endpoint and copy the secret once
Endpoints → Add endpoint. Pick the application, paste a URL, subscribe to at least one event type (for example
invoice.paid). Outemit shows awhsec_signing secret once. Store it in your password manager or receiver config. You cannot view it again; rotate if lost. - 4
Create an API key
Settings → API keys → Create. Copy the
sk_test_value once. Use Bearer auth on every management and ingest call. - 5
Emit a message
From your shell (or the Events → Send test event drawer):
curlcurl -X POST https://outemit.dev/api/v1/messages \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: demo-1" \ -H "Content-Type: application/json" \ -d '{"app_id":"app_...","event_type":"invoice.paid","payload":{"id":"inv_1"}}'Pass
Idempotency-Keyso retries of the HTTP call do not create duplicate messages. - 6
Inspect the delivery
Open Deliveries. Click the row for status, attempt number, latency, request headers, and response body. If the receiver returned 5xx or timed out, Outemit schedules automatic retries with backoff.
- 7
Verify the signature on the receiver
Check
webhook-id,webhook-timestamp, andwebhook-signature. Full snippets live on Signing and verify. Minimal HMAC (Node):nodeimport crypto from "node:crypto"; function verify(secret, rawBody, headers) { const id = headers["webhook-id"]; const ts = headers["webhook-timestamp"]; const sigHeader = headers["webhook-signature"]; // "v1,<b64> ..." const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const toSign = `${id}.${ts}.${rawBody}`; const expected = crypto.createHmac("sha256", key).update(toSign).digest("base64"); const now = Math.floor(Date.now() / 1000); if (Math.abs(now - Number(ts)) > 300) return false; // 5m tolerance return sigHeader.split(" ").some((part) => { const [, b64] = part.split(","); try { return b64 && crypto.timingSafeEqual(Buffer.from(b64), Buffer.from(expected)); } catch { return false; } }); } verify("whsec_...", rawBody, headers);
Test vs Live
- Test: safe sandbox. Localhost URLs allowed. Keys start with
sk_test_. - Live: real customer traffic. HTTPS required. Keys start with
sk_live_.
Never paste a live key into a shared notebook or client-side code.
Localhost vs HTTPS
- Test accepts
http://localhost:…andhttp://127.0.0.1:…so you can iterate without a tunnel. - Live requires
https://. Use a tunnel (Cloudflare Tunnel, ngrok) or deploy the receiver before switching environments.
For a public request bin while prototyping, see Testing (webhook.site, send-test, CLI listen).
What to do next
- Teach receivers to verify: Signing and verify
- Learn Retry vs Replay: Core concepts
- Debug failures: Debug deliveries
- Share self-serve with customers: Customer portal