# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/railway-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/railway-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/railway-webhooks/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

```bash
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):

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/railway-webhooks/references/overview.md) - What Railway webhooks are, payload shape, delivery behavior
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/railway-webhooks/references/setup.md) - Add the webhook and its secret custom header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/railway-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/railway-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests