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:
Partner API
Manage webhooks via the Partner API.
Distributor API
Manage webhooks via the Distributor API.
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
| Header | Meaning |
|---|---|
Content-Type | Always application/json. |
X-Signature | Hex-encoded HMAC-SHA256 of the raw JSON body bytes, using your current active signing secret token (UUID string from secret generation). |
X-Token | A 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
}| Field | Description |
|---|---|
idempotencyToken | Stable id for this logical event instance (used for deduplication if you see retries). |
type | Canonical event name (see Event types). |
data | Event 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. |
timestamp | Milliseconds 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-Signaturewith hex-encoded HMAC-SHA256 over the UTF-8 string formed by: theX-Tokenheader 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 requestX-Signatureand for computing the expected responseX-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-
2xxHTTP status from your server; - an invalid or missing response
X-Signature(the header does not match the value expected for the request body andX-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):
type | When it fires |
|---|---|
customer.created | A new customer record exists for the org. |
customer.deleted | A customer was removed / offboarded. |
customer.asset.added | One or more domains or emails were added to monitoring. |
customer.asset.removed | Assets were removed. |
customer.breach.found | New breach rows were detected for the customer. |
customer.breach.resolved | Breach(es) transitioned to resolved. |
customer.breach.unresolved | Breach(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
- 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.
- Register your HTTPS URL — Create webhook endpoint with
url,eventTypes, optionalenabledandmaxAttempts(default retry budget per delivery). - Implement the POST handler — Verify
X-Signature, parse JSON, process asynchronously, return200with a correctX-Signatureresponse header as above. - 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
- Partner API — List endpoints, create, get, update, delete, and signing secrets.
- Distributor — Webhook endpoints (list, create, get, update, delete) and signing secret lifecycle (generate, cycle, deactivate).
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| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Your HTTPS endpoint URL |
events | string[] | Yes | Event types to subscribe to |
active | boolean | No | Defaults to true |
Returns the endpoint including the secret. Store it securely — it is not returned on subsequent requests.
List endpoints
GET /api/v1/integrations/webhooksReturns 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
2xxwithin 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-Signatureheader before trusting the payload. - Handle duplicates — in rare cases the same event may be delivered more than once. Use the
created_atand payload fields to deduplicate. - Monitor failures — if your endpoint is consistently failing, disable it and investigate before re-enabling.
Get partner analytics GET
Retrieve high-level analytics about your partner account including total customers, active monitors, and breach statistics. Requires valid partner authentication via SW-PARTNER-ID and SW-API-KEY headers.
Webhooks overview
Partner-facing HTTP APIs to register outbound webhook URLs and manage signing secrets. For the delivery protocol, event catalogue, and receiver implementation details, read the top-level Outbound webhooks guide.