Skip to main content

Webhooks

Quickstart

Get your first SmartComply webhook delivering events in four steps. This guide takes about five minutes if you already have an HTTPS endpoint ready.

Prerequisites

  • A SmartComply API key (sc_live_...)
  • An HTTPS endpoint that can receive POST requests

No endpoint yet? Use a tunnel tool to expose a local server, or deploy a minimal receiver first and come back.

1

Register an endpoint

Call the Create Endpoint API (or use the dashboard at Settings → Webhooks → Add endpoint). Specify your URL and the events you want to receive.

Create endpoint (cURL)
curl -X POST https://app.smartcomply.app/api/v1/webhooks/endpoints \
  -H "Authorization: Bearer sc_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example/webhooks/smartcomply",
    "events": ["test.submitted", "notice.created"],
    "description": "Production receiver"
  }'

The response includes your signing_secret. This is the only time it is returned in full — copy it now.

Response (JSON)
{
  "id": "whe_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "url": "https://your-app.example/webhooks/smartcomply",
  "events": ["test.submitted", "notice.created"],
  "description": "Production receiver",
  "status": "active",
  "signing_secret": "whsec_k7Lm9PqRsTuVwXyZ...",
  "created_at": "2026-05-05T14:00:00.000Z"
}
2

Save the signing secret

Store signing_secretin your application's environment variables — for example as SMARTCOMPLY_WEBHOOK_SECRET. You will use it to verify that incoming deliveries actually came from SmartComply and have not been tampered with.

Security note: treat the signing secret like a password. Never log it, commit it to version control, or expose it to the browser. If you suspect it has been compromised, rotate it immediately via the dashboard or the POST .../rotate-secret endpoint.
3

Verify with a test event

Send a test event to confirm your endpoint is reachable and your signature verification works. You can do this from the dashboard (click Send test event on the endpoint detail page) or via the API:

Send test event (cURL)
curl -X POST https://app.smartcomply.app/api/v1/webhooks/endpoints/whe_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/test \
  -H "Authorization: Bearer sc_live_YOUR_API_KEY"

SmartComply fires a webhook.testevent with a synthetic payload. Check your server logs — you should see the delivery arrive within a few seconds. The delivery will appear in your endpoint's delivery log in the dashboard so you can inspect the full request/response cycle.

4

Handle real events

Here is a complete Express.js receiver that verifies the signature and switches on event type. Adapt to your framework of choice — the important parts are: read the raw body, verify the HMAC, respond with 200 fast, then process asynchronously.

server.mjs (Express.js)
import express from "express";
import crypto from "node:crypto";

const app = express();

// IMPORTANT: use the raw body for signature verification
app.post(
  "/webhooks/smartcomply",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const secret = process.env.SMARTCOMPLY_WEBHOOK_SECRET; // whsec_...
    const sigHeader = req.headers["x-smartcomply-signature"];

    // --- 1. Verify the signature ---
    const parts = Object.fromEntries(
      sigHeader.split(",").map((p) => p.split("="))
    );
    const t = Number(parts.t);
    const v1 = parts.v1;
    const body = req.body.toString();

    const age = Math.floor(Date.now() / 1000) - t;
    if (age > 300) return res.status(400).send("Signature too old");

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${t}.${body}`)
      .digest("hex");

    if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
      return res.status(400).send("Invalid signature");
    }

    // --- 2. Parse the event ---
    const event = JSON.parse(body);
    console.log(`Received ${event.type} (id: ${event.id})`);

    // --- 3. Handle by type ---
    switch (event.type) {
      case "test.submitted":
        // sync the test result to your system
        break;
      case "notice.created":
        // open a case in your ticketing system
        break;
      case "webhook.test":
        // test event — just log it
        console.log("Test event received successfully");
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    // --- 4. Ack fast! ---
    res.status(200).json({ received: true });
  }
);

app.listen(3000, () => console.log("Webhook receiver running on :3000"));

Key points from the code above:

  • Raw body parsing. express.raw() gives you the un-parsed request body as a Buffer — this is what you sign against. If your framework parses JSON before you get the raw bytes, the signature will not match.
  • Clock-skew tolerance. The 300-second (5-minute) window protects against replay attacks while accommodating reasonable clock drift.
  • Fast acknowledgment. Return 200 before doing any heavy processing. SmartComply times out after 10 seconds.
  • Idempotency. Use event.id to deduplicate. Store it in your database and skip events you have already processed.

What's next