Skip to content

Send your first webhook

This page takes one event from your system to your customer’s endpoint through the API, and shows the receiver how to prove the request came from you. You need a project ID and an API key with the READ_WRITE scope (Quickstart covers both).

Terminal window
export RAILHOOK_URL=https://railhook.io # or your own instance, e.g. http://localhost or https://hooks.example.com
export RAILHOOK_API_KEY=...
export PROJECT_ID=...
  1. Register an endpoint

    Terminal window
    curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/endpoints" \
    -H "X-API-Key: $RAILHOOK_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://api.customer.com/webhooks","description":"Orders"}'

    The response includes the endpoint’s id and its secret. The receiver needs the secret to check signatures.

  2. Subscribe it to an event type

    Terminal window
    curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/subscriptions" \
    -H "X-API-Key: $RAILHOOK_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"endpointId":"'"$ENDPOINT_ID"'","eventType":"order.completed"}'

    Patterns work too: order.* matches one segment, order.** matches any number.

  3. Send an event

    Terminal window
    curl -X POST "$RAILHOOK_URL/api/v1/events" \
    -H "X-API-Key: $RAILHOOK_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: order-12345-completed" \
    -d '{"type":"order.completed","data":{"orderId":"ord_12345","amount":99.99}}'

    The response is 201, with eventId and deliveriesCreated.

  4. Verify the signature on the receiver

    The endpoint receives a POST whose body is the event’s data object, plus these headers:

    What lands on the endpoint
    POST /webhooks HTTP/1.1
    Content-Type: application/json
    X-Signature: t=1738000000000,v1=<hex hmac-sha256>
    X-Timestamp: 1738000000000
    X-Event-Id: 6f0e…
    X-Delivery-Id: 91ab…
    {"orderId":"ord_12345","amount":99.99}

    The signature is HMAC-SHA256 over <t>.<raw body>, keyed with the endpoint secret. Verify against the raw bytes, before you parse the JSON:

    import express from 'express';
    import { verifySignature } from '@railhook/node';
    const app = express();
    app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
    try {
    verifySignature(req.body.toString(), req.headers['x-signature'], process.env.WEBHOOK_SECRET);
    } catch {
    return res.status(400).send('Invalid signature');
    }
    const data = JSON.parse(req.body.toString());
    res.sendStatus(200);
    });

    By default the SDKs also reject a timestamp more than five minutes old, so a captured request cannot be replayed later.