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

# Webhooks Overview

> How Kolaria receives GitHub webhooks, and what to use instead of outbound webhooks

Webhooks in Kolaria are inbound. GitHub sends events to Kolaria so that event triggers can generate content the moment a release is published or commits land on the default branch.

<Warning>
  Kolaria does not send outbound webhooks to your servers. There is no way to register a callback URL, and Kolaria never signs or delivers events to third-party endpoints. To react to things that happen in Kolaria, poll the job endpoints or read the agent session stream. See [No outbound webhooks](#no-outbound-webhooks) below.
</Warning>

## Inbound webhooks from GitHub

GitHub is the only provider with a working webhook setup. Linear, Slack, and the other integrations connect through their APIs and do not use this endpoint.

### Payload URL

Each connected repository gets its own payload URL:

```text theme={null}
POST https://app.kolaria.com/api/webhooks/github/{organizationId}/{integrationId}/{repositoryId}
```

The dashboard generates this URL for you. Copy it from the **Setup Webhook** dialog rather than assembling it by hand: the IDs are internal and the URL is checked against the organization and integration on every delivery.

### Set up the webhook

<Steps>
  <Step title="Open the repository in Kolaria">
    Go to **Integrations**, then **GitHub**, and open the repository you connected. Kolaria also shows the Setup Webhook dialog right after you add a repository.
  </Step>

  <Step title="Copy the Payload URL and Secret">
    The dialog shows three values: the **Payload URL**, the content type (`application/json`), and the **Secret**. A secret is generated automatically the first time you open the dialog. Use the copy buttons; the secret is masked until you focus the field.
  </Step>

  <Step title="Add the webhook on GitHub">
    In your repository on GitHub, open **Settings**, then **Webhooks**, then **Add webhook**. Paste the Payload URL, set the content type to `application/json`, paste the Secret, and choose **Let me select individual events**. Select **Pushes** and **Releases**. You can also select **Pull requests** if you want merged pull requests recorded for Iris.
  </Step>

  <Step title="Confirm in Kolaria">
    Click **I've added the webhook**. GitHub sends a `ping` event as soon as the webhook is saved; Kolaria answers it and writes a log entry so you can confirm the connection worked.
  </Step>
</Steps>

<Note>
  Without a webhook you can still generate content manually, on a schedule, or through `POST /v1/posts/generate`. Webhooks are only needed for event triggers.
</Note>

### Rotate the secret

Open the repository page in Kolaria and use **Regenerate** in the webhook section. The old secret stops working immediately, so update the webhook on GitHub right after regenerating.

## Security

Kolaria verifies every delivery before reading the payload:

* **Signature**: GitHub signs the body with HMAC SHA-256 using your secret and sends it in the `X-Hub-Signature-256` header. Kolaria recomputes the signature and compares it in constant time. A missing header returns `400`; a mismatch returns `401`.
* **Ownership checks**: The organization, integration, and repository IDs in the URL must match each other. Deliveries to a disabled integration, or with IDs that do not belong together, are rejected with `403`.
* **Deduplication**: The `X-GitHub-Delivery` header is remembered for 24 hours. A redelivery of the same ID returns `200` with `"duplicate": true` and is not processed again.

## Responses

Kolaria answers every delivery with JSON. GitHub shows the body under **Recent Deliveries** in your webhook settings.

<CodeGroup>
  ```json Processed theme={null}
  {
    "message": "Processed release event (published)",
    "event": "release",
    "delivery": "12345678-1234-1234-1234-123456789abc",
    "processed": {
      "type": "release",
      "action": "published",
      "data": { "tagName": "v1.2.0", "name": "Version 1.2.0", "body": "...", "prerelease": false, "draft": false, "publishedAt": "2026-09-01T14:30:00Z", "url": "https://github.com/owner/repo/releases/tag/v1.2.0" }
    },
    "repository": { "id": 123456, "fullName": "owner/repo" }
  }
  ```

  ```json Ping theme={null}
  {
    "message": "Pong! Webhook configured successfully",
    "event": "ping",
    "delivery": "12345678-1234-1234-1234-123456789abc"
  }
  ```

  ```json Filtered theme={null}
  {
    "message": "Event 'push' with action '' was filtered out",
    "event": "push",
    "action": "",
    "filtered": true
  }
  ```

  ```json Ignored theme={null}
  {
    "message": "Event type 'issues' is not supported",
    "event": "issues",
    "ignored": true
  }
  ```

  ```json Duplicate theme={null}
  {
    "message": "Webhook already processed (duplicate delivery)",
    "event": "push",
    "delivery": "12345678-1234-1234-1234-123456789abc",
    "duplicate": true
  }
  ```
</CodeGroup>

### Error responses

<ResponseField name="400 Bad Request" type="error">
  Invalid URL parameters, missing `X-GitHub-Event` or `X-Hub-Signature-256` header, no webhook secret generated for the repository yet, or a body that is not valid JSON in the expected shape.
</ResponseField>

<ResponseField name="401 Unauthorized" type="error">
  The signature does not match the repository's secret. Regenerate the secret in Kolaria and update GitHub if they drifted apart.
</ResponseField>

<ResponseField name="403 Forbidden" type="error">
  The integration is disabled, does not belong to the organization in the URL, or the repository does not belong to the integration.
</ResponseField>

<ResponseField name="404 Not Found" type="error">
  The integration or repository was deleted.
</ResponseField>

<ResponseField name="500 Internal Server Error" type="error">
  Kolaria could not finish processing the delivery. GitHub keeps the delivery in its history so you can redeliver it from the webhook settings page.
</ResponseField>

<ResponseField name="501 Not Implemented" type="error">
  The provider segment in the URL is not `github`.
</ResponseField>

<CodeGroup>
  ```json Invalid signature theme={null}
  { "error": "Invalid webhook signature" }
  ```

  ```json Integration disabled theme={null}
  { "error": "Integration is disabled" }
  ```

  ```json Missing event header theme={null}
  { "error": "Missing X-GitHub-Event header" }
  ```

  ```json Secret not generated theme={null}
  { "error": "Webhook secret not configured for this repository" }
  ```
</CodeGroup>

## Logs

Every delivery, including rejected ones, is written to **Settings**, then **Logs** in the dashboard with its status, HTTP status code, GitHub delivery ID, and payload summary. Retention is 7, 14, or 30 days depending on your plan.

## No outbound webhooks

Kolaria does not push notifications when a post is generated, a brand identity finishes analyzing, or a GEO scan completes. Use these instead:

<CardGroup cols={2}>
  <Card title="Poll post generation" icon="rotate" href="/api-reference/content/get-async-post-generation-status">
    `GET /v1/posts/generate/{jobId}` returns the job and its event log. Stop polling when `job.status` is `completed`, `failed`, or `skipped`; `job.postId` is set on completion.
  </Card>

  <Card title="Poll brand analysis" icon="palette" href="/api-reference/content/get-async-brand-identity-generation-status">
    `GET /v1/brand-identities/generate/{jobId}` reports `queued`, `running`, `completed`, or `failed` along with the current step.
  </Card>

  <Card title="Poll GEO scans" icon="radar">
    `POST /v1/projects/{projectId}/geo/scans` returns a `statusUrl` and a `Location` header. Poll `GET /v1/projects/{projectId}/geo/scans/{scanId}` until `scan.status` leaves `running`.
  </Card>

  <Card title="Stream agent sessions" icon="wave-pulse">
    `GET /v2/eve/v1/session/{sessionId}/stream` is a durable, replayable newline-delimited JSON stream. Pass `startIndex` to resume from a known position.
  </Card>
</CardGroup>

<CodeGroup>
  ```bash curl theme={null}
  JOB_ID=$(curl -s https://api.kolaria.com/v1/posts/generate \
    -H "Authorization: Bearer $KOLARIA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"contentType":"changelog","lookbackWindow":"last_7_days"}' | jq -r '.job.id')

  until curl -s "https://api.kolaria.com/v1/posts/generate/$JOB_ID" \
    -H "Authorization: Bearer $KOLARIA_API_KEY" \
    | jq -e '.job.status | IN("completed","failed","skipped")' > /dev/null; do
    sleep 10
  done
  ```

  ```typescript TypeScript theme={null}
  const headers = { Authorization: `Bearer ${process.env.KOLARIA_API_KEY}` };

  const queued = await fetch("https://api.kolaria.com/v1/posts/generate", {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ contentType: "changelog", lookbackWindow: "last_7_days" }),
  }).then((res) => res.json());

  const jobId: string = queued.job.id;
  const terminal = new Set(["completed", "failed", "skipped"]);

  let job = queued.job;
  while (!terminal.has(job.status)) {
    await new Promise((resolve) => setTimeout(resolve, 10_000));
    const status = await fetch(`https://api.kolaria.com/v1/posts/generate/${jobId}`, { headers })
      .then((res) => res.json());
    job = status.job;
  }

  if (job.status === "completed") {
    const { post } = await fetch(`https://api.kolaria.com/v1/posts/${job.postId}`, { headers })
      .then((res) => res.json());
    console.log(post.title);
  }
  ```
</CodeGroup>

<Tip>
  Polling counts against the standard per-key rate limits. Ten seconds between checks is plenty; generation jobs usually take a few minutes.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Event reference" icon="bell" href="/api/webhooks/events">
    Which GitHub events Kolaria processes, how they are filtered, and what the processed payload looks like
  </Card>

  <Card title="Event triggers" icon="bolt" href="/automation/event-based">
    Turn webhook events into generated content
  </Card>
</CardGroup>
