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

# Hitorino Webhooks API — Register, List, and Delete

> Register webhook endpoints to receive real-time Hitorino events. Covers stream, video, and subscription events, payload schema, and signature verification.

Webhooks let your server receive real-time notifications when things happen on Hitorino — a stream goes live, a video is published, a viewer subscribes. Instead of polling the API repeatedly, you register a URL and Hitorino delivers a signed HTTP `POST` request to it whenever a matching event occurs. This page covers how to register, list, and remove webhook endpoints, as well as how to verify incoming payloads.

***

## Webhook Events

Hitorino emits the following event types. You specify which events a webhook endpoint should receive when you register it.

| Event                    | Trigger                                                          |
| ------------------------ | ---------------------------------------------------------------- |
| `stream.started`         | A live stream transitions from `scheduled` or standby to `live`. |
| `stream.ended`           | A live stream ends and transitions to `ended`.                   |
| `video.published`        | A new on-demand video is made publicly available.                |
| `subscription.created`   | A viewer subscribes to a creator.                                |
| `subscription.cancelled` | A viewer cancels their subscription to a creator.                |

***

## Register a Webhook

Registers a new webhook endpoint. Hitorino immediately sends a `POST` request to your URL with a `ping` event to verify it's reachable. Your endpoint must respond with `2xx` within 5 seconds.

```bash theme={null}
POST /webhooks
```

<Note>
  This endpoint requires a **write-scoped** API key.
</Note>

### Request Body

<ParamField body="url" type="string" required>
  The HTTPS URL of your webhook endpoint. Must use `https://`. Hitorino does not support `http://` endpoints.
</ParamField>

<ParamField body="events" type="array" required>
  Array of event type strings to subscribe to. Must contain at least one item. Use `["*"]` to subscribe to all current and future events.
</ParamField>

### Request Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.hitorino.tv/v1/webhooks \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://yourapp.com/webhooks/hitorino",
      "events": ["stream.started", "stream.ended", "video.published"]
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.hitorino.tv/v1/webhooks', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://yourapp.com/webhooks/hitorino',
      events: ['stream.started', 'stream.ended', 'video.published'],
    }),
  });

  const webhook = await response.json();
  console.log(webhook.id);     // "wh_01HWBK1234"
  console.log(webhook.secret); // save this — shown only once!
  ```

  ```python Python theme={null}
  import requests

  headers = {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
  }

  payload = {
      'url': 'https://yourapp.com/webhooks/hitorino',
      'events': ['stream.started', 'stream.ended', 'video.published'],
  }

  response = requests.post(
      'https://api.hitorino.tv/v1/webhooks',
      json=payload,
      headers=headers,
  )
  webhook = response.json()
  print(webhook['id'])      # "wh_01HWBK1234"
  print(webhook['secret'])  # save this — shown only once!
  ```
</CodeGroup>

### Response

Returns `201 Created` with the newly registered webhook object.

```json theme={null}
{
  "id": "wh_01HWBK1234",
  "url": "https://yourapp.com/webhooks/hitorino",
  "events": ["stream.started", "stream.ended", "video.published"],
  "secret": "whsec_a7b3c9d1e2f4g5h6i7j8k9l0m1n2o3p4q5r6s7t8",
  "status": "active",
  "created_at": "2024-06-15T10:00:00Z"
}
```

<ResponseField name="id" type="string">
  Unique identifier for the webhook registration, prefixed with `wh_`.
</ResponseField>

<ResponseField name="url" type="string">
  The HTTPS endpoint URL you registered.
</ResponseField>

<ResponseField name="events" type="array">
  Array of event type strings this webhook is subscribed to.
</ResponseField>

<ResponseField name="secret" type="string">
  A webhook signing secret used to verify incoming payloads. This value is returned **only once**, at creation time. Store it securely — you cannot retrieve it again. If you lose it, delete this webhook and create a new one.
</ResponseField>

<ResponseField name="status" type="string">
  Current status of the webhook registration. One of `active` or `disabled`. Hitorino may automatically disable a webhook after repeated delivery failures.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp of when the webhook was registered.
</ResponseField>

<Warning>
  Copy the `secret` immediately. It is displayed exactly once in the registration response. Store it in your secrets manager alongside your API key.
</Warning>

***

## List Webhooks

Returns all webhook endpoints registered to your account.

```bash theme={null}
GET /webhooks
```

### Request Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.hitorino.tv/v1/webhooks \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.hitorino.tv/v1/webhooks', {
    headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
  });

  const { data } = await response.json();
  data.forEach(wh => console.log(wh.id, wh.url, wh.status));
  ```

  ```python Python theme={null}
  import requests

  headers = {'Authorization': 'Bearer YOUR_API_KEY'}
  response = requests.get('https://api.hitorino.tv/v1/webhooks', headers=headers)

  for webhook in response.json()['data']:
      print(webhook['id'], webhook['url'], webhook['status'])
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "data": [
    {
      "id": "wh_01HWBK1234",
      "url": "https://yourapp.com/webhooks/hitorino",
      "events": ["stream.started", "stream.ended", "video.published"],
      "status": "active",
      "created_at": "2024-06-15T10:00:00Z"
    }
  ]
}
```

<Note>
  The `secret` field is **not** returned on list responses. Only the registration response (`POST /webhooks`) includes the secret.
</Note>

***

## Delete a Webhook

Permanently removes a webhook registration. Hitorino immediately stops delivering events to the registered URL. This action cannot be undone.

```bash theme={null}
DELETE /webhooks/{id}
```

<Note>
  This endpoint requires a **write-scoped** API key.
</Note>

### Path Parameters

<ParamField path="id" type="string" required>
  The unique webhook ID to remove (e.g., `wh_01HWBK1234`).
</ParamField>

### Request Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.hitorino.tv/v1/webhooks/wh_01HWBK1234 \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const webhookId = 'wh_01HWBK1234';

  const response = await fetch(`https://api.hitorino.tv/v1/webhooks/${webhookId}`, {
    method: 'DELETE',
    headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
  });

  if (response.ok) {
    console.log('Webhook deleted successfully.');
  }
  ```

  ```python Python theme={null}
  import requests

  webhook_id = 'wh_01HWBK1234'
  headers = {'Authorization': 'Bearer YOUR_API_KEY'}

  response = requests.delete(
      f'https://api.hitorino.tv/v1/webhooks/{webhook_id}',
      headers=headers,
  )

  if response.ok:
      print('Webhook deleted successfully.')
  ```
</CodeGroup>

### Response

Returns `200 OK` with a confirmation object.

```json theme={null}
{
  "deleted": true,
  "id": "wh_01HWBK1234"
}
```

***

## Webhook Payload

When a subscribed event occurs, Hitorino sends a `POST` request to your endpoint with a JSON body structured as follows.

```json theme={null}
{
  "id": "evt_01HEVT9876",
  "type": "stream.started",
  "created_at": "2024-06-10T22:00:00Z",
  "data": {
    "id": "str_01HXYZ1234",
    "title": "Late Night Lo-Fi Coding",
    "status": "live",
    "creator_id": "usr_01HABC5678",
    "started_at": "2024-06-10T22:00:00Z",
    "stream_url": "https://watch.hitorino.tv/str_01HXYZ1234"
  }
}
```

<ResponseField name="id" type="string">
  Unique identifier for this event delivery, prefixed with `evt_`. Use this for idempotency checks in your handler.
</ResponseField>

<ResponseField name="type" type="string">
  The event type string that triggered this delivery (e.g., `stream.started`).
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp of when the event was generated by Hitorino.
</ResponseField>

<ResponseField name="data" type="object">
  The event payload. The shape of this object varies by event type and mirrors the corresponding API resource object (stream, video, or subscription).
</ResponseField>

***

## Signature Verification

Every webhook delivery includes an `X-Hitorino-Signature` header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook `secret`. You should always verify this signature before processing the payload to ensure the request genuinely came from Hitorino.

### How Signatures Work

1. Hitorino computes `HMAC-SHA256(rawBody, secret)` and hex-encodes the result.
2. The hex digest is sent in the `X-Hitorino-Signature` header.
3. Your server recomputes the same HMAC using the raw request body and your stored secret, then compares it to the header value using a constant-time comparison.

<Warning>
  Always verify the signature **before** deserializing or acting on the webhook payload. Skipping this step makes your endpoint vulnerable to forged events.
</Warning>

### Verification Examples

<CodeGroup>
  ```javascript JavaScript (Node.js) theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(rawBody, signature, secret) {
    const expectedSig = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');

    // Use timingSafeEqual to prevent timing attacks
    const sigBuffer = Buffer.from(signature, 'hex');
    const expectedBuffer = Buffer.from(expectedSig, 'hex');

    if (sigBuffer.length !== expectedBuffer.length) return false;
    return crypto.timingSafeEqual(sigBuffer, expectedBuffer);
  }

  // Express.js example
  app.post('/webhooks/hitorino', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-hitorino-signature'];
    const secret = process.env.HITORINO_WEBHOOK_SECRET;

    if (!verifyWebhookSignature(req.body, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body);
    console.log('Verified event:', event.type);

    res.status(200).json({ received: true });
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import os
  from flask import Flask, request, jsonify

  app = Flask(__name__)

  def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode('utf-8'),
          raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature)

  @app.route('/webhooks/hitorino', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Hitorino-Signature', '')
      secret = os.environ['HITORINO_WEBHOOK_SECRET']

      if not verify_webhook_signature(request.get_data(), signature, secret):
          return jsonify({'error': 'Invalid signature'}), 401

      event = request.get_json()
      print(f"Verified event: {event['type']}")

      return jsonify({'received': True}), 200
  ```
</CodeGroup>

### Retry Behavior

If your endpoint responds with a non-`2xx` status code or times out (after 5 seconds), Hitorino retries the delivery with exponential backoff up to **5 attempts** over approximately 24 hours. After all retries are exhausted, the event is marked as failed and no further attempts are made.

<Tip>
  To handle retries safely, use the `id` field of the webhook payload as an idempotency key. Store processed event IDs in your database and skip any event whose `id` you've already handled.
</Tip>
