Webhooks
Simbee delivers webhook notifications when platform events occur. Subscribe to event types, verify signatures, and build reactive integrations that respond to matches, clustering, and campaign changes in real time.
Subscribing
Create a webhook subscription by providing a URL, the event types you want to receive, and an optional signing secret for payload verification.
curl -X POST https://api.simbee.io/api/v1/clients/cl_abc123/webhooks \
-H "Authorization: Bearer $SIMBEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/simbee",
"event_types": ["match.computed", "clustering.completed"],
"secret": "whsec_your_signing_secret"
}'Event catalog
Five event types are available for webhook delivery. Internal service-to-service events (signal processing, embedding regeneration, etc.) are not exposed via webhooks.
match.computedFired when a match is computed between two users. Use this to notify users of new matches, trigger downstream workflows, or update your application's match state.
| Field | Type | Description |
|---|---|---|
client_id | string | Tenant identifier |
user_id | string | The user who was matched |
matched_user_id | string | The other user in the match |
score | float | Match compatibility score (0.0–1.0) |
clustering.completedFired when a clustering pipeline run completes successfully. Use this to trigger segmentation updates, refresh dashboards, or sync cluster assignments to your application.
| Field | Type | Description |
|---|---|---|
client_id | string | Tenant identifier |
consent_layer_id | string | Consent layer the clustering was scoped to |
clustering_run_id | string | The pipeline run that produced these clusters |
clusters | array | Summary of discovered clusters (IDs, sizes, labels) |
clustering.failedFired when a clustering pipeline run fails. Use this to alert your operations team or trigger fallback logic.
| Field | Type | Description |
|---|---|---|
client_id | string | Tenant identifier |
consent_layer_id | string | Consent layer the clustering was scoped to |
error_details | string | Human-readable error description |
campaign.budget_exhaustedFired when a campaign's total spend reaches its budget. The campaign is automatically completed. Use this to notify campaign managers or trigger follow-up campaigns.
| Field | Type | Description |
|---|---|---|
client_id | string | Tenant identifier |
campaign_id | string | The campaign that exhausted its budget |
cluster.drift_detectedFired when a cluster's composition has shifted significantly between runs. Use this to detect changing user segments, trigger re-targeting, or alert on behavioral shifts.
| Field | Type | Description |
|---|---|---|
client_id | string | Tenant identifier |
cluster_id | string | The cluster that drifted |
Payload format
Every webhook delivery is an HTTP POST with a JSON body containing the event type, payload data, and a timestamp.
POST https://your-app.com/webhooks/simbee
Content-Type: application/json
X-Simbee-Event: match.computed
X-Simbee-Signature: a1b2c3d4e5f6...
{
"event_type": "match.computed",
"data": {
"client_id": "cl_abc123",
"user_id": "usr_abc123",
"matched_user_id": "usr_def456",
"score": 0.87
},
"timestamp": "2026-04-11T14:30:00Z"
}Headers
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Simbee-Event | The event type (e.g. match.computed) |
X-Simbee-Signature | HMAC-SHA256 hex digest of the request body (present only if a secret was provided) |
Signature verification
If you provided a secret when creating the subscription, every delivery includes an X-Simbee-Signature header. The signature is the HMAC-SHA256 hex digest of the raw request body using your secret as the key. Always verify signatures before processing webhook payloads.
import crypto from "crypto";
function verifyWebhook(
body: string,
signature: string,
secret: string,
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
// In your webhook handler:
app.post("/webhooks/simbee", (req, res) => {
const signature = req.headers["x-simbee-signature"];
const rawBody = req.body; // raw string, not parsed JSON
if (!verifyWebhook(rawBody, signature, "whsec_your_signing_secret")) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(rawBody);
console.log(event.event_type, event.data);
res.status(200).send("OK");
});timingSafeEqual, secure_compare, compare_digest) when verifying signatures. Standard string equality is vulnerable to timing attacks.Delivery guarantees
At-least-once delivery
Simbee guarantees at-least-once delivery. If your endpoint returns a non-2xx status code or times out, the delivery is retried. This means your handler may receive the same event more than once — design your handlers to be idempotent.
Retry schedule
Failed deliveries are retried up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | ~30 seconds |
| 2nd retry | ~1 minute |
| 3rd retry | ~2 minutes |
| 4th retry | ~4 minutes |
| 5th retry | ~8 minutes |
After 5 failed attempts the event is dropped. Paused subscriptions skip delivery entirely — events that occur while a subscription is paused are not queued for later delivery.
Idempotency
Your webhook handler should be idempotent. Use the combination of event_type + timestamp + payload fields (e.g. user_id) as a deduplication key if your handler has side effects.
Managing subscriptions
List subscriptions
curl https://api.simbee.io/api/v1/clients/cl_abc123/webhooks \
-H "Authorization: Bearer $SIMBEE_TOKEN"Update a subscription
Change the URL, event types, or status of an existing subscription. Set status to paused to temporarily stop deliveries without deleting the subscription.
# Update event types
curl -X PATCH https://api.simbee.io/api/v1/clients/cl_abc123/webhooks/wh_abc123 \
-H "Authorization: Bearer $SIMBEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "event_types": ["match.computed", "campaign.budget_exhausted"] }'
# Pause a subscription
curl -X PATCH https://api.simbee.io/api/v1/clients/cl_abc123/webhooks/wh_abc123 \
-H "Authorization: Bearer $SIMBEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "paused" }'Delete a subscription
curl -X DELETE https://api.simbee.io/api/v1/clients/cl_abc123/webhooks/wh_abc123 \
-H "Authorization: Bearer $SIMBEE_TOKEN"Integration examples
These examples show how to use webhooks for different application patterns. Each subscribes to different event types and handles the payload differently.
Match notifications
Subscribe to match.computed. When a match is found, notify both users via your application's notification system.
// Handle match.computed
if (event.event_type === "match.computed") {
const { user_id, matched_user_id, score } = event.data;
await notifyUser(user_id, {
message: "You have a new match!",
match_id: matched_user_id,
compatibility: score,
});
}Segmentation pipeline
Subscribe to clustering.completed. When clustering finishes, pull cluster assignments and update your application's user segments.
// Handle clustering.completed
if (event.event_type === "clustering.completed") {
const { clustering_run_id, clusters } = event.data;
for (const cluster of clusters) {
const members = await simbee.clusters.listMembers(cluster.id);
await syncSegment(cluster.label, members);
}
}Campaign budget alerting
Subscribe to campaign.budget_exhausted. When a campaign runs out of budget, alert the marketing team and optionally create a follow-up campaign.
// Handle campaign.budget_exhausted
if (event.event_type === "campaign.budget_exhausted") {
const { campaign_id } = event.data;
await slack.send("#marketing", `Campaign ${campaign_id} budget exhausted`);
// Optionally create a follow-up campaign
const analytics = await simbee.campaigns.getAnalytics(campaign_id);
if (analytics.engagement_rate > 0.05) {
await simbee.campaigns.create({ ...renewedConfig });
}
}Drift monitoring
Subscribe to cluster.drift_detected. When user segments shift, log the change for analysis and alert if a high-value segment is affected.
// Handle cluster.drift_detected
if (event.event_type === "cluster.drift_detected") {
const { cluster_id } = event.data;
const cluster = await simbee.clusters.get(cluster_id);
logger.info(`Cluster "${cluster.label}" drifted (size: ${cluster.cluster_size})`);
if (watchedSegments.includes(cluster.label)) {
await alertOps(`High-value segment "${cluster.label}" has drifted`);
}
}Next steps
- Getting Started: Webhooks — Create your first webhook subscription.
- API Reference — Full webhook endpoint schemas.
- Use Case Recipes — See webhooks in context with complete integration patterns.