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

# Webhooks

> Get an HTTPS request when something finishes in Dcycle, instead of polling

A webhook endpoint is an HTTPS URL of yours that Dcycle calls when an event happens in your organization, for
example when a [bulk ingest job](/api-reference/ingest-jobs/get) finishes. Your integration reacts at once instead of
polling.

<Warning>
  **Beta.** The webhooks API is in beta: the contract may still change before general availability. Ignore unknown
  fields in the payload so new ones do not break you.
</Warning>

## How it works

<Steps>
  <Step title="Register an endpoint">
    [`POST /v2/webhook-endpoints`](/api-reference/webhooks/create-endpoint) with your URL and the events you want.
    The response carries the endpoint's **signing secret** (`whsec_…`). Store it: it is shown only once.
  </Step>

  <Step title="Dcycle sends the events">
    Each event is a `POST` with a JSON body, signed with your secret in the `Dcycle-Signature` header.
  </Step>

  <Step title="Answer 2xx quickly">
    Verify the signature, store the event and answer any `2xx` within 10 seconds. Do the heavy work afterwards. Any
    other answer, a timeout or a network error is retried.
  </Step>
</Steps>

Only organization admins can manage endpoints, with an API key or from the app. An endpoint receives the events of
the organization that registered it (the `x-organization-id` it was created with).

## Events

| `type` | When | `data` |
| - | - | - |
| `ingest_job.finished` | A bulk ingest job stops processing: `completed`, `completed_with_errors` or `failed`. | The job, exactly as [`GET /v2/ingest-jobs/{id}`](/api-reference/ingest-jobs/get) returns it. |
| `webhook.test` | You call [Send Test Event](/api-reference/webhooks/test-endpoint). Needs no subscription. | `{"endpoint_id": "…"}` |

Every event has the same envelope:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "6f1d2c3b-9a8e-4f7d-b6c5-a4e3d2c1b0a9",
  "type": "ingest_job.finished",
  "created_at": "2026-10-01T10:16:42Z",
  "organization_id": "a8315ef3-dd50-43f8-b7ce-d839e68d51fa",
  "data": {
    "id": "2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa",
    "status": "completed_with_errors",
    "entity_type": "wastes",
    "operation": "create",
    "source": "api_bulk",
    "counts": { "submitted": 250, "succeeded": 247, "failed": 3 },
    "chunks_total": 3,
    "chunks_done": 3,
    "created_at": "2026-10-01T10:15:02.184311",
    "finished_at": "2026-10-01T10:16:41.902774",
    "links": {
      "self": "/v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa",
      "items": "/v2/ingest-jobs/2d4b90c1-7c1e-4b6f-9a55-3f2b8e61d0aa/items"
    }
  }
}
```

The payload is a summary. Fetch the details you need with the API, e.g. the failed records with
[`GET /v2/ingest-jobs/{id}/items?status=failed`](/api-reference/ingest-jobs/list-items).

### Request headers

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `User-Agent` | `Dcycle-Webhooks/1.0` |
| `Dcycle-Event-Id` | The event `id`. The same on every retry. |
| `Dcycle-Event-Type` | The event `type`. |
| `Dcycle-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>` |

## Verifying the signature

`v1` is the HMAC-SHA256, keyed with your secret, of the timestamp `t`, a dot, and the **raw** request body. Compute
it over the bytes you received, before parsing the JSON, and compare in constant time. Reject requests whose `t` is
more than 5 minutes old to stop replays.

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


  def verify(raw_body: bytes, signature_header: str, secret: str, tolerance_s: int = 300) -> bool:
      fields = dict(part.split("=", 1) for part in signature_header.split(","))
      timestamp = fields["t"]
      if abs(time.time() - int(timestamp)) > tolerance_s:
          return False
      expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, fields["v1"])
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const crypto = require('crypto');

  function verify(rawBody, signatureHeader, secret, toleranceS = 300) {
    const fields = Object.fromEntries(signatureHeader.split(',').map((part) => part.split('=')));
    if (Math.abs(Date.now() / 1000 - Number(fields.t)) > toleranceS) return false;
    const expected = crypto
      .createHmac('sha256', secret)
      .update(`${fields.t}.`)
      .update(rawBody) // a Buffer with the exact bytes received
      .digest('hex');
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(fields.v1));
  }
  ```
</CodeGroup>

## Retries and failures

* **At-least-once.** An event can arrive more than once. Deduplicate on `id` (or `Dcycle-Event-Id`).
* **Order is not guaranteed.** Use the payload's timestamps and statuses, not the arrival order.
* **Retry schedule.** An attempt fails on any non-`2xx` answer, a timeout (5 s to connect, 10 s to answer) or a
  network error. Redirects are not followed. Dcycle retries after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h: seven
  attempts over about 21 hours, then the delivery is `failed`.
* **Automatic disabling.** After 5 deliveries in a row end `failed`, the endpoint is disabled
  (`disabled_reason: TOO_MANY_FAILURES`) and its creator is notified in the app. Fix your server and re-enable it
  with [`PATCH`](/api-reference/webhooks/update-endpoint) `{"enabled": true}`.
* **Delivery log.** [List Webhook Deliveries](/api-reference/webhooks/list-deliveries) shows every delivery with its
  status code, error and next retry.

## URL requirements

The URL must use `https` and resolve to a **public** address. Private, loopback and link-local addresses (`10.x`,
`192.168.x`, `127.0.0.1`, `169.254.169.254`, …), `localhost` and URLs with credentials are rejected when you
register the endpoint, and the name is resolved again before every request.

## Endpoints

<CardGroup cols={2}>
  <Card title="List Webhook Endpoints" icon="list" href="/api-reference/webhooks/list-endpoints">
    Endpoints of your organization
  </Card>

  <Card title="Get Webhook Endpoint" icon="magnifying-glass" href="/api-reference/webhooks/get-endpoint">
    One endpoint by id
  </Card>

  <Card title="Create Webhook Endpoint" icon="plus" href="/api-reference/webhooks/create-endpoint">
    Register a URL and get its secret
  </Card>

  <Card title="Update Webhook Endpoint" icon="pencil" href="/api-reference/webhooks/update-endpoint">
    Change URL, events or re-enable it
  </Card>

  <Card title="Delete Webhook Endpoint" icon="trash" href="/api-reference/webhooks/delete-endpoint">
    Stop sending to a URL
  </Card>

  <Card title="Rotate Webhook Secret" icon="key" href="/api-reference/webhooks/rotate-secret">
    Replace the signing secret
  </Card>

  <Card title="Send Test Event" icon="paper-plane" href="/api-reference/webhooks/test-endpoint">
    Check your server end to end
  </Card>

  <Card title="List Webhook Deliveries" icon="clock-rotate-left" href="/api-reference/webhooks/list-deliveries">
    What was sent and how it went
  </Card>
</CardGroup>
