Agent skill
Magic Hour Webhooks Skill
Receive and verify Magic Hour webhooks (magichour.ai, the AI video, image and audio generation API). Use when setting up a Magic Hour webhook handler, debugging magic-hour-event-signature verification, or handling render job events like video.completed, video.errored, image.completed or audio.completed. Not Standard Webhooks: do not use the svix or standardwebhooks libraries.
Install this skill
npx skills add hookdeck/webhook-skills --skill magic-hour-webhooks
Magic Hour (magichour.ai) is an AI video, image and audio generation API. It sends a webhook when a render job starts, completes or errors. Every delivery is signed with HMAC-SHA256 using the webhook secret from the Magic Hour Developer Hub.
When to Use This Skill
- How do I receive Magic Hour webhooks?
- How do I verify the
magic-hour-event-signatureheader? - How do I handle
video.completed,image.completedoraudio.completedevents? - Why does my Magic Hour webhook signature verification fail?
- Why can't I use the Svix or Standard Webhooks library for Magic Hour?
Verification (core)
Two headers arrive on every delivery: magic-hour-event-signature (64 hex characters, no prefix) and magic-hour-event-timestamp (Unix seconds). The signature is the hex HMAC-SHA256 of {timestamp header}.{raw request body}. The key is the secret's UTF-8 bytes, used whole including whsec_: nothing is base64- or hex-decoded.
const crypto = require('crypto');
function verifyMagicHour(rawBody, headers, secret) { // rawBody: Buffer, exactly as received
const sig = headers['magic-hour-event-signature'];
const ts = headers['magic-hour-event-timestamp'];
if (!secret || !sig || !ts || !/^[0-9a-fA-F]{64}$/.test(sig)) return false;
// Build the message as bytes: header latin1, "." as UTF-8, then the untouched body bytes.
const message = Buffer.concat([Buffer.from(ts, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(message).digest();
const received = Buffer.from(sig, 'hex'); // safe: format validated above
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Return 401 for a missing, malformed or mismatched signature. Never decode the body to a string and re-encode it, and never JSON.parse before verifying (Express: express.raw({ type: '*/*' }); Next.js: await request.arrayBuffer(); FastAPI: await request.body()).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Timestamp tolerance is opt-in
Magic Hour recommends rejecting timestamps more than 5 minutes old, but this skill does not do that by default. Retries (and Hookdeck replays) resend the original headers, and Hookdeck itself applies no tolerance, so a fixed window would reject legitimate redeliveries. To opt in, set MAGIC_ (unset or 0 = off, e.g. 300). The examples then require a numeric timestamp within that many seconds of now.
Hookdeck source setup
Hookdeck's source type is MAGIC_HOUR ("Magic Hour"). When you create it, Hookdeck shows this note, and it applies here too:
Paste the whole secret, including the whsec_ prefix. Magic Hour uses it as a plain HMAC key.
Event Types
Dispatch on the body's type field ({type, payload}; the payload matches the corresponding GET /v1/{video,image,audio}-projects/:id response).
| Event | Sent When |
|---|---|
video.started / image.started / audio.started | Job moves from queued to rendering |
video.completed / image.completed / audio.completed | Job finished; payload.downloads[] holds URLs that expire |
video.errored / image.errored / audio.errored | Job failed; payload.error has code and message |
A canceled job fires no webhook (canceled is a status only). Payloads have no event ID: dedupe on {payload.id}:{type}.
Delivery Behavior
Network failures, timeouts and non-2xx responses are retried at 1, 2, 4, 8, ... minutes (capped at 60); events older than 24 hours are marked failed. The timestamp and signature are regenerated for each attempt. No timeout value is documented, so acknowledge quickly. Webhooks are POST requests.
Environment Variables
MAGIC_HOUR_WEBHOOK_SECRET=whsec_your_webhook_secret_here
# Optional: reject timestamps older/newer than this many seconds (unset or 0 = off)
# MAGIC_HOUR_WEBHOOK_TOLERANCE_SECONDS=300
Local Development
Magic Hour can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 magic-hour --path /webhooks/magic-hour
Register the printed URL as the webhook Endpoint URL in the Magic Hour Developer Hub.
Resources
- references/overview.md - What Magic Hour webhooks are, events, payloads, retries
- references/setup.md - Create the webhook and get the secret in the Developer Hub
- references/verification.md - Signature details, gotchas, debugging
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests