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 timestamp isn'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 Authorization or X-Webhook-Secret header to a Railway webhook?
  • How do I handle Deployment.failed events?
  • 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, default Authorization); look it up case-insensitively.
  • Compare the whole value against RAILWAY_WEBHOOK_SECRET. Do not strip or parse a Bearer prefix: if you typed Bearer abc123 in Railway, the env var is Bearer abc123.
  • Python: hmac.compare_digest on bytes (encode both sides; passing non-ASCII str raises).
  • Return 401 on a missing header or mismatch, 400 for a body that isn't a JSON object, 200 once 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.

TypeSent When
Deployment.failedA build or deploy fails (the docs' example payload)
Deployment.crashedA running deployment crashes
Deployment.oom_killedA deployment is stopped for running out of memory
Deployment.<state>Other deployment status changes; see Railway's Deployments reference for states
alert typesA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →