Skip to content
Back to site

Send your first webhook

You need a project ID and an API key with the READ_WRITE scope (see Quickstart).

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, which the receiver uses 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);
    });

    The SDKs also reject a timestamp older than five minutes by default, so a captured request cannot be replayed.