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.

Step by step

  1. 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. 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. 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 a whsec_ signing secret once. Store it in your password manager or receiver config. You cannot view it again; rotate if lost.

  4. 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. 5

    Emit a message

    From your shell (or the Events → Send test event drawer):

    curl
    curl -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-Key so retries of the HTTP call do not create duplicate messages.

  6. 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. 7

    Verify the signature on the receiver

    Check webhook-id, webhook-timestamp, and webhook-signature. Full snippets live on Signing and verify. Minimal HMAC (Node):

    node
    import 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:… and http://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