Sources
A source is one third-party provider you connect, such as Stripe or GitHub, together with what Railhook needs to prove a webhook really came from it. Creating a source gives you an ingress URL to paste into the provider.
How it works
Section titled “How it works”-
The provider sends a webhook to the source’s ingress URL.
-
Railhook verifies it, as the source is configured to. A webhook that fails verification is refused and not stored.
-
The webhook is stored as it arrived, as an incoming event. The dashboard lists these under Received.
-
Railhook creates one forward for each destination of the source, and works on each until it succeeds or is abandoned. See Destinations.
Create a source
Section titled “Create a source”import { Railhook } from '@railhook/node';
const client = new Railhook({ apiKey: process.env.RAILHOOK_API_KEY, baseUrl: process.env.RAILHOOK_URL });
const source = await client.incomingSources.create(projectId, { name: 'Stripe', providerType: 'STRIPE', verificationMode: 'PROVIDER', hmacSecret: process.env.STRIPE_WEBHOOK_SECRET,});
console.log(source.ingressUrl);import os
from railhook import IncomingSourceCreateParams, Railhook
client = Railhook(api_key=os.environ["RAILHOOK_API_KEY"], base_url=os.environ["RAILHOOK_URL"])
source = client.incoming_sources.create( project_id, IncomingSourceCreateParams( name="Stripe", provider_type="STRIPE", verification_mode="PROVIDER", hmac_secret=os.environ["STRIPE_WEBHOOK_SECRET"], ),)
print(source.ingress_url)<?phpuse Railhook\Railhook;
$client = new Railhook(apiKey: getenv('RAILHOOK_API_KEY'), baseUrl: getenv('RAILHOOK_URL'));
$source = $client->incomingSources->create($projectId, [ 'name' => 'Stripe', 'providerType' => 'STRIPE', 'verificationMode' => 'PROVIDER', 'hmacSecret' => getenv('STRIPE_WEBHOOK_SECRET'),]);
echo $source['ingressUrl'];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":"Stripe","providerType":"STRIPE","verificationMode":"PROVIDER","hmacSecret":"'"$STRIPE_WEBHOOK_SECRET"'"}'Paste ingressUrl into the provider’s webhook settings, then add at least one destination.
| Field | What it does |
|---|---|
name |
Required. Display name, up to 255 characters. |
slug |
URL-friendly name, lower case letters, digits and dashes, up to 64 characters. Generated when left out. |
providerType |
GENERIC, GITHUB, GITLAB, STRIPE, SHOPIFY, SLACK or TWILIO. |
verificationMode |
NONE, HMAC_GENERIC or PROVIDER. Left out, it follows hmacSecret: with a secret, PROVIDER for a provider that has a preset and HMAC_GENERIC for GENERIC; without one, NONE. See Verification. |
hmacSecret |
The secret the provider signs with. Write-only: the API never returns it. |
hmacHeaderName, hmacSignaturePrefix |
Where the signature is, for HMAC_GENERIC. |
rateLimitPerSecond |
Requests per second this source accepts. |
status |
ACTIVE or DISABLED. |
The ingress URL
Section titled “The ingress URL”The ingress URL is WEBHOOK_INGRESS_BASE_URL followed by /ingress/ and an opaque token. The token is the only thing that names the source, so treat the URL as a credential. Verification is what stops someone who learns the URL from sending you events.
What the ingress answers
Section titled “What the ingress answers”Each code is what the provider’s retry logic sees.
| Status | Meaning |
|---|---|
200 |
Slack’s url_verification handshake to a SLACK source, answered {"challenge":"…"} once its signature is verified. This is how Slack enables the Request URL, so the source needs PROVIDER verification with the app’s signing secret. The handshake is not an event: nothing is stored, forwarded or counted. |
202 |
Accepted and stored. The body carries requestId. |
401 |
Verification is configured and the signature did not check out, or a signature already seen arrived on a webhook that is not a resend of a stored one. Nothing is stored. |
403 |
The organization is suspended. Answered 403, not 410, so a provider that deletes its subscription on 410 still has it when the suspension is lifted. |
404 |
No source has that token, or its project has been deleted. |
410 |
The source exists but is disabled. |
413 |
The body is larger than the limit, 512 KB by default. |
429 |
The source’s rate limit is spent, with Retry-After: 1. Also returned, with Retry-After: 3600, when the organization cannot accept more webhooks right now; a resend of a webhook already stored is still answered 202. |
| Variable | Default | Effect |
|---|---|---|
WEBHOOK_INCOMING_MAX_PAYLOAD_SIZE_BYTES |
524288 |
Largest body the ingress accepts. |
WEBHOOK_INCOMING_RATE_LIMIT_PER_SECOND |
100 |
Rate limit for a source that sets none of its own. 0 disables this default. |
Provider resends
Section titled “Provider resends”Providers send the same webhook again. When a request carries an id Railhook recognises, a second arrival with the same id is answered 202 with the stored event instead of forwarding it again. The signature is still verified, but a resend is refused neither because that signature was seen before nor because the organization cannot accept new webhooks right now. Railhook recognises these ids:
| Provider | Id |
|---|---|
| Generic | X-Webhook-Id header |
| Stripe | id in the JSON body (evt_…), on a request signed with Stripe-Signature |
| GitHub | X-GitHub-Delivery header |
| GitLab | webhook-id header, or Idempotency-Key (GitLab 17.4 and later) |
| Shopify | X-Shopify-Webhook-Id header |
| Twilio | I-Twilio-Idempotency-Token header |
| Slack | event_id in the JSON body |
A provider that sends none of these is not deduplicated.