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

# Webhooks

> Get notified when a review finishes instead of polling.

Rather than polling, you can register a webhook endpoint to be notified when
a review reaches a terminal state.

## Events

| Event              | Fired when                                                                      |
| ------------------ | ------------------------------------------------------------------------------- |
| `review.completed` | The review finished and its report is available.                                |
| `review.failed`    | The review could not be completed.                                              |
| `review.amended`   | A released review was corrected; fetch the report again for the amended record. |

## Payload

```json theme={null}
{
  "event": "review.completed",
  "review_id": "9b1f....",
  "reference_id": "LN-2026-0042",
  "status": "completed",
  "occurred_at": "2026-05-30T18:09:12Z"
}
```

The payload intentionally carries only identifiers and status; fetch
`GET /reviews/{review_id}/report` to retrieve the result over an
authenticated channel.

## Verifying signatures

Each delivery is signed so you can verify it came from Flightline. The
signature is sent in the `X-Flightline-Signature` header as
`t=<timestamp>,v1=<hex-hmac>`, where the HMAC is `HMAC_SHA256(secret, "<t>.<raw-body>")`
using the signing secret shown when you register the endpoint.

```python theme={null}
import hashlib, hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = f"{parts['t']}.{raw_body.decode()}".encode()
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

Reject any delivery whose signature does not verify, or whose timestamp is
older than a few minutes.

## Managing endpoints

Register, list, and delete your webhook endpoints with your organization API
key (`Authorization: Bearer <key>`), the same key you use for every other
`/v1` call.

| Method | Path                         | Purpose                                                |
| ------ | ---------------------------- | ------------------------------------------------------ |
| POST   | `/v1/webhook-endpoints`      | Register an endpoint; returns the signing secret once. |
| GET    | `/v1/webhook-endpoints`      | List your registered endpoints.                        |
| DELETE | `/v1/webhook-endpoints/{id}` | Delete an endpoint.                                    |

### Register

```bash theme={null}
curl -X POST https://api.flightlinehq.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $FLIGHTLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/flightline", "description": "prod"}'
```

```json theme={null}
{
  "id": "5f0c....",
  "url": "https://example.com/hooks/flightline",
  "enabled": true,
  "description": "prod",
  "secret_hint": "whsec_…a1b2",
  "created_at": "2026-05-30T18:09:12Z",
  "signing_secret": "whsec_3f9c...store-this-now"
}
```

<Warning>
  The `signing_secret` is returned **only** in this registration response. Store
  it immediately; it cannot be retrieved again. List and delete responses
  return only a non-reversible `secret_hint`, never the secret itself.
</Warning>

The endpoint URL must be **HTTPS** and resolve to a public host. URLs pointing
at internal, private, loopback, link-local, or cloud-metadata addresses are
rejected with a `400`.

### List

```bash theme={null}
curl https://api.flightlinehq.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $FLIGHTLINE_API_KEY"
```

```json theme={null}
{
  "endpoints": [
    {
      "id": "5f0c....",
      "url": "https://example.com/hooks/flightline",
      "enabled": true,
      "description": "prod",
      "secret_hint": "whsec_…a1b2",
      "created_at": "2026-05-30T18:09:12Z"
    }
  ]
}
```

### Delete

```bash theme={null}
curl -X DELETE https://api.flightlinehq.com/v1/webhook-endpoints/5f0c... \
  -H "Authorization: Bearer $FLIGHTLINE_API_KEY"
```

Returns `204 No Content`. Endpoints are scoped to your organization; an id
that belongs to another organization returns `404`.

## Delivery, retries, and security

* **Transport:** `POST` with a JSON body to your registered `https` URL.
  Endpoints must be `https`; internal/private hosts are rejected at
  registration.
* **Retries:** up to **3 attempts** with exponential backoff (\~0.5s, 1s) on a
  non-2xx response or transport error. Return a `2xx` quickly (within \~10s) to
  acknowledge; we time out a slow endpoint and retry.
* **Best-effort:** webhooks are a convenience, not the source of truth. If
  every attempt fails, the review state is unchanged; always treat
  `GET /reviews/{review_id}` / `…/report` as authoritative, and reconcile on a
  schedule as a backstop.
* **Verify the signature** (see above) on every delivery. Reject any delivery
  whose signature doesn't verify, or whose `t` timestamp is more than **5
  minutes** old, to prevent replay.
* **Idempotency:** a delivery may arrive more than once. De-duplicate on
  (`review_id`, `event`); processing should be idempotent.
* **Source:** deliveries originate from Flightline's infrastructure without a
  fixed source-IP range; authenticate them by **verifying the signature**,
  not by IP allowlisting.
