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

# Webhooks

> Receive evaluation results automatically, verify their signatures, and handle the payload.

Webhooks deliver evaluation results to your endpoint as soon as they're ready, so you don't have to poll. You provide an endpoint URL during onboarding and receive a shared secret for verifying that requests genuinely came from Adclear.

## Overview and configuration

Webhooks are configured per organisation by the Adclear team during onboarding.

| Event                  | Trigger                                     | Payload                                                    |
| ---------------------- | ------------------------------------------- | ---------------------------------------------------------- |
| `evaluation-completed` | An evaluation finishes (success or failure) | Issues, severity, cited rules, location, and your metadata |

**Delivery:**

* Sent as `application/json` via `POST`.
* Make your endpoint idempotent, using `evaluationId` as the deduplication key, in case a result is delivered more than once.
* Treat delivery as best-effort. If a webhook doesn't arrive, fall back to [polling the evaluation](/developer/evaluation#poll-evaluation-status).
* Every payload includes the `metadata` you provided at promotion creation, so you can correlate statelessly.

## Signature verification

Every request includes an `X-Webhook-Signature` header: an HMAC-SHA256 hex digest of the raw request body, prefixed with `sha256=`. Verify it before processing the payload. Use the raw bytes, not a re-serialised object, since whitespace differences change the digest.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  export function verifyWebhookSignature(rawBody: string, signatureHeader: string, secret: string): boolean {
    const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
    const provided = signatureHeader?.startsWith("sha256=") ? signatureHeader.slice(7) : signatureHeader;
    // Reject anything that isn't a 64-char hex digest. Buffer.from(_, "hex") stops at
    // the first invalid character, so timingSafeEqual would otherwise throw on a
    // malformed or missing signature instead of returning false.
    if (!/^[0-9a-f]{64}$/i.test(provided ?? "")) return false;
    // Constant-time compare to avoid leaking the first differing byte via timing.
    return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(provided, "hex"));
  }
  ```

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

  def verify_webhook_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
      if not signature_header:
          return False
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      provided = signature_header.removeprefix("sha256=")
      return hmac.compare_digest(expected, provided)  # constant-time compare
  ```
</CodeGroup>

Wire it into your endpoint using the **raw** body. Parsing to JSON first changes the bytes and invalidates the signature.

```typescript theme={null}
import express from "express";
const app = express();

app.post("/webhooks/adclear", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const rawBody = req.body.toString("utf-8");

  if (!verifyWebhookSignature(rawBody, signature, process.env.ADCLEAR_WEBHOOK_SECRET!)) {
    res.status(401).json({ error: "Invalid signature" });
    return;
  }

  // Acknowledge immediately, then process asynchronously to avoid timeouts.
  res.status(200).json({ received: true });
  handleEvaluationCompleted(JSON.parse(rawBody));
});
```

## Evaluation payload reference

Headers sent with every webhook:

| Header                | Description                                          |
| --------------------- | ---------------------------------------------------- |
| `Content-Type`        | `application/json`                                   |
| `X-Webhook-Signature` | `sha256=<hex>` HMAC-SHA256 signature of the raw body |
| `X-Adclear-Event`     | Event type, for example `evaluation-completed`       |
| `X-Correlation-ID`    | Request trace ID; include it in support requests     |

Example `evaluation-completed` payload:

```json theme={null}
{
  "organizationId": "org-uuid-...",
  "workspaceId": "ws-uuid-...",
  "evaluationId": "eval-uuid-...",
  "promotionId": "promo-uuid-...",
  "promotionVersionId": "version-uuid-...",
  "status": "succeeded",
  "reviewRequired": true,
  "overallAssessment": "This promotion requires review",
  "issues": [
    {
      "recommendation": "Add a risk warning stating that the value of investments can go down as well as up.",
      "rationale": "FCA COBS 4.6.2 requires a prominent risk warning for non-readily realisable securities.",
      "severity": "HIGH",
      "citedRules": [
        { "id": "rule-uuid-1", "name": "Risk Warning Required", "product": "ISA", "channel": "Email" }
      ],
      "location": { "type": "text", "content": "Invest today and earn guaranteed returns!", "pageNumber": 2 }
    }
  ],
  "metadata": { "workfrontProjectId": "PROJ-123", "workfrontTaskId": "TASK-456" }
}
```

<ResponseField name="organizationId" type="string">Your Adclear organisation ID.</ResponseField>
<ResponseField name="workspaceId" type="string">The workspace the promotion belongs to.</ResponseField>
<ResponseField name="evaluationId" type="string">Unique evaluation identifier. Use it as a deduplication key.</ResponseField>
<ResponseField name="promotionId" type="string">The promotion that was evaluated.</ResponseField>
<ResponseField name="promotionVersionId" type="string">The specific version that was evaluated.</ResponseField>
<ResponseField name="status" type="string">`succeeded` or `failed`.</ResponseField>
<ResponseField name="reviewRequired" type="boolean">`true` if the promotion has issues requiring human review.</ResponseField>
<ResponseField name="overallAssessment" type="string">One-sentence summary suitable for a dashboard or notification.</ResponseField>

<ResponseField name="issues" type="array">
  Ordered list of compliance findings. Empty when `status` is `failed`.

  <Expandable title="issue">
    <ResponseField name="recommendation" type="string">Actionable remediation guidance.</ResponseField>
    <ResponseField name="rationale" type="string">Why it needs changing, with rule citations and regulatory context.</ResponseField>
    <ResponseField name="severity" type="string">`HIGH` (likely non-compliant), `MEDIUM` (should fix), or `LOW` (advisory).</ResponseField>

    <ResponseField name="citedRules" type="array">
      Rules that triggered this finding. Empty for general best-practice issues.

      <Expandable title="rule">
        <ResponseField name="id" type="string">Rule identifier.</ResponseField>
        <ResponseField name="name" type="string">Human-readable rule name.</ResponseField>
        <ResponseField name="product" type="string">Product the rule applies to, if scoped.</ResponseField>
        <ResponseField name="channel" type="string">Channel the rule applies to, if scoped.</ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="location" type="object">
      Where in the content the issue was found. Absent for document-level issues.

      <Expandable title="location">
        <ResponseField name="type" type="string">`text`, `image`, `general`, or `insertion`.</ResponseField>
        <ResponseField name="content" type="string">The exact text or description of the element.</ResponseField>
        <ResponseField name="pageNumber" type="integer">1-based PDF page number. Present only for PDFs.</ResponseField>
        <ResponseField name="timestamp" type="string">`MM:SS` timestamp. Present only for video or audio.</ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="metadata" type="object">Your metadata, echoed back unchanged from promotion creation.</ResponseField>
<ResponseField name="correlationId" type="string">Request trace ID. Include it in support requests.</ResponseField>

## Handling the result

Acknowledge fast, then branch on `status` and `reviewRequired`. Generate the types from the OpenAPI spec (see [Authentication](/developer/authentication#generate-types-from-the-spec)).

```typescript theme={null}
function handleEvaluationCompleted(payload: EvaluationWebhookPayload): void {
  // Evaluation itself failed (for example, a corrupt file). No issues are returned.
  if (payload.status === "failed") {
    console.error(`Evaluation ${payload.evaluationId} failed. correlationId=${payload.correlationId}`);
    return;
  }

  // Passed compliance review — safe to auto-approve in your workflow.
  if (!payload.reviewRequired) {
    markApproved(payload.metadata?.workfrontTaskId);
    return;
  }

  // Issues found — prioritise by severity and link back via your metadata.
  const critical = payload.issues.filter((i) => i.severity === "HIGH");
  createReviewTask({
    taskId: payload.metadata?.workfrontTaskId,
    summary: payload.overallAssessment,
    criticalCount: critical.length,
    issues: payload.issues,
  });
}
```

## Endpoint requirements

| Requirement   | Detail                                                                  |
| ------------- | ----------------------------------------------------------------------- |
| Protocol      | HTTPS only. HTTP endpoints are rejected during configuration            |
| Response time | Respond `2xx` within 30 seconds. Queue longer processing                |
| Response code | Return `200` or `202` to acknowledge receipt                            |
| Idempotency   | A result may be delivered more than once. Deduplicate on `evaluationId` |
| Signature     | Verify `X-Webhook-Signature` before processing                          |

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook not being received" icon="triangle-exclamation">
    1. Confirm your endpoint is HTTPS. HTTP isn't supported.
    2. Verify it responds with `2xx` within 30 seconds.
    3. Check your signature verification. If it fails silently, you may be discarding valid payloads.
    4. Confirm webhooks are configured for your organisation. Contact Adclear if unsure.

    If a webhook still doesn't arrive, poll the [evaluation endpoint](/developer/evaluation#poll-evaluation-status) as a fallback.
  </Accordion>

  <Accordion title="Should I use webhooks or polling?" icon="circle-question">
    Either works. Webhooks avoid polling and timeout concerns. Polling (`GET .../evaluation`) is simpler to start with and useful as a fallback. Many integrations rely on webhooks and poll only if one is delayed. See [Evaluation](/developer/evaluation#recommended-flow).
  </Accordion>
</AccordionGroup>

For cross-cutting errors (401, 403, 429, 502), see [Errors & rate limits](/developer/errors).
