# OpenRouter Webhooks

## When to Use This Skill

* How do I receive OpenRouter webhooks for a video job's `callback_url`?
* How do I verify the OpenRouter `X-OpenRouter-Signature` header?
* How do I handle `video.generation.completed` and download the finished video?
* How do I handle `video.generation.failed`, `cancelled` or `expired` jobs?
* Why is my OpenRouter webhook signature verification failing?

> OpenRouter signs only video-generation callbacks (the callback_url of a video request). Use this source for those callbacks.

OpenRouter's Broadcast "Webhook" destination (OTLP traces) is unsigned and is not covered by this skill.

## Verification (core)

Every callback carries `X-OpenRouter-Signature: t=<unix seconds>,v1=<hex>`. The `v1` value is lowercase hex **HMAC-SHA256**, keyed with the workspace webhook signing secret (its UTF-8 bytes, used as-is), over the bytes `"<t>,"` (a literal **comma**, not a dot) followed by the **raw request body**.

```javascript
const crypto = require('crypto');

function verifyOpenRouterSignature(rawBody, header, secret) {
  if (!header || !secret) return false;
  let t = null;
  const sigs = [];
  for (const part of header.split(',')) {
    const p = part.trim();
    if (p.startsWith('t=') && t === null) t = p.slice(2);
    else if (p.startsWith('v1=')) sigs.push(p.slice(3));
  }
  if (!t || sigs.length === 0) return false;

  const signed = Buffer.concat([Buffer.from(`${t},`, 'latin1'), rawBody]); // bytes, never a re-encoded string
  const expected = crypto.createHmac('sha256', secret).update(signed).digest();
  return sigs.some((s) => /^[0-9a-fA-F]{64}$/.test(s) &&
    crypto.timingSafeEqual(expected, Buffer.from(s, 'hex')));
}

```

Respond **204** for a valid delivery and **401** for a missing, malformed or mismatched signature. Read the raw body first (`express.raw({ type: '*/*' })`, `await request.arrayBuffer()`, `await request.body()`) and parse JSON only after the check.

**Timestamp window is off by default.** OpenRouter's own receiver example rejects `t` older than 300 s, but Hookdeck retries and replays resend the original headers and timestamp (and Hookdeck applies no tolerance itself), so a fixed window would reject legitimate retries. The examples offer an opt-in: set `OPENROUTER_WEBHOOK_TOLERANCE_SECONDS=300` to enforce one (unset or `0` = off). Use idempotency instead (see below).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/openrouter-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/openrouter-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/openrouter-webhooks/examples/fastapi/).

## Common Event Types

Dispatch on the body's `type` field; the job is in `data` (`id`, `status`, `generation_id`, `model`, `unsigned_urls`, `usage`, `error`).

| Event | Triggered When |
| --- | --- |
| `video.generation.completed` | The job finished; download the video from `data.unsigned_urls[0]` |
| `video.generation.failed` | The job failed; see `data.error` |
| `video.generation.cancelled` | The job was cancelled |
| `video.generation.expired` | The job expired before completing |

Each delivery also has an `X-OpenRouter-Idempotency-Key` header (e.g. `job_test-completed`); use it to deduplicate retries. The retry schedule is not documented.

## Environment Variables

```bash
# Webhook signing secret from your OpenRouter workspace settings
OPENROUTER_WEBHOOK_SECRET=your_openrouter_webhook_signing_secret
# Optional: reject signatures older than N seconds (unset or 0 = off)
OPENROUTER_WEBHOOK_TOLERANCE_SECONDS=0

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 openrouter --path /webhooks/openrouter

```

Use the printed public HTTPS URL as the `callback_url` of a `POST /api/v1/videos` request (or as the workspace default callback URL).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/openrouter-webhooks/references/overview.md) - How video callbacks work, events, payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/openrouter-webhooks/references/setup.md) - Signing secret and `callback_url` configuration
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/openrouter-webhooks/references/verification.md) - Signature verification details and gotchas