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

# Webhooks Overview

> Inbound GitHub webhooks, outbound generation events and polling alternatives

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

<Warning>
  Notra can also send signed [outbound webhooks](/docs/api/webhooks/outbound) to your own endpoint. Set them up in **Settings**, then **Webhooks**, or through `/v1/webhooks`. The GitHub endpoint below only receives events and has nothing to do with outbound subscriptions.
</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={"theme":{"light":"github-light","dark":"github-dark"}}
POST https://app.usenotra.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 Notra">
    Go to **Integrations**, then **GitHub**, and open the repository you connected. Notra 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 Notra">
    Click **I've added the webhook**. GitHub sends a `ping` event as soon as the webhook is saved; Notra 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 Notra and use **Regenerate** in the webhook section. The old secret stops working immediately, so update the webhook on GitHub right after regenerating.

## Security

Notra 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. Notra 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

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

<CodeGroup>
  ```json Processed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "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={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "message": "Pong! Webhook configured successfully",
    "event": "ping",
    "delivery": "12345678-1234-1234-1234-123456789abc"
  }
  ```

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

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

  ```json Duplicate theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "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 Notra 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">
  Notra 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={"theme":{"light":"github-light","dark":"github-dark"}}
  { "error": "Invalid webhook signature" }
  ```

  ```json Integration disabled theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "error": "Integration is disabled" }
  ```

  ```json Missing event header theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "error": "Missing X-GitHub-Event header" }
  ```

  ```json Secret not generated theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "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.

## Polling alternatives

Post generation and brand analysis can send [outbound events](/docs/api/webhooks/outbound). You can still poll them if you prefer. GEO scans don't send outbound events yet, so polling is the way to go there:

<CardGroup cols={2}>
  <Card title="Poll post generation" icon="rotate" href="/docs/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="/docs/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={"theme":{"light":"github-light","dark":"github-dark"}}
  JOB_ID=$(curl -s https://api.usenotra.com/v1/posts/generate \
    -H "Authorization: Bearer $NOTRA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"contentType":"changelog","lookbackWindow":"last_7_days"}' | jq -r '.job.id')

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

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const headers = { Authorization: `Bearer ${process.env.NOTRA_API_KEY}` };

  const queued = await fetch("https://api.usenotra.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.usenotra.com/v1/posts/generate/${jobId}`, { headers })
      .then((res) => res.json());
    job = status.job;
  }

  if (job.status === "completed") {
    const { post } = await fetch(`https://api.usenotra.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="/docs/api/webhooks/events">
    Which GitHub events Notra processes, how they are filtered, and what the processed payload looks like
  </Card>

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.