# Gumlet Webhooks

Gumlet (video hosting and image optimisation) sends a JSON `POST` when something happens
asynchronously: a video asset becomes ready or errors, a live stream starts or ends, a viewer submits
a form in the player. Webhooks are **not signed**. If you set the optional **Secret Token** on the
webhook, Gumlet sends it back, as plain text, in the `x-gumlet-token` header of every request, and
your endpoint authenticates the request by comparing that header with the secret you stored.

> Set a Secret Token on the Gumlet webhook. Webhooks without one send nothing to verify.

## When to Use This Skill

* How do I receive Gumlet webhooks?
* How do I verify the `x-gumlet-token` header?
* Does Gumlet sign webhook payloads? (No: use the Secret Token)
* How do I handle `video.status.ready`, `video.status.errored` or `live.video.status.complete`?
* Why does my Gumlet 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 verifyGumletRequest(headers, secret = process.env.GUMLET_WEBHOOK_SECRET) {
  if (!secret) return false;                   // never accept when unconfigured
  const received = headers['x-gumlet-token'];  // the whole Secret Token, plain text
  return Boolean(received) && safeEqual(received, secret);
}

```

* Header: `x-gumlet-token: <Secret Token>` (the entire value, no prefix, no signature).
* Return `401` on a missing header or a mismatch, `400` for a body that isn't a JSON object, `200`
  once handled. Any 2xx counts as delivered.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS, and rotate the Secret Token if it leaks.

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

## Event Types

The JSON body has a `type` field and no common envelope; dispatch on `type`.

| Event | Sent When |
| --- | --- |
| `video.status.created` | A video asset is created |
| `video.status.stream_ready` | Video can be streamed at a single resolution |
| `video.status.downloaded` | Video file is downloaded for processing |
| `video.status.processed` | Video is processed |
| `video.status.ready` | Video is ready to embed |
| `video.status.errored` | Processing failed |
| `video.status.deleted` | Video asset is deleted |
| `video.status.repackaged` | A subtitle or audio stream was added |
| `live.video.status.created` | A live session is created |
| `live.video.status.preparing` | A live session is preparing to start |
| `live.video.status.ready` | A live session is ready for streaming |
| `live.video.status.connected` | A live session connected to the source |
| `live.video.status.active` | A live session is streaming |
| `live.video.status.disconnected` | A live session disconnected from the source |
| `live.video.status.complete` | A live session ended |
| `event.video.uploaded` | A video is uploaded |
| `event.video.updated` | Title, tags, description or privacy changed |
| `event.embed.viewed` | A video is viewed for the first time |
| `event.embed.cta_clicked` | A call-to-action is clicked in the embed |
| `event.embed.form_submitted` | A lead is captured |
| `event.playlist.created` | A playlist is created |
| `event.playlist.asset` | An asset is added to or removed from a playlist |
| `event.playlist.deleted` | A playlist is deleted |
| `event.comment.all` | A comment is added, replied to or resolved |
| `event.channel.member_joined` | A new channel member joined |

Video status payloads carry `asset_id`, `status`, `input` and `output`; live payloads carry
`live_asset_id`, `status`, `input` and `output`.

## Delivery

Gumlet retries any non-2xx response every 30 minutes for 24 hours, so make handlers idempotent.

## Environment Variables

```bash
GUMLET_WEBHOOK_SECRET=your-secret-token   # the Secret Token set on the Gumlet webhook

```

## Local Development

Gumlet can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 gumlet --path /webhooks/gumlet

```

Use the printed URL as the webhook endpoint URL in Gumlet.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gumlet-webhooks/references/overview.md) - What Gumlet webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gumlet-webhooks/references/setup.md) - Create the webhook and set the Secret Token
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gumlet-webhooks/references/verification.md) - Token check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/gumlet-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests