Agent skill
Railway Webhooks Skill
Receive and authenticate Railway webhooks. Use when setting up a Railway webhook endpoint for deployment status changes and alerts, adding a secret custom header (Authorization or X-Webhook-Secret), debugging 401s, or handling events like Deployment.failed.
Install this skill
npx skills add hookdeck/webhook-skills --skill railway-webhooks
Railway sends a JSON webhook to your URL when a service's deployment status changes or an alert fires (volume usage, CPU/RAM). Webhooks are configured per project and cover every environment. Payloads are not signed. Railway's docs tell you to add a custom header with a secret value (for example Authorization: Bearer <token> or X-Webhook-Secret) and reject any request without it. Your endpoint authenticates the request by comparing that header with the value you stored.
A static shared credential authenticates the sender, not the body. There is no signature and the body's
timestampisn't signed, so the check gives no replay protection and does not prove the payload is unmodified. Use HTTPS, and rotate the header value if it leaks. Treat a webhook as a prompt to act, not a source of truth: Railway delivery is best-effort and unordered, so reconcile against the Railway API when you need certainty.
When to Use This Skill
- How do I receive Railway webhooks?
- How do I verify a request came from Railway? (Railway doesn't sign: use a secret custom header)
- How do I add an
AuthorizationorX-Webhook-Secretheader to a Railway webhook? - How do I handle
Deployment.failedevents? - Why does my Railway webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyRailwayRequest(
headers,
secret = process.env.RAILWAY_WEBHOOK_SECRET, // exact, whole header value
headerName = process.env.RAILWAY_WEBHOOK_HEADER || 'Authorization'
) {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()]; // Node lowercases header names
return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}
- The header name is whatever you chose in Railway (
RAILWAY_WEBHOOK_HEADER, defaultAuthorization); look it up case-insensitively. - Compare the whole value against
RAILWAY_WEBHOOK_SECRET. Do not strip or parse aBearerprefix: if you typedBearer abc123in Railway, the env var isBearer abc123. - Python:
hmac.compare_digeston bytes (encode both sides; passing non-ASCIIstrraises). - Return
401on a missing header or mismatch,400for a body that isn't a JSON object,200once handled. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The body has a type of the form <Resource>.<state>, plus details, resource, severity and timestamp. Railway's webhook docs give one example type and point to the Deployments reference for states, without saying how each is written in type. Railway's own agent skill also names Deployment.crashed and Deployment.oom_killed. Alert types are not enumerated.
| Type | Sent When |
|---|---|
Deployment.failed | A build or deploy fails (the docs' example payload) |
Deployment.crashed | A running deployment crashes |
Deployment.oom_killed | A deployment is stopped for running out of memory |
Deployment.<state> | Other deployment status changes; see Railway's Deployments reference for states |
| alert types | A volume usage or CPU/RAM monitor alert fires; names not documented, so log unknown types |
Environment Variables
RAILWAY_WEBHOOK_HEADER=Authorization # name of the custom header you added in Railway
RAILWAY_WEBHOOK_SECRET=Bearer your-long-token # the exact full value of that header
Header values are write-only in Railway after you save, so keep the secret in your own secret store when you create it.
Local Development
Railway can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 railway --path /webhooks/railway
Use the printed URL as the webhook URL in Railway and add the custom header there.
Resources
- references/overview.md - What Railway webhooks are, payload shape, delivery behavior
- references/setup.md - Add the webhook and its secret custom header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests