Gareth Wilson Gareth Wilson

Guide to ComplyCube Webhooks: Features and Best Practices

Published


ComplyCube runs identity verification and anti-money-laundering checks for regulated businesses: document and selfie checks, screening against sanctions and PEP lists, address verification and ongoing monitoring. Most checks finish seconds or minutes after the customer submits, and some need a human reviewer, so your onboarding flow can't simply wait for an API response. ComplyCube's webhooks tell your backend when a check completes and with what outcome, when a hosted workflow session moves on, and when client, document and address records change.

This guide covers ComplyCube's event types and their outcome variants, how to verify ComplyCube-Signature without falling into the trap in ComplyCube's own Node sample, and the retry limit that ends with a disabled webhook.

What are ComplyCube webhooks?

Each ComplyCube webhook is an HTTPS endpoint with a list of event types. When one of those events happens, ComplyCube sends an Event object to it. The event name is in the body's type field, resourceType says what kind of object is in payload, and id identifies the event so you can skip duplicates.

How much of the object you get depends on the event. A create event carries the full object, an update carries its ID and the changed fields, a delete carries only the ID, and check events carry the ID, status, outcome and timestamps. For anything more, fetch the object from the API.

Every delivery is signed with the endpoint's secret in ComplyCube-Signature, a hex HMAC-SHA256 of the body.

ComplyCube webhook features

FeatureDetails
ConfigurationWeb portal, or POST /webhooks
URLHTTPS only
SubscriptionPer endpoint, by event type. ["*"] subscribes to all
Signaturehex HMAC-SHA256
Signature headercomplycube-signature
Signed contentThe request body
SecretOne per endpoint, returned once at creation
BodyJSON
Event routingBody field type
Response windowA 20x within 15 seconds
RetriesUp to 10, with exponential backoff (60 s, 120 s, 480 s, ...)
After retriesThe webhook is disabled
OrderingNot guaranteed. Duplicates are possible

Events and payloads

EventTriggered WhenCommon Use Cases
workflow.session.startedA workflow session has been startedTrack onboarding progress
workflow.session.cancelledA workflow session has been cancelledClean up pending onboarding
workflow.session.processingThe customer finished all steps; checks are in progressShow a "pending review" state
workflow.session.completedA workflow session has been completedApprove or reject the customer
workflow.session.updatedA workflow session outcome has been updatedRe-evaluate a decision
check.pendingA check was created and is pendingTrack check creation
check.completedA check completed with any outcomeFetch the check and decide
check.completed.clearA check completed with a clear outcomeApprove the customer
check.completed.attentionA check completed with an attention outcomeRoute to manual review
check.completed.rejectedA check completed with a rejected outcomeDecline the customer
check.completed.match_confirmedA check completed with a match_confirmed outcomeEscalate a confirmed screening match
check.monitoring.attentionA monitoring check completed with attentionOngoing AML monitoring alerts
check.failedA check has failedRetry or alert
check.updatedA check has been updatedSync state
client.createdA client has been createdSync customers
client.updatedA client has been updatedSync customers
client.deletedA client has been deletedSync customers
document.createdA document has been createdTrack uploads
document.updatedA document has been updatedTrack changes
document.updated.image_uploadedA document got a new imageTrigger a document check
document.updated.image_deletedA document image was deletedTrack changes
document.deletedA document has been deletedSync state
address.createdAn address has been createdTrack records
address.updatedAn address has been updatedTrack records
address.deletedAn address has been deletedTrack records

A completed check fires check.completed and, if you subscribed to it, the outcome-specific type too. Subscribe only to the events you need.

Pick one style for checks. Either subscribe to check.completed and read payload.outcome, or subscribe to the outcome types you act on, such as check.completed.attention and check.completed.rejected. Subscribing to both means handling every completed check twice. workflow.session.updated matters if your compliance team overrides outcomes, because it reports a change to a decision you may already have acted on.

Event Payload Structure

  • id - unique event id; use it to skip duplicate deliveries
  • type - the event type
  • resourceType - the kind of object in payload
  • payload - the object (a check, client, document, address or workflow session)
  • createdAt - when the event was created

Published payload example

This is the check.completed.clear sample from the webhook samples library. It is the one example payload ComplyCube publishes.

{
  "id": "5ed170fe99c81a0007b9a3d3",
  "type": "check.completed.clear",
  "resourceType": "check",
  "payload": {
    "id": "5ed3918fbf06f30008e552c9",
    "outcome": "clear",
    "status": "complete",
    "createdAt": "2020-01-01T11:14:23.964Z",
    "updatedAt": "2020-01-01T11:14:23.964Z"
  },
  "createdAt": "2020-01-01T11:14:23.964Z"
}

The check event doesn't include the breakdown or the client. Fetch the check by payload.id to see why it came out the way it did.

Setting up ComplyCube webhooks

Webhooks can be created in the ComplyCube web portal or with the API (POST /webhooks).

  1. Provide the endpoint URL (HTTPS; plain HTTP is not accepted) and an optional description.
  2. Choose the events to receive, for example check.completed, client.created and workflow.session.completed. ComplyCube recommends subscribing only to what you use rather than ["*"].
  3. Make sure the webhook is enabled.
  4. Copy the webhook's secret. It is only returned once, at creation. Store it as WEBHOOK_SECRET and treat it like a password. Each endpoint has its own secret.

You can change the subscribed events later in the portal or by updating the webhook through the API. If ComplyCube has disabled the webhook after failed retries, fix the endpoint and re-enable it in the portal or by updating it with enabled: true.

To test against ComplyCube's sandbox from your machine, forward deliveries with the Hookdeck CLI:

npx hookdeck-cli listen 3000 complycube --path /webhooks/complycube

Securing ComplyCube webhooks

Compute an HMAC-SHA256 of the body you received with the endpoint's secret, encode it as hex and compare it with ComplyCube-Signature in constant time. ComplyCube's doc pages don't name the encoding. Its official Node SDK decodes the header as hex. See the verification documentation.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['complycube-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler verifies against the raw bytes, skips event IDs it has seen, and records check outcomes for your onboarding flow. Everything else is stored for later processing.

const express = require('express');

function createComplyCubeApp({ seen, onboarding, store }) {
  const app = express();
  app.post('/webhooks/complycube', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    let event;
    try {
      event = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!event || typeof event !== 'object' || Array.isArray(event)) return res.sendStatus(400);
    if (
      !event.id ||
      typeof event.type !== 'string' ||
      !event.payload ||
      typeof event.payload !== 'object'
    ) {
      return res.sendStatus(400);
    }

    try {
      if (await seen(event.id)) return res.sendStatus(200);
      if (event.type === 'check.completed') {
        await onboarding.recordCheck(event.payload.id, event.payload.outcome);
      } else if (event.type === 'workflow.session.completed' || event.type === 'workflow.session.updated') {
        await onboarding.recordSession(event.payload.id, event.payload);
      } else {
        await store(event);
      }
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

ComplyCube signs only the body and sends no timestamp header. Deduplicate on id to cover both retries and replays.

Hookdeck's ComplyCube source type verifies ComplyCube-Signature as a hex HMAC-SHA256 of the raw body with the endpoint's secret. Hookdeck always answers ComplyCube with 200, records a failed check as failed verification and does not forward that request. Hookdeck doesn't check any timestamp.

Make ComplyCube webhooks production-ready. Hookdeck Event Gateway answers ComplyCube inside its 15-second window, verifies each signature over the raw body, and keeps every check and workflow event so a compliance decision is never lost.

ComplyCube webhook limitations and pain points

The Node sample re-serialises the body

The Problem: Verification fails intermittently, or for some events and not others.

Why It Happens: ComplyCube's Node example parses the body with bodyParser.json() and then signs JSON.stringify(request.body). That only matches the original when ComplyCube's JSON happens to be compact and in the same key order. Its Python, PHP and .NET samples use the raw body.

Workarounds:

  • Verify the raw bytes, as in the code above, and parse only afterwards.
  • Make sure no JSON body parser runs on the webhook route first.

How Hookdeck Can Help: Hookdeck verifies against the exact bytes ComplyCube sent, so parsing on your side can't affect the check.

Ten retries, then the webhook is off

The Problem: Check results stop arriving and onboarding stalls.

Why It Happens: ComplyCube retries a failed delivery up to 10 times with exponential backoff, then disables the webhook. It stays disabled until someone re-enables it.

Workarounds:

  • Reply 20x within 15 seconds and do the work later.
  • Monitor the webhook's enabled state through the API.
  • After re-enabling, list recent checks through the API to catch anything missed.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives, so failures in your own service don't use up ComplyCube's retries or get the webhook disabled.

A secret you see once

The Problem: You lose the secret, or need to rotate it, and can't read it again.

Why It Happens: The secret is only returned when the webhook is created.

Workarounds:

  • Write it to your secret manager in the same step that creates the webhook.
  • To rotate, create a new webhook, switch your receiver, then delete the old one.

How Hookdeck Can Help: Keep the secret on the Hookdeck source. During a rotation, run a second source for the new webhook and route both to the same destination.

Duplicates and reordering

The Problem: A rejection is overwritten by an earlier attention, or one check is processed twice.

Why It Happens: ComplyCube warns that duplicates are possible and order is not guaranteed, and a retry can arrive after newer events.

Workarounds:

  • Deduplicate on the event id.
  • Compare the check's updatedAt with what you stored, or fetch the check before acting.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated event IDs before they reach your service.

Best practices

Verify raw bytes, accept any content type

Read raw bytes: express.raw({ type: '*/*' }) in Express, request.arrayBuffer() in Next.js and await request.body() in FastAPI. Check the header is exactly 64 hex characters before decoding it, because Node's hex decoder stops at the first invalid character.

Choose generic or outcome events, not both

Decide whether your code reads outcome from check.completed or reacts to check.completed.*, and subscribe accordingly.

Treat attention as a human task

check.completed.attention and check.monitoring.attention usually mean a reviewer needs to look. Route them to a queue for your compliance team rather than auto-declining.

Keep personal data out of logs

ComplyCube events concern identity documents and screening results. Log the event id, type and object ID, not payloads.

Process asynchronously

Fifteen seconds is enough to verify and store, not to fetch a check and update several systems. See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

ComplyCube's webhooks drive onboarding decisions, so a missed or misread one has real cost. Verify the raw body rather than following the Node sample, pick one style of check event, deduplicate on id, and answer within 15 seconds so the webhook is never disabled.

Hookdeck Event Gateway verifies each ComplyCube delivery, absorbs retries and outages, and gives your compliance team a full record of every event received.

Get started with Hookdeck for free and handle ComplyCube webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.