Skip to main content
DeveloperIntegrations

Webhooks

Send and receive webhooks in NimbusOS for real-time event integration with external systems, with HMAC signature verification and retry logic.

9 min read
Updated April 23, 2026
1,950 words

Webhooks in NimbusOS cover two flows: outbound webhooks that push events from NimbusOS to your systems, and inbound webhooks that ingest events from providers like ReachInbox, Stripe, and Clay. Both use HMAC-SHA256 signature verification, exponential backoff retry, and per-endpoint audit logs. This article covers the webhook model, setup for both directions, the event types available, and the verification and retry behavior.

Outbound Webhooks

An outbound webhook sends a POST request to your endpoint when a subscribed event occurs in NimbusOS. Typical uses:

  • Notify a custom data pipeline when contacts change
  • Fire a Slack or Zapier webhook on campaign events
  • Push engagement events to a data warehouse in near real time
  • Trigger downstream automation when a meeting is booked

Setting up an outbound webhook

Go to Integrations -> Webhooks -> Add Endpoint. Configure:

  • URL. The POST destination. Must be HTTPS.
  • Event subscriptions. One or more event types from the catalog.
  • Signing secret. A random string you generate and share between NimbusOS and your endpoint. Used for HMAC verification.
  • Active. Toggle to pause without deleting.
  • Custom headers. Optional. Added to every request.

Save. The endpoint receives a test event immediately for verification.

Event types

The outbound event catalog includes 40+ events. Categories:

Contact events. contact.created, contact.updated, contact.deleted, contact.enriched, contact.scored, contact.tagged.

Campaign events. campaign.created, campaign.started, campaign.paused, campaign.completed, campaign.auto_paused.

Send events. email.sent, email.delivered, email.opened, email.clicked, email.replied, email.bounced, email.unsubscribed, email.spam_complaint.

Reply intelligence events. reply.classified, reply.interested, reply.meeting_request, reply.objection.

Sequence events. sequence.enrolled, sequence.step_completed, sequence.exited.

Deliverability events. deliverability.alert_fired, deliverability.inbox_paused, deliverability.nds_dropped.

Billing events (enterprise). billing.plan_changed, billing.invoice_paid, billing.usage_threshold.

Payload format

Every webhook is a JSON POST. The body has:

{
  "event_type": "email.replied",
  "event_id": "evt_abc123",
  "workspace_id": "ws_xyz789",
  "occurred_at": "2026-04-23T14:32:11Z",
  "data": {
    "campaign_id": "camp_abc",
    "contact_id": "cont_xyz",
    "reply_intent": "interested",
    "confidence": 0.94
  }
}

The data object varies by event type. Full schema for each event is in the API reference.

Signature verification

Every outbound webhook includes three headers.

  • X-Nimbus-Signature: HMAC-SHA256 hex of the request body, keyed with the signing secret.
  • X-Nimbus-Timestamp: Unix timestamp of the request.
  • X-Nimbus-Event-Id: Unique event ID for idempotency.

On receipt, your endpoint should:

  1. Read the body as raw bytes (not parsed JSON, to avoid whitespace differences).
  2. Compute HMAC-SHA256 of timestamp.body using the shared secret.
  3. Compare to the X-Nimbus-Signature header using constant-time comparison.
  4. Reject the request if signatures do not match or if the timestamp is older than 5 minutes.

A Python example:

import hmac, hashlib, time

def verify_webhook(request_body, timestamp, signature, secret):
    if abs(time.time() - int(timestamp)) > 300:
        return False
    payload = f"{timestamp}.{request_body}"
    expected = hmac.new(
        secret.encode(), payload.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Retry behavior

A webhook is considered successful if your endpoint returns a 2xx status within 30 seconds. Any other outcome triggers retry.

Retry schedule:

  • Attempt 1: immediate
  • Attempt 2: 30 seconds later
  • Attempt 3: 2 minutes later
  • Attempt 4: 10 minutes later
  • Attempt 5: 1 hour later
  • Attempt 6: 4 hours later
  • Attempt 7: 24 hours later

After 7 failed attempts, the event is marked failed and your endpoint is marked unhealthy. Repeated unhealthy status may auto-pause the endpoint.

Retried events carry the same X-Nimbus-Event-Id. Your endpoint should be idempotent.

Event ordering

Webhooks are not strictly ordered. Events for a single contact might arrive out of order due to retries or parallel processing.

If order matters for your workflow, use the occurred_at timestamp and buffer briefly to reorder. Or query the NimbusOS API to fetch canonical state rather than relying on the webhook's data field alone.

Endpoint limits

Per workspace:

  • Up to 20 active outbound webhook endpoints
  • Up to 40 event types per endpoint (practically unlimited)
  • Rate limit: 100 requests per second to a single endpoint (your endpoint must handle this)

Inbound Webhooks

Inbound webhooks ingest events from providers. Core supported providers:

  • ReachInbox (send, delivery, open, click, reply, bounce, unsubscribe)
  • Stripe (billing events)
  • Clay (enrichment completion)
  • Apify (LinkedIn Jobs Scraper results)

Each provider has its own webhook configuration and signing scheme. The verification logic is provider-specific but the pattern is consistent.

ReachInbox webhook

Configure the webhook URL in ReachInbox's campaign settings. Point to https://getnimbusos.com/api/webhooks/reachinbox/. Use the signing secret from your Integrations -> ReachInbox settings.

ReachInbox signs with HMAC-SHA256. NimbusOS verifies on ingress.

Stripe webhook

For the billing integration. Configure the endpoint in Stripe's webhook settings. NimbusOS handles subscription events, invoice events, and payment events.

Stripe uses its own signing scheme (also HMAC-SHA256 but with a different timestamp format). The verification code is built into NimbusOS.

Clay webhook

Configure in Clay's integration settings. NimbusOS handles enrichment completion events. The payload includes the Clay workspace identifier and the enriched data.

Generic webhook endpoint

For custom integrations, a generic webhook endpoint at https://getnimbusos.com/api/webhooks/custom/:secret/ accepts any JSON POST. The secret is used for basic authentication (shared secret, not HMAC). Workflows receive the payload as input for downstream automation.

Webhook Testing

Each endpoint has a Test button in the Integrations UI. Clicking it sends a synthetic test event to your endpoint with a known-good signature. You can verify:

  • Your endpoint is reachable
  • Your signature verification works
  • Your processing logic runs without errors

Test events have event_type=webhook.test and a predictable payload.

Webhook Debugging

The Integrations -> Webhooks page shows per-endpoint delivery logs.

  • Successful deliveries (2xx response)
  • Failed deliveries with status codes and response bodies
  • Retry attempts with timestamps

A failed delivery can be manually retried from the log. Logs retain for 30 days.

Common debugging patterns:

"My endpoint returns 200 but NimbusOS shows failure"

The platform treats any response body over 1 MB as a failure to prevent abuse. Return a small body (204 No Content or {"status": "ok"} is ideal).

"Signature verification keeps failing"

Check that you are signing the raw body, not the parsed JSON (whitespace differences break signatures). Check that you are using the timestamp correctly.

"Events arrive out of order"

Expected. Use the occurred_at timestamp to reorder, or fetch state from the API for canonical data.

Webhook Security

Five security considerations.

HTTPS required. Webhooks will not deliver to HTTP endpoints. TLS is enforced.

Signature verification. Always verify. Without verification, an attacker can forge events.

Timestamp check. Reject requests older than 5 minutes to prevent replay attacks.

Idempotency. Design your endpoint to handle the same event ID multiple times safely.

IP allowlisting. Optional but supported. Restrict your endpoint to accept only from NimbusOS's published egress IP range.

Webhook Alerts

The platform sends an alert when:

  • An endpoint has 10 consecutive failures.
  • An endpoint has over 50 percent failure rate in 1 hour.
  • An endpoint has been failing for 24 hours continuously.

Alerts route to the workspace alert recipients.

Event Volume Estimation

Typical event volume per workspace per day:

  • 100-inbox fleet sending 5k emails per day: roughly 25k webhook events per day (send, delivered, opened, clicked events amplify the base send count)
  • Large fleet at 50k emails per day: 250k events per day.

Plan your endpoint capacity accordingly. Most custom endpoints should handle at least 10 requests per second.

Frequently Asked Questions

Can I filter events at the NimbusOS side or do I have to filter at my endpoint?

Filter at the NimbusOS side. The endpoint configuration specifies exactly which event types to subscribe to. Events outside the subscription are never sent.

Is there a webhook payload size limit?

Yes. Payloads over 1 MB are truncated with a flag. Large data fields (for example, the full email body on a email.replied event) are replaced with a reference URL that your endpoint can fetch separately.

Do webhooks respect workspace tenant isolation?

Yes. A webhook configured in workspace A receives only events from workspace A.

What happens if I disable and re-enable an endpoint?

Disabled endpoints do not receive events. Enabling resumes from the current event stream; no backlog of missed events is replayed.

Can I receive historical events?

Historical events up to 30 days can be replayed from the Integrations UI. Select an event range and trigger a replay. Replayed events are marked with X-Nimbus-Replay: true header.

Useful next pages after this one: Webhooks API for managing webhooks programmatically, Authentication for the API authentication that webhook management uses, and Integration Overview for the broader integration context.

Related articles

Still stuck?

Our team answers every support ticket. If the answer is not in the docs, open a ticket and we will write the missing page.