Skip to content
Back to site

Verification

Anyone who learns a source’s ingress URL can send requests to it. Verification checks each request against the secret the provider signs with, and refuses what does not match before anything is stored.

verificationMode What is checked
NONE Nothing. Whatever reaches the URL is stored and forwarded. Use it only while you are wiring a provider up.
PROVIDER The provider’s own scheme, chosen by providerType. Every provider type except GENERIC has one.
HMAC_GENERIC An HMAC-SHA256 signature in a header you name. This is how a GENERIC provider is verified.

A request that fails verification is answered 401 and is not stored.

GET /api/v1/incoming-providers returns the providers a running Railhook verifies, with the header each signs in. A provider missing from the list is one class to add; CONTRIBUTING.md shows how.

providerType Header checked How
GITHUB X-Hub-Signature-256 sha256= followed by the hex HMAC-SHA256 of the raw body.
GITLAB X-Gitlab-Token The header must equal the secret. GitLab sends a token, not a signature.
STRIPE Stripe-Signature t=…,v1=…: hex HMAC-SHA256 of <t>.<raw body>. t must be within 300 seconds of now. While a secret is being rolled Stripe sends one v1 per secret; the request verifies if any of them matches.
SHOPIFY X-Shopify-Hmac-SHA256 Base64 HMAC-SHA256 of the raw body.
SLACK X-Slack-Signature, X-Slack-Request-Timestamp v0= followed by the hex HMAC-SHA256 of v0:<timestamp>:<raw body>. The timestamp must be within 300 seconds of now.
TWILIO X-Twilio-Signature Base64 HMAC-SHA1 with the auth token. For form-encoded requests it covers the URL followed by every parameter sorted by name; for other requests it covers the URL, and the bodySHA256 query parameter must match the body.
SQUARE x-square-hmacsha256-signature Base64 HMAC-SHA256 of the notification URL followed immediately by the raw body, with no separator.
ADYEN hmacsignature, or none Base64 HMAC-SHA256 keyed with the decoded bytes of the hex HMAC key. A standard payments webhook carries no header: each notification item signs eight of its own fields, joined with colons, into additionalData.hmacSignature. Management and Balance Platform webhooks sign the raw body and put the result in the header. Both are accepted, so one source handles either.
SENDGRID X-Twilio-Email-Event-Webhook-Signature, X-Twilio-Email-Event-Webhook-Timestamp ECDSA over SHA-256 of the timestamp followed by the raw body, verified with SendGrid’s public key. Not an HMAC.
HUBSPOT X-HubSpot-Signature-v3, X-HubSpot-Request-Timestamp Base64 HMAC-SHA256 of the request method, the full URL, the raw body and the timestamp, in that order, with the app’s client secret. The timestamp must be within 300 seconds of now. Only v3; v1 and v2 bind a request to no moment at all.

For every provider the secret goes in the source’s hmacSecret: the webhook signing secret, the GitLab secret token, the Twilio auth token, the Square signature key, the Adyen HMAC key as the hexadecimal string the Customer Area shows, or the HubSpot app’s client secret.

  1. Create the source with verificationMode set to PROVIDER, the providerType, and the provider’s secret:

    Terminal window
    curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources" \
    -H "X-API-Key: $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"GitHub","providerType":"GITHUB","verificationMode":"PROVIDER","hmacSecret":"'"$GITHUB_WEBHOOK_SECRET"'"}'
  2. Paste the source’s ingressUrl into the provider’s webhook settings, with the same secret.

  3. Send a test webhook from the provider. It appears under Received; a signature mismatch is answered 401 and does not.

For a provider without a preset, use HMAC_GENERIC:

Field Default Meaning
hmacHeaderName X-Signature The header that carries the signature.
hmacSignaturePrefix none A prefix to strip before comparing, such as sha256=.
hmacSecret The shared secret.

Two header shapes are accepted:

Shape Signed content Replay protection
t=<unix-ms>,v1=<hex>, Railhook’s own format <t>.<raw body> t must be within 300 seconds of now.
A bare hex digest The raw body only None from the signature: the same request verifies again for as long as the secret lives.
Terminal window
curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Acme","providerType":"GENERIC","verificationMode":"HMAC_GENERIC","hmacHeaderName":"X-Acme-Signature","hmacSignaturePrefix":"sha256=","hmacSecret":"'"$ACME_SECRET"'"}'

A verified signature is remembered for a window, and a second request with the same signature inside that window is refused with 401. For providers whose signature carries a timestamp this suppresses duplicates. For a bare hex digest it is the only protection against a captured request being sent again.

A request carrying a provider event id that Railhook has already stored is a resend, not a replay: it still has to verify, and is then answered 202 with the stored event. See provider resends.

Variable Default
WEBHOOK_INGRESS_REPLAY_WINDOW_MINUTES 5