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.computed

Fired 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.

FieldTypeDescription
client_idstringTenant identifier
user_idstringThe user who was matched
matched_user_idstringThe other user in the match
scorefloatMatch compatibility score (0.0–1.0)
clustering.completed

Fired when a clustering pipeline run completes successfully. Use this to trigger segmentation updates, refresh dashboards, or sync cluster assignments to your application.

FieldTypeDescription
client_idstringTenant identifier
consent_layer_idstringConsent layer the clustering was scoped to
clustering_run_idstringThe pipeline run that produced these clusters
clustersarraySummary of discovered clusters (IDs, sizes, labels)
clustering.failed

Fired when a clustering pipeline run fails. Use this to alert your operations team or trigger fallback logic.

FieldTypeDescription
client_idstringTenant identifier
consent_layer_idstringConsent layer the clustering was scoped to
error_detailsstringHuman-readable error description
campaign.budget_exhausted

Fired 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.

FieldTypeDescription
client_idstringTenant identifier
campaign_idstringThe campaign that exhausted its budget
cluster.drift_detected

Fired 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.

FieldTypeDescription
client_idstringTenant identifier
cluster_idstringThe 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

HeaderDescription
Content-TypeAlways application/json
X-Simbee-EventThe event type (e.g. match.computed)
X-Simbee-SignatureHMAC-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");
});
Always use constant-time comparison (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:

AttemptDelay
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