Skip to main content

Overview and Configuration

Webhooks are configured per-organisation by the Adclear team during onboarding. You provide your endpoint URL and receive a shared HMAC secret for signature verification. Event types:
EventTriggerPayload
on_evaluation_completeEvaluation finishes (success or failure)Full issues, severity, cited rules, location
Delivery guarantees:
  • Webhooks are delivered at-least-once. Your endpoint should be idempotent. Use evaluationId as a deduplication key.
  • Payloads are sent as application/json via POST.
  • Failed deliveries (non-2xx) are retried with exponential back-off.
  • All webhook payloads include the metadata field you provided at promotion creation, enabling stateless correlation with your system.

Signature Verification

Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 hex digest of the raw request body, prefixed with sha256=. You must verify this signature before processing the payload to confirm the request genuinely came from Adclear. The shared secret used for signing is provisioned during onboarding. Store it securely (e.g. as an environment variable). Verification function (TypeScript/Node.js):
import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * Verifies that a webhook request genuinely came from Adclear.
 *
 * Adclear signs every outbound webhook payload using HMAC-SHA256
 * with the shared secret provisioned during onboarding. The hex
 * digest is sent in the X-Webhook-Signature header, optionally
 * prefixed with "sha256=".
 *
 * @param rawBody    - The raw, unparsed request body string.
 *                     Important: use the raw bytes, not a re-serialised
 *                     JSON object, because whitespace differences
 *                     will produce a different digest.
 * @param signatureHeader - Value of the X-Webhook-Signature header.
 * @param secret     - The HMAC secret provided by Adclear during
 *                     onboarding. Store this securely (e.g. env var).
 * @returns true if the signature is valid; false otherwise.
 */
function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string
): boolean {
  // Compute what the signature *should* be for this body + secret
  const expected = createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // Strip the optional "sha256=" prefix that Adclear includes
  const provided = signatureHeader.startsWith('sha256=')
    ? signatureHeader.slice(7)
    : signatureHeader;

  // Reject early if lengths differ (avoids leaking info via timingSafeEqual)
  if (expected.length !== provided.length) return false;

  // Use constant-time comparison to prevent timing attacks.
  // A naive === comparison can leak the position of the first
  // differing character via response time.
  return timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(provided, 'hex')
  );
}
Express middleware example:
import express from 'express';

const app = express();

// Critical: use express.raw() so we get the unparsed body for
// signature verification. Parsing to JSON first would change
// the bytes and invalidate the HMAC.
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!)) {
      // Reject unsigned or tampered requests immediately.
      // Do not process the payload.
      res.status(401).json({ error: 'Invalid signature' });
      return;
    }

    const payload = JSON.parse(rawBody);

    // Signature valid -- safe to process.
    // Respond 200 immediately; handle asynchronously to avoid
    // timeouts on long-running processing.
    res.status(200).json({ received: true });

    handleEvaluationCompleted(payload);
  }
);

Evaluation Payload Reference

Headers included with every webhook:
HeaderDescription
Content-Typeapplication/json
X-Webhook-Signaturesha256=<hex> HMAC-SHA256 signature of the raw body
X-Adclear-EventEvent type (e.g. evaluation-completed)
X-Correlation-IDUnique request ID for tracing; include in support requests
Example payload (on_evaluation_complete):
{
  "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. The current copy omits this entirely.",
      "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
      }
    },
    {
      "recommendation": "Remove the word 'guaranteed' when referring to investment returns.",
      "rationale": "Promising guaranteed returns on investments is misleading under FCA COBS 4.2.1 unless the guarantee is underwritten.",
      "severity": "HIGH",
      "citedRules": [
        { "id": "rule-uuid-2", "name": "No Misleading Claims" }
      ],
      "location": {
        "type": "text",
        "content": "Invest today and earn guaranteed returns!",
        "pageNumber": 2
      }
    },
    {
      "recommendation": "Include the firm's FCA registration number in the footer.",
      "rationale": "All financial promotions must include the firm's FCA registration number per FCA COBS 4.6.8.",
      "severity": "MEDIUM",
      "location": {
        "type": "general",
        "content": "Footer section"
      }
    }
  ],
  "metadata": {
    "workfrontProjectId": "PROJ-123",
    "workfrontTaskId": "TASK-456"
  }
}

Top-level Fields

FieldTypeDescription
evaluationIdstringUnique evaluation identifier. Use as a deduplication key
promotionIdstringThe promotion that was evaluated
promotionVersionIdstring?The specific version that was evaluated
status"succeeded" | "failed"Whether the evaluation completed successfully
reviewRequiredbooleantrue if the promotion has compliance issues requiring human review
overallAssessmentstringA one-sentence summary suitable for display in a dashboard or notification
issuesarrayOrdered list of compliance findings. Empty when status is "failed"
metadataobject?Your metadata echoed back unchanged from promotion creation (string key-value pairs)
correlationIdstring?Request trace ID. Include in support requests for fast debugging

Issue Fields (issues[])

FieldTypeDescription
recommendationstringWhat should be changed — actionable remediation guidance
rationalestringWhy it needs changing, with specific rule citations and regulatory context
severity"LOW" | "MEDIUM" | "HIGH"HIGH = likely non-compliant; MEDIUM = should fix; LOW = advisory
citedRulesarray?Rules that triggered this finding. Empty for general best-practice issues
locationobject?Where in the content the issue was found. Absent for document-level issues

Cited Rule Fields (issues[].citedRules[])

FieldTypeDescription
idstringRule identifier
namestring?Human-readable rule name (e.g. “Risk Warning Required”)
productstring?Product the rule applies to, if scoped (e.g. “ISA”)
channelstring?Channel the rule applies to, if scoped (e.g. “Email”)

Location Fields (issues[].location)

FieldTypeDescription
type"text" | "image" | "general" | "insertion"What kind of content element is affected
contentstringThe exact text or description of the element in the source content
pageNumbernumber?1-based PDF page number. Only present for PDF content
timestampstring?Timestamp in MM:SS format. Only present for video/audio content
For the full machine-readable schema, see the OpenAPI spec at https://public-api.adclear.ai/v1/openapi.json.

Handling the Evaluation Result (TypeScript)

For full type definitions, generate types from the OpenAPI spec at https://public-api.adclear.ai/v1/openapi.json or explore the schema interactively at https://public-api.adclear.ai/v1/docs.
// -----------------------------------------------------------------------
// Types: generate from the OpenAPI spec at
// https://public-api.adclear.ai/v1/openapi.json
// or use the Swagger UI at https://public-api.adclear.ai/v1/docs
// to explore the full schema interactively.
// -----------------------------------------------------------------------

// -----------------------------------------------------------------------
// Handler -- demonstrates both the happy path and error path
// -----------------------------------------------------------------------

function handleEvaluationCompleted(payload: EvaluationWebhookPayload): void {
  // --- Error path: evaluation itself failed ---
  // This means the AI pipeline could not process the file (e.g. corrupt PDF,
  // processing timeout). No issues are returned. You should log the
  // correlationId and either retry or escalate to support.
  if (payload.status === 'failed') {
    console.error(
      `Evaluation ${payload.evaluationId} failed. ` +
      `correlationId=${payload.correlationId} -- contact support or retry.`
    );
    return;
  }

  // --- Happy path: no issues found ---
  // The promotion passed compliance review. reviewRequired is false and the
  // issues array is empty. Safe to auto-approve in your workflow.
  if (!payload.reviewRequired) {
    markApproved(payload.metadata?.workfrontTaskId);
    return;
  }

  // --- Happy path: issues found, review required ---
  // Filter by severity so you can prioritise critical findings.
  const critical = payload.issues.filter((i) => i.severity === 'HIGH');
  const warnings = payload.issues.filter((i) => i.severity === 'MEDIUM');

  // Group issues by page number for PDF content. This makes it easy to
  // present findings alongside the relevant page in your review UI.
  // Issues without a pageNumber (e.g. document-level or non-PDF) are
  // grouped under `undefined`.
  const byPage = new Map<number | undefined, EvaluationIssue[]>();
  for (const issue of payload.issues) {
    const page = issue.location?.pageNumber;
    const group = byPage.get(page) ?? [];
    group.push(issue);
    byPage.set(page, group);
  }

  // Use metadata to link back to your own system. The metadata object
  // is exactly what you passed when creating the promotion, so you
  // don't need to maintain an Adclear-to-internal ID mapping.
  createReviewTask({
    taskId: payload.metadata?.workfrontTaskId,
    summary: payload.overallAssessment,
    criticalCount: critical.length,
    warningCount: warnings.length,
    issuesByPage: byPage
  });
}
When status is "failed": The evaluation encountered an error (e.g. unsupported file format, processing timeout). The issues array will be empty and reviewRequired will be false. Re-trigger the evaluation, or contact support with the correlationId.

Endpoint Requirements

Your webhook endpoint must meet the following requirements:
RequirementDetail
ProtocolHTTPS only. HTTP endpoints are rejected during configuration
Response timeRespond with 2xx within 30 seconds. Longer processing should be queued asynchronously
Response codeReturn 200 or 202 to acknowledge receipt. Any non-2xx triggers a retry
IdempotencyWebhooks are delivered at-least-once. Use evaluationId to deduplicate
Signature verificationVerify the X-Webhook-Signature header before processing (see above)
Recommended pattern: Accept the webhook, return 200 immediately, then process the payload asynchronously via a job queue. This avoids timeouts and ensures reliable acknowledgement.