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

# Webhooks

> Receive real-time notifications when content is ready

Instead of polling for status, webhooks push events to your server as they happen.

## Webhook Delivery Flow

This sequence diagram shows the full lifecycle of a typical content generation integration using webhooks:

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant Client as Your App
    participant API as Hrizn API
    participant WH as Your Webhook Endpoint
    Client->>API: POST /ideaclouds
    API-->>Client: 202 Accepted
    Note over API: IdeaCloud researching...
    API->>WH: POST ideacloud.completed
    Note over WH: Verify HMAC signature
    WH-->>API: 200 OK
    Client->>API: POST /content
    API-->>Client: 202 Accepted
    Note over API: Content generating...
    API->>WH: POST content.progress (multiple)
    WH-->>API: 200 OK
    API->>WH: POST content.completed
    WH-->>API: 200 OK
    Note over API: User publishes content...
    API->>WH: POST content.published
    WH-->>API: 200 OK
    Client->>API: GET /content/id/publish-data
    API-->>Client: 200 Full publish payload
```

## How It Works

<Steps>
  <Step title="Register a webhook URL">
    Select which events to subscribe to when creating the webhook.
  </Step>

  <Step title="Receive events">
    When an event occurs, Hrizn sends an HTTP POST to your URL with a JSON payload.
  </Step>

  <Step title="Verify the signature">
    Your server verifies the HMAC-SHA256 signature in the `X-Webhook-Signature` header.
  </Step>

  <Step title="Process and acknowledge">
    Process the payload and return a `2xx` response. If delivery fails, Hrizn retries with exponential backoff.
  </Step>
</Steps>

## Setting Up Webhooks

### Via the Dashboard

Go to the **API** tab in the Dealership Manager, switch to the **Webhooks** section, and click **Add Webhook**.

### Via the API

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST https://api.app.hrizn.io/v1/public/webhooks \
    -H "X-API-Key: hzk_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://your-server.com/webhooks/horizn",
      "events": ["content.progress", "content.completed", "ideacloud.completed"]
    }'
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch("https://api.app.hrizn.io/v1/public/webhooks", {
    method: "POST",
    headers: {
      "X-API-Key": "hzk_your_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: "https://your-server.com/webhooks/horizn",
      events: ["content.progress", "content.completed", "ideacloud.completed"],
    }),
  });
  const { data } = await res.json();
  // data.secret — store this securely
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  response = requests.post(
      "https://api.app.hrizn.io/v1/public/webhooks",
      headers={"X-API-Key": "hzk_your_key_here"},
      json={
          "url": "https://your-server.com/webhooks/horizn",
          "events": ["content.progress", "content.completed", "ideacloud.completed"],
      },
  )
  data = response.json()["data"]
  # data["secret"] — store this securely
  ```

  ```php PHP theme={"theme":{"light":"github-light","dark":"github-dark"}}
  $ch = curl_init("https://api.app.hrizn.io/v1/public/webhooks");
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => [
          "X-API-Key: hzk_your_key_here",
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "url" => "https://your-server.com/webhooks/horizn",
          "events" => ["content.progress", "content.completed", "ideacloud.completed"],
      ]),
      CURLOPT_RETURNTRANSFER => true,
  ]);
  $data = json_decode(curl_exec($ch));
  // $data->data->secret — store this securely
  ```
</CodeGroup>

The response includes a `secret` for verifying signatures.

<Warning>
  Store the signing secret in a secrets manager or environment variable. It is returned **only once** at creation time and cannot be retrieved later.
</Warning>

## Event Payload

Every webhook delivery sends a JSON payload:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "evt_1707000000000_abc1234",
  "type": "content.completed",
  "site_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "article_id": "a1b2c3d4-...",
    "article_type": "basic"
  },
  "timestamp": "2026-02-10T12:00:00.000Z",
  "api_version": "2025-01-01"
}
```

## Verifying Signatures

Every delivery includes an `X-Webhook-Signature` header with an HMAC-SHA256 signature of the request body:

```
X-Webhook-Signature: sha256=abc123def456...
```

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import { createHmac } from "crypto";

  function verifyWebhookSignature(body: string, signature: string, secret: string) {
    const expected = createHmac("sha256", secret).update(body).digest("hex");
    return `sha256=${expected}` === signature;
  }

  // Express middleware
  app.post("/webhooks/horizn", (req, res) => {
    const signature = req.headers["x-webhook-signature"];
    const isValid = verifyWebhookSignature(req.rawBody, signature, WEBHOOK_SECRET);

    if (!isValid) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(req.rawBody);
    console.log(`Received: ${event.type}`, event.data);

    res.status(200).send("OK");
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import hmac
  import hashlib

  def verify_signature(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode(), body, hashlib.sha256
      ).hexdigest()
      return f"sha256={expected}" == signature
  ```

  ```php PHP theme={"theme":{"light":"github-light","dark":"github-dark"}}
  function verifyWebhookSignature(string $body, string $signature, string $secret): bool {
      $expected = hash_hmac("sha256", $body, $secret);
      return "sha256={$expected}" === $signature;
  }

  // In your webhook handler
  $body = file_get_contents("php://input");
  $signature = $_SERVER["HTTP_X_WEBHOOK_SIGNATURE"] ?? "";

  if (!verifyWebhookSignature($body, $signature, $webhookSecret)) {
      http_response_code(401);
      exit("Invalid signature");
  }

  $event = json_decode($body, true);
  error_log("Received: " . $event["type"]);

  http_response_code(200);
  echo "OK";
  ```
</CodeGroup>

## Available Events

<AccordionGroup>
  <Accordion title="IdeaCloud Events">
    | Event                 | Description                 | Payload Data                       |
    | --------------------- | --------------------------- | ---------------------------------- |
    | `ideacloud.completed` | IdeaCloud research finished | `ideacloud_id`, `keyword`          |
    | `ideacloud.failed`    | IdeaCloud research failed   | `ideacloud_id`, `keyword`, `error` |
  </Accordion>

  <Accordion title="Content Events">
    | Event               | Description                 | Payload Data                                                                           |
    | ------------------- | --------------------------- | -------------------------------------------------------------------------------------- |
    | `content.progress`  | Generation stage transition | `article_id`, `article_type`, `component_type`, `stage`, `message`, `progress_percent` |
    | `content.completed` | Content generation finished | `article_id`, `article_type`                                                           |
    | `content.failed`    | Content generation failed   | `article_id`, `article_type`, `error`                                                  |
  </Accordion>

  <Accordion title="Content Approval Events">
    | Event                         | Description                                    | Payload Data                                            |
    | ----------------------------- | ---------------------------------------------- | ------------------------------------------------------- |
    | `content.approval.submitted`  | Content submitted for approval                 | `article_id`, `article_type`, `submitted_by`            |
    | `content.approval.approved`   | Content approved by reviewer                   | `article_id`, `article_type`, `approved_by`             |
    | `content.approval.rejected`   | Content changes requested by reviewer          | `article_id`, `article_type`, `rejected_by`, `reason`   |
    | `content.approval.escalated`  | Content approval escalated to next reviewer    | `article_id`, `article_type`, `escalated_to`            |
    | `content.approval.overridden` | Content approval overridden by authorized user | `article_id`, `article_type`, `overridden_by`, `reason` |
  </Accordion>

  <Accordion title="Publishing Events">
    | Event                 | Description                                               | Payload Data                                   |
    | --------------------- | --------------------------------------------------------- | ---------------------------------------------- |
    | `content.published`   | Content published and available via publish-data endpoint | `article_id`, `article_type`, `posting_status` |
    | `content.unpublished` | Content unpublished (reverted to draft)                   | `article_id`, `article_type`, `posting_status` |
    | `content.scheduled`   | Content scheduled for future publication                  | `article_id`, `article_type`, `posting_status` |
  </Accordion>

  <Accordion title="Quality Events">
    | Event                     | Description               | Payload Data                                    |
    | ------------------------- | ------------------------- | ----------------------------------------------- |
    | `compliance.completed`    | Compliance check finished | `article_id`, `overall_status`, `overall_score` |
    | `content_tools.completed` | Content tools generated   | `article_id`, `tools`                           |
  </Accordion>

  <Accordion title="Inventory Events">
    | Event                                   | Description                   | Payload Data                   |
    | --------------------------------------- | ----------------------------- | ------------------------------ |
    | `inventory.description.completed`       | Vehicle description generated | `vin`, `year`, `make`, `model` |
    | `inventory.description.batch_completed` | Batch descriptions finished   | `count`, `site_id`             |
    | `inventory.feed.updated`                | Inventory feed refreshed      | `vehicle_count`                |
  </Accordion>

  <Accordion title="Social Events">
    | Event                   | Description                                    | Payload Data                                                          |
    | ----------------------- | ---------------------------------------------- | --------------------------------------------------------------------- |
    | `social.post.published` | Social post published to one or more platforms | `id`, `status`, `platforms`                                           |
    | `social.post.scheduled` | Social post scheduled for future publication   | `id`, `status`, `platforms`                                           |
    | `social.post.failed`    | Social post failed to publish                  | `id`, `status`, `platforms`                                           |
    | `social.review.created` | A new social review was ingested               | `review_id`, `platform`, `star_rating`, `location_name`, `account_id` |
    | `social.review.updated` | A social review was updated                    | `review_id`, `platform`, `star_rating`, `location_name`, `account_id` |
  </Accordion>
</AccordionGroup>

## Content Progress Events

The `content.progress` event provides real-time visibility into content generation stages. Unlike terminal events (`content.completed`, `content.failed`), progress events fire at meaningful stage transitions — typically 4-6 events per content item.

### Stages

| Stage         | Description                                                         | Typical % |
| ------------- | ------------------------------------------------------------------- | --------- |
| `researching` | Vehicle data being fetched (comparisons & model landing pages only) | 5%        |
| `outlining`   | Article structure being created                                     | 5%        |
| `writing`     | Content generation started                                          | 10-80%    |
| `finalizing`  | Post-processing (links, TOC, formatting)                            | 95%       |

### Example Payload

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "evt_1707000000000_abc1234",
  "type": "content.progress",
  "site_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "article_id": "a1b2c3d4-...",
    "article_type": "comparison",
    "component_type": "Body",
    "stage": "writing",
    "message": "Writing content...",
    "progress_percent": 45
  },
  "timestamp": "2026-02-10T12:00:05.000Z",
  "api_version": "2025-01-01"
}
```

<Note>
  Subscribers must explicitly include `content.progress` in their `events` array when creating or updating a webhook subscription. Existing subscribers only receiving `content.completed` will not receive progress events unless they update their subscription.
</Note>

## Retry Policy

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart TD
    Deliver["Deliver webhook"] --> Check{"2xx response?"}
    Check -->|Yes| Done["Delivered ✓"]
    Check -->|No| R1["Retry 1: ~1 min"]
    R1 --> Check2{"2xx?"}
    Check2 -->|Yes| Done
    Check2 -->|No| R2["Retry 2: ~5 min"]
    R2 --> Check3{"2xx?"}
    Check3 -->|Yes| Done
    Check3 -->|No| R3["Retry 3: ~30 min"]
    R3 --> Check4{"2xx?"}
    Check4 -->|Yes| Done
    Check4 -->|No| R4["Retry 4: ~2 hrs"]
    R4 --> Check5{"2xx?"}
    Check5 -->|Yes| Done
    Check5 -->|No| DLQ["Dead letter queue"]
```

If your endpoint returns a non-2xx status or times out (15s), Hrizn retries with exponential backoff:

* **Attempt 1**: Immediate
* **Attempt 2**: \~1 minute later
* **Attempt 3**: \~5 minutes later
* **Attempt 4**: \~30 minutes later
* **Attempt 5**: \~2 hours later

After **5 failed attempts**, the delivery is sent to a dead letter queue.

<Danger>
  After **10 consecutive failures** across different events, the webhook subscription is **automatically disabled**. You'll need to re-enable it from the dashboard or via `PATCH /webhooks/{id}`.
</Danger>

## Testing

Send a test ping to verify your webhook URL:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST https://api.app.hrizn.io/v1/public/webhooks/{id}/test \
    -H "X-API-Key: hzk_your_key_here"
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch(
    "https://api.app.hrizn.io/v1/public/webhooks/{id}/test",
    {
      method: "POST",
      headers: { "X-API-Key": "hzk_your_key_here" },
    },
  );
  const { data } = await res.json();
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  response = requests.post(
      "https://api.app.hrizn.io/v1/public/webhooks/{id}/test",
      headers={"X-API-Key": "hzk_your_key_here"},
  )
  data = response.json()["data"]
  ```

  ```php PHP theme={"theme":{"light":"github-light","dark":"github-dark"}}
  $ch = curl_init("https://api.app.hrizn.io/v1/public/webhooks/{id}/test");
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => ["X-API-Key: hzk_your_key_here"],
      CURLOPT_RETURNTRANSFER => true,
  ]);
  $data = json_decode(curl_exec($ch));
  ```
</CodeGroup>

This sends a `test.ping` event to your URL and returns the response status.

<Tip>
  Use [ngrok](https://ngrok.com) or a similar tunneling tool to expose a local development server for testing webhooks without deploying.
</Tip>

## Managing Webhooks

| Action     | Method   | Endpoint                    |
| ---------- | -------- | --------------------------- |
| Create     | `POST`   | `/webhooks`                 |
| List       | `GET`    | `/webhooks`                 |
| Update     | `PATCH`  | `/webhooks/{id}`            |
| Delete     | `DELETE` | `/webhooks/{id}`            |
| Test       | `POST`   | `/webhooks/{id}/test`       |
| Deliveries | `GET`    | `/webhooks/{id}/deliveries` |
