SafeWeb API
Partner API

Outbound webhooks

Overview

SafeWeb pushes customer lifecycle and breach events to HTTPS URLs you register. Deliveries are processed asynchronously: SafeWeb’s webhooks runner loads each pending delivery from our database, signs the request, POSTs to your URL, and treats the exchange as successful only when your handler returns a valid response signature.

This page documents the wire protocol (what hits your server and how to respond). Use the management APIs to register URLs and signing secrets:

When an event fires for a partner organization, the gateway resolves every enabled webhook_endpoints row whose event_types overlaps the event, for both that organization and (when configured) the parent distributor. Partner-owned URLs and secrets are managed under the Partner API; distributor-owned URLs and their signing secrets are managed under the Distributor API above. Each match becomes a queued delivery processed by the runner.

Delivery HTTP contract

Each attempt is a single POST to your registered URL.

Request headers

HeaderMeaning
Content-TypeAlways application/json.
X-SignatureHex-encoded HMAC-SHA256 of the raw JSON body bytes, using your current active signing secret token (UUID string from secret generation).
X-TokenA random UUID generated for this request. You must incorporate it when computing your response signature (see below).

Request body (JSON)

The body is a stable envelope:

{
  "idempotencyToken": "8f2c1b0e-…",
  "type": "customer.breach.found",
  "data": {
    "customer": {
      "uuid": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Acme Ltd",
      "reference": "ACME-001"
    },
    "breaches": [
      {
        "uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "name": "LinkedIn-2024",
        "emails": ["user@example.com"]
      }
    ]
  },
  "timestamp": 1715601234567
}
FieldDescription
idempotencyTokenStable id for this logical event instance (used for deduplication if you see retries).
typeCanonical event name (see Event types).
dataEvent payload (see per-type shapes below). Organization metadata is applied server-side; your receiver should rely on the nested customer, assets, or breaches objects inside data.
timestampMilliseconds since Unix epoch for when SafeWeb created this delivery record (server time). It may differ from any timestamps inside data.

Timeouts and response requirements

  • The runner uses a 3 second request timeout per HTTP call.
  • The HTTP status must be 2xx.
  • The response must include header X-Signature with hex-encoded HMAC-SHA256 over the UTF-8 string formed by: the X-Token header value, a literal colon (:), then the raw request body string (identical bytes to the body you verified on ingress). The same signing secret token is used for the request X-Signature and for computing the expected response X-Signature.

Retries

Each registered endpoint has a maxAttempts value (from 1 to 3, set when you create or update the endpoint). SafeWeb may run up to that many delivery attempts for a single queued event. A failed attempt is followed by another attempt when the previous one ended with any of:

  • a timeout (no complete HTTP response within 3 seconds);
  • a non-2xx HTTP status from your server;
  • an invalid or missing response X-Signature (the header does not match the value expected for the request body and X-Token);
  • a transport-level failure before a response is received (for example a network error).

When attempts are exhausted without a successful delivery, the event is treated as failed (subject to your monitoring and support processes).

Fallback signing secret — If you have both an active and a fallback signing secret (for example immediately after cycling secrets), and an attempt fails only because the response X-Signature did not verify against the active secret, SafeWeb sends one extra HTTP request for that delivery using the fallback secret. That immediate retry addresses rotation windows where your server still verifies with the previous secret. It does not apply to timeouts, non-2xx responses, or invalid request X-Signature headers.

Signing secret rotation

Secrets are stored per organization UUID or distributor id. At most one active and one fallback secret may exist. After you cycle (Partner API) or cycle distributor secrets, the previous active becomes fallback so in-flight deliveries can still complete while you deploy the new active token.

Event types

Canonical type strings (also used in event_types when you create or update an endpoint):

typeWhen it fires
customer.createdA new customer record exists for the org.
customer.deletedA customer was removed / offboarded.
customer.asset.addedOne or more domains or emails were added to monitoring.
customer.asset.removedAssets were removed.
customer.breach.foundNew breach rows were detected for the customer.
customer.breach.resolvedBreach(es) transitioned to resolved.
customer.breach.unresolvedBreach(es) transitioned back to unresolved.

Payload shapes (data)

The data object does not repeat organization-level fields from the gateway. Every event includes customer; asset lifecycle events also include assets; breach events include breaches instead.

CustomerAsset objects (uuid, type, value) appear only on customer.asset.added and customer.asset.removed. Breach events do not include an assets array — they identify which monitored email addresses were affected via breaches[].emails.

Shared types

/** Customer record on the webhook payload */
type Customer = {
  uuid: string;
  name: string;
  reference: string;
};

/** Monitored domain or email address (asset lifecycle events only) */
type CustomerAsset = {
  uuid: string;
  /** Determines how to interpret `value` */
  type: 'domain' | 'email';
  value: string;
};

/** Breach row summarized for the customer */
type BreachSummary = {
  /** Stable breach identifier (HIBP breach row UUID) */
  uuid: string;
  /** HIBP breach name (for example `LinkedIn-2024`) */
  name: string;
  /**
   * Monitored email address(es) where this breach was found for this customer.
   * Plain email strings — not `CustomerAsset` objects. Domain-only assets are
   * not listed here; if a domain hunt discovers an email and that email is
   * breached, the email address appears in this array.
   */
  emails: string[];
};

customer.created and customer.deleted

type Data = {
  customer: Customer;
};

customer.asset.added and customer.asset.removed

type Data = {
  customer: Customer;
  /** Always at least one entry */
  assets: CustomerAsset[];
};

customer.breach.found, customer.breach.resolved, and customer.breach.unresolved

type Data = {
  customer: Customer;
  /** Always at least one entry. No `assets` field on breach events. */
  breaches: BreachSummary[];
};

For customer.breach.found, each entry in breaches is a newly detected breach for the customer. SafeWeb emits one internal event per scanned email that returns new breaches; the gateway may merge those into a single delivery with multiple breaches rows (and union emails when the same breach UUID appears more than once).

To correlate an affected email with a monitored asset UUID or domain, call Get customer monitored assets and match on type: 'email' and value. Breach payloads intentionally carry email strings only, not full asset records.

To mark a breach resolved or unresolved from your integration, call Update breach resolved status with the breach uuid from the webhook payload and resolved: true or false. SafeWeb emits customer.breach.resolved or customer.breach.unresolved when the flag changes.

The gateway may batch and consolidate multiple internal events for the same organization window before enqueueing separate deliveries per endpoint and event type; your receiver should remain idempotent on idempotencyToken.

End-to-end checklist

  1. Create a signing secret — Without an active secret token, deliveries are rejected before HTTP (events are marked failed). Use Generate signing secret (Partner API) or Generate distributor signing secret depending on scope.
  2. Register your HTTPS URLCreate webhook endpoint with url, eventTypes, optional enabled and maxAttempts (default retry budget per delivery).
  3. Implement the POST handler — Verify X-Signature, parse JSON, process asynchronously, return 200 with a correct X-Signature response header as above.
  4. Rotate safely — Call the cycle endpoint, deploy the new active secret, then deactivate old material when traffic has drained.

Example local receiver

SafeWeb’s engineering repositories (including any sample receivers we use internally) are private—they are not something you can clone or run. For local or staging tests, use your own HTTPS endpoint that implements the same rules as production: read the raw POST body bytes, verify X-Signature, then return 2xx with a response X-Signature built from X-Token, a colon, and that same raw body string.

The script below mirrors what our delivery runner expects. Install Express (npm install express), save the file as local-webhook-receiver.mjs, set WEBHOOK_SECRET_TOKEN to the token returned once from the signing secret API, and run it with a current Node.js:

npm install express
WEBHOOK_SECRET_TOKEN=<token-uuid> node local-webhook-receiver.mjs
1. **Register an endpoint** `POST /api/v1/integrations/webhooks` with your URL and the events you want
2. **Store your secret** the response includes an HMAC signing secret (only shown once)
3. **Handle deliveries** accept POST requests at your URL and return a `2xx` status
4. **Verify signatures** validate the `X-SafeWeb-Signature` header to confirm authenticity

```bash
curl -X POST https://connect.safeweb.co/api/v1/integrations/webhooks \
  -H "Content-Type: application/json" \
  -H "SW-PARTNER-ID: your-partner-id" \
  -H "SW-API-KEY: your-api-key" \
  -d '{
    "url": "https://yourapp.com/webhooks/safeweb",
    "events": ["breach.new", "breach.resolved", "breach.unresolved", "email.added", "email.removed"]
  }'

Expose it to the internet (for example with ngrok or Cloudflare Tunnel) and register that public URL on a staging webhook endpoint.

/**
 * Minimal local webhook receiver for SafeWeb deliveries (Express).
 *
 * Prerequisites: npm install express
 *
 * Usage:
 *   WEBHOOK_SECRET_TOKEN=<uuid-from-signing-secret-api> node local-webhook-receiver.mjs
 * Optional: PORT=8765
 */
import { createHmac } from 'node:crypto';
import express from 'express';

const token = process.env.WEBHOOK_SECRET_TOKEN;
if (!token) {
  console.error('Missing WEBHOOK_SECRET_TOKEN (signing secret token UUID).');
  process.exit(1);
}

const port = Number(process.env.PORT ?? 8765);
const app = express();

app.get('/', (_req, res) => res.status(204).end());
app.head('/', (_req, res) => res.status(204).end());

app.post('/', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body instanceof Buffer ? req.body.toString('utf8') : '';
  const requestSig = req.get('x-signature');
  const responseToken = req.get('x-token');

  if (!requestSig || !responseToken) {
    res.status(400).send('missing X-Signature or X-Token');
    return;
  }

  const expectedRequestSig = createHmac('sha256', token)
    .update(rawBody)
    .digest('hex');
  if (requestSig !== expectedRequestSig) {
    res.status(401).send('invalid X-Signature');
    return;
  }

  try {
    console.dir(JSON.parse(rawBody), { depth: null });
  } catch {
    console.log(rawBody);
  }

  const responseSig = createHmac('sha256', token)
    .update(`${responseToken}:${rawBody}`)
    .digest('hex');

  res.status(200).setHeader('X-Signature', responseSig).json({ ok: true });
});

app.listen(port, '127.0.0.1', () => {
  console.error(`listening on http://127.0.0.1:${port}`);
});

API reference

Retries

If your endpoint returns a non-2xx status code or the request times out (2 seconds), SafeWeb retries delivery up to 3 times with exponential backoff. After all attempts are exhausted the delivery is logged as failed.

Managing Endpoints

All management endpoints use the standard SW-PARTNER-ID and SW-API-KEY authentication headers.

Create an endpoint

POST /api/v1/integrations/webhooks
FieldTypeRequiredDescription
urlstringYesYour HTTPS endpoint URL
eventsstring[]YesEvent types to subscribe to
activebooleanNoDefaults to true

Returns the endpoint including the secret. Store it securely — it is not returned on subsequent requests.

List endpoints

GET /api/v1/integrations/webhooks

Returns all webhook endpoints for your organization.

Update an endpoint

PATCH /api/v1/integrations/webhooks/{id}

Provide any combination of url, events, or active to update. At least one field is required.

Delete an endpoint

DELETE /api/v1/integrations/webhooks/{id}

Permanently removes the endpoint and all associated delivery logs.

Best Practices

  • Respond quickly — return a 2xx within a few seconds and process the payload asynchronously. Deliveries time out after 2 seconds.
  • Use HTTPS — webhook URLs must be publicly reachable HTTPS endpoints.
  • Verify every delivery — always validate the X-SafeWeb-Signature header before trusting the payload.
  • Handle duplicates — in rare cases the same event may be delivered more than once. Use the created_at and payload fields to deduplicate.
  • Monitor failures — if your endpoint is consistently failing, disable it and investigate before re-enabling.

On this page