> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rimp.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive `generation.completed` events instead of polling. HMAC-signed and retried for 24 hours.

Webhooks let your backend react to Rimp events without polling. Every payload is HMAC-signed; verify the signature before trusting it.

## Events

| Event                    | When                                                      |
| ------------------------ | --------------------------------------------------------- |
| `generation.completed`   | Any image or video generation reaches `succeeded`         |
| `generation.failed`      | Generation reaches `failed` (refund issued automatically) |
| `comparison.completed`   | All children of a `MultiModelComparison` reach terminal   |
| `usage.budget_threshold` | API key reaches 50%, 80%, or 100% of `monthly_budget_usd` |
| `usage.anomaly_detected` | Spending anomaly detector pauses the org                  |

## Subscribing

```bash theme={null}
curl -X POST https://api.rimp.example/v1/webhooks \
  -H "Authorization: Bearer $RIMP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.your-app.com/webhooks/rimp",
    "events": ["generation.completed", "generation.failed"]
  }'
```

Response (the `secret` is shown ONCE):

```json theme={null}
{
  "id": "whk_abc",
  "url": "https://api.your-app.com/webhooks/rimp",
  "events": ["generation.completed", "generation.failed"],
  "secret": "whsec_..."
}
```

Use `"*"` in the events array to subscribe to everything.

## Payload format

```json theme={null}
{
  "id": "evt_abc",
  "type": "generation.completed",
  "created_at": "2026-05-19T18:24:01Z",
  "data": {
    "generation_id": "gen_xyz",
    "modality": "video",
    "status": "succeeded",
    "charged_credits": 600
  }
}
```

Always parse the `id` first — events are guaranteed unique per delivery attempt, so use it as your dedupe key.

## Verifying the signature

Every delivery includes:

```http theme={null}
POST /webhooks/rimp HTTP/1.1
Content-Type: application/json
X-Rimp-Signature: t=1715900400,v1=ad8c1b7f...
X-Rimp-Event: generation.completed
X-Rimp-Delivery: whd_abc
```

The signature is HMAC-SHA-256 over `${timestamp}.${rawBody}` with your webhook secret as the key.

<CodeGroup>
  ```ts TypeScript theme={null}
  import crypto from 'node:crypto';

  function verify(rawBody: string, header: string, secret: string): boolean {
    const parts = Object.fromEntries(
      header.split(',').map((p) => p.split('=', 2) as [string, string]),
    );
    const ts = Number(parts.t);
    const sig = parts.v1;
    if (!ts || !sig) return false;

    // Reject events older than 5 minutes (replay protection)
    if (Math.abs(Date.now() / 1000 - ts) > 300) return false;

    const expected = crypto
      .createHmac('sha256', secret)
      .update(`${ts}.${rawBody}`)
      .digest('hex');

    return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'));
  }

  // Express handler
  app.post('/webhooks/rimp', express.raw({ type: 'application/json' }), (req, res) => {
    const ok = verify(
      req.body.toString('utf8'),
      req.headers['x-rimp-signature'] as string,
      process.env.RIMP_WEBHOOK_SECRET!,
    );
    if (!ok) return res.status(401).end();

    const event = JSON.parse(req.body.toString('utf8'));
    // ... handle event
    res.status(204).end();
  });
  ```

  ```python Python theme={null}
  import hmac, hashlib, time
  from fastapi import Request, HTTPException

  def verify(raw_body: bytes, header: str, secret: str) -> bool:
      parts = dict(part.split('=', 1) for part in header.split(','))
      ts = int(parts.get('t', 0))
      sig = parts.get('v1', '')
      if not ts or not sig: return False
      if abs(time.time() - ts) > 300: return False
      expected = hmac.new(
          secret.encode(), f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, sig)

  @app.post("/webhooks/rimp")
  async def handler(req: Request):
      raw = await req.body()
      sig = req.headers.get("x-rimp-signature", "")
      if not verify(raw, sig, os.environ["RIMP_WEBHOOK_SECRET"]):
          raise HTTPException(401)
      event = await req.json()
      # ... handle event
  ```
</CodeGroup>

## Retry policy

If your endpoint doesn't return 2xx within 15s, Rimp retries with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | immediate  |
| 2       | 1 minute   |
| 3       | 5 minutes  |
| 4       | 30 minutes |
| 5       | 2 hours    |
| 6       | 12 hours   |

After 6 attempts the delivery is marked `exhausted`. You can replay any delivery from the dashboard.

## Best practices

* **Return 2xx fast.** Acknowledge the event, then process async. Heavy work should never run in the webhook handler.
* **Dedupe by `event.id`.** Same event can be delivered more than once during retries.
* **Verify before trusting.** Don't parse the body before checking the signature.
* **Pin the secret.** Each webhook has its own `whsec_…`. Store it in your env per environment.
* **Listen for `*` in dev.** Easier than subscribing to each event individually.
