Quick Links

Using Blumira Webhooks

Overview

You can use Blumira Webhooks to send notifications to your HTTPS endpoints when finding and case events occur. You can use these notifications to trigger SOAR playbooks, create tickets in external systems, feed SIEMS, and drive custom automation.

Key characteristics

The table below explains some of the key characteristics of the Blumira Webhooks feature.

Characteristic What it means
Outbound only Blumira sends notifications to your endpoint; you cannot send notifications to Blumira.
Per-org configuration Each webhook belongs to a single Blumira account.
HMAC-signed  Blumira signs each delivery, so you can verify that the notification came from Blumira. 
Filterable You choose which event types you will receive notifications for.
Automatic retries Blumira retries failed deliveries up to five times over approximately one hour and keeps an audit trail of every delivery attempt.
Auto-disable To prevent noise, Blumira automatically disables webhook endpoints that have sustained failures.

Supported event types

Blumira delivers notifications for the finding events, case events, and synthetic events detailed below.

Event type Fires when
finding.created Blumira creates a new finding.
finding.status.changed A finding's status changes.
finding.owners.changed Someone assigns or reassigns a finding.
finding.comment.added Someone adds a comment to a finding.
case.created Blumira creates a new case.
case.status.changed A case's status changes.
case.severity.changed A case's severity level changes.
webhook.test You trigger a test delivery from the UI or API. This is not a real event.
webhook.secret.rotated Your endpoint's HMAC secret is rotated. During the overlap window, Blumira dual-signs this event.

Getting started

Endpoint URL requirements

When you create your endpoint, ensure the following:

  • You use HTTPS only. Blumira rejects HTTP, file://, and custom schemes.
  • The endpoint always resolves to a public IP. Blumira blocks private IPs (RFC 1918), loopback, link-local, metadata endpoints (169.254.169.254), and CGNAT ranges.
Note: Blumira re-resolves your hostname and validates and pins the hostname’s IP on delivery to protect against DNS rebinding. If your hostname resolves to a private or internal address on delivery, Blumria rejects the delivery. Ensure your endpoint always resolves to a public IP.

Creating a webhook

To create a webhook, do the following:

  1. In Blumira, navigate to Settings > Webhooks or to MSP Portal > Webhooks if you are an MSP managing customer sub-account data.

  2. Click Add Webhook.
  3. Type your Endpoint URL.
  4. (Optional) Add a description.
  5. (Optional) Under Custom headers, add a header by typing a Header name and Value. See more at HTTP headers below.

    Note: To add additional headers, click + Add header, and type the Header name and Value for each header you want to add.
  6. (Optional) Under Event filters, click + Add filter, and select the Event type and finding type, priorities, or case severity beneath that type, depending on the events you want to filter for. See more at Filtering webhook events below.
  7. Click Create webhook.
  8. Copy and save the HMAC signing secret.

    Important: You must copy the secret before you close the window. You will not be able to see the secret again, and if you lose it, you must rotate to get a new one.
  9. Click Done.

Sending a test delivery

To test your webhook, do the following:

  1. In the Webhooks table, locate the endpoint URL you want to test.
  2. Click the ellipsis at the end of the row.
  3. In the context menu, click Send Test.

Example: See the code below for an example of a test payload.

{
  "id": "evt_ddee1122-ccdd-4eef-8899-001122334455",
  "type": "webhook.test",
  "schema_version": "1",
  "livemode": false,
  "event_created_at": "2026-05-13T14:22:31.123456Z",
  "delivery_created_at": "2026-05-13T14:22:33.654321Z",
  "org_id": "0a1b2c3d-1111-2222-3333-444455556666",
  "msp_org_id": null,
  "data": {
    "endpoint_id": "aa11bb22-cc33-dd44-ee55-ff6677889900",
    "message": "Test delivery from Blumira webhook-integration. If you can read this, your signature verification and handler are working.",
    "triggered_by": {
      "kind": "customer_user",
      "person_id": "11111111-2222-3333-4444-555555555555",
      "email": "responder@customer.example.com",
      "name": "Jane Responder"
    }
  }
}

Understanding payload format

Each delivery to your endpoint contains crucial information including the event type, when the event was created, and who or what triggered the event. You can find details about the data Blumira includes in each section of the delivery below.

JSON envelope

Every webhook delivery uses a consistent JSON envelope.

Example:

{
  "schema_version": "1",
  "id": "evt_a1b2c3d4-e5f6-7890-abcd-ef0123456789",
  "type": "case.status.changed",
  "livemode": true,
  "event_created_at": "2026-05-13T14:22:31Z",
  "delivery_created_at": "2026-05-13T14:22:33Z",
  "org_id": "0a1b2c3d-1111-2222-3333-444455556666",
  "msp_org_id": null,
  "data": { }
}

See the table below for a description of each field.

Field Type Description
schema_version string Always "1". Blumira increments this value on breaking changes.
id string Idempotency key. Format: evt_<UUID>. Use this for deduplication. Stable across retries.
type string Event type discriminator (e.g., finding.created).
livemode boolean true for real production events. false only for webhook.test.
event_created_at string (ISO 8601) When the source event occurred upstream. Stable across retries.
delivery_created_at string (ISO 8601) When Blumira enqueued the delivery for your endpoint.
org_id string (UUID) The child organization that produced the event.
msp_org_id string (UUID) or null The MSP parent org ID, if the notification is delivered from an MSP-tier webhook. Always present as a key;null when coming from single-org webhooks.
data object Event-type-specific payload. See different payloads by event type below.

Envelope timestamps (i.e., event_created_at, delivery_created_at) include six fractional digits when the source has sub-second precision and omit fractional digits when the source does not.

Timestamps inside data (e.g., created_at, matched, expires_at) come from the original source and might use different precision. Always use a standard ISO 8601 or RFC 3339 parser. Do not rely on a fixed number of fractional digits.

HTTP headers

Every delivery includes the following headers:

  • Content-Type
  • User-Agent
  • X-Blumira-Signature
  • X-Blumira-Event-Type
  • X-Blumira-Delivery-Id
  • X-Blumira-Delivery-Attempt

If you configured custom headers or an authorization header, the delivery includes those headers as well. See more about custom headers and authorization headers below.

Example:

Content-Type:                 application/json
User-Agent:                   Blumira-Webhooks/1.0
X-Blumira-Signature:          t=1715608951,v1=<hex_hmac_sha256>
X-Blumira-Event-Type:         case.status.changed
X-Blumira-Delivery-Id:        0a1b2c3d-4e5f-4789-9bcd-ef0123456789
X-Blumira-Delivery-Attempt:   1

X-Blumira-Delivery-Attempt increments from 1 to 5 as Blumira retries failed deliveries.

Org_id and msp_org_id are not available as HTTP headers. If you want to route by organization without parsing the body, use the X-Blumira-Event-Type header for event-level routing.

Event type data block

The data object varies by event type. The following sections illustrate the shape for each supported event:

Many events include an actor field that identifies who or what triggered the event. See the table below for a description of each actor.

Actor Shape Description
customer_user {"kind": "customer_user", "person_id": "...", "email": "...", "name": "..."} A user in your organization.
blumira_analyst {"kind": "blumira_analyst", "person_id": "..."} A Blumira analyst. Email and name are never included.
blumira_system {"kind": "blumira_system"} An automated Blumira process.

Finding created

{
  "finding_id": "ffffffff-1111-2555-0000-000000000001",
  "finding_short_id": "F-26-21-A1B2",
  "finding_web_url": "https://app.blumira.com/.../findings/...",
  "name": "Suspect: AD Domain Admin Created",
  "summary": "An account was promoted to Domain Admin from an unusual source IP.",
  "priority": 2,
  "priority_label": "P2",
  "status": "open",
  "type": "suspect",
  "category": "privilege_escalation",
  "owners": {
    "analysts": [{"kind": "blumira_analyst", "person_id": "..."}],
    "responders": [{"kind": "customer_user", "person_id": "...", "email": "...", "name": "..."}],
    "managers": []
  },
  "actor": {"kind": "blumira_system"},
  "src_country": ["US"],
  "matched": "2026-05-13T14:20:00.000000Z",
  "created_at": "2026-05-13T14:22:31.000000Z"
}

Finding status changed

{
  "finding_id": "...",
  "finding_short_id": "F-26-21-A1B2",
  "finding_web_url": "https://app.blumira.com/.../findings/...",
  "name": "Suspect: AD Domain Admin Created",
  "status_change": {"previous": "open", "current": "resolved"},
  "priority": 2,
  "priority_label": "P2",
  "type": "suspect",
  "category": "privilege_escalation",
  "resolution": "false_positive",
  "resolution_notes": "Confirmed-noise rule was misconfigured; updated.",
  "actor": {"kind": "blumira_analyst", "person_id": "..."},
  "created_at": "2026-05-13T15:02:10.000000Z"
}

Finding owners changed

{
  "finding_id": "...",
  "finding_short_id": "F-26-21-A1B2",
  "finding_web_url": "https://app.blumira.com/.../findings/...",
  "name": "Suspect: AD Domain Admin Created",
  "status": "open",
  "priority": 2,
  "priority_label": "P2",
  "type": "suspect",
  "category": "privilege_escalation",
  "owners_change": {
    "analysts": {
      "added": [{"kind": "blumira_analyst", "person_id": "..."}],
      "removed": []
    },
    "responders": {
      "added": [],
      "removed": [{"kind": "customer_user", "person_id": "...", "email": "old-responder@example.com", "name": "Olivia Old"}]
    },
    "managers": {"added": [], "removed": []}
  },
  "actor": {"kind": "customer_user", "person_id": "...", "email": "admin@example.com", "name": "Alex Admin"},
  "created_at": "2026-05-13T15:10:00.000000Z"
}

Finding comment added

{
  "finding_id": "...",
  "finding_short_id": "F-26-21-A1B2",
  "finding_web_url": "https://app.blumira.com/.../findings/...",
  "type": "suspect",
  "category": "privilege_escalation",
  "comment_id": "bbbbbbbb-0000-0000-0000-0000000000e1",
  "comment_created_at": "2026-05-13T15:20:00.000000Z",
  "actor": {"kind": "blumira_analyst", "person_id": "..."},
  "comment_body": "Confirmed false positive — closing this out.",
  "comment_body_truncated": false
}

Blumira caps the comment_body field at 64 KiB (65,536 bytes of UTF-8). If the comment exceeds the cap, the body is truncated at a code-point boundary and comment_body_truncated is true. Otherwise, comment_body_truncated is false.

Case created

{
  "case_id": "550e8400-e29b-41d4-a716-446655440000",
  "case_web_url": "https://torch.blumira.com/cases/case?id=550e8400-...",
  "title": "Suspicious admin account creation",
  "status": "new",
  "severity": "high",
  "created_at": "2026-05-13T14:22:31.000000Z",
  "expires_at": "2026-05-20T14:22:31.000000Z",
  "customer_summary": "Active Directory admin role assigned to a recently-created account.",
  "detection_names": ["AD: Domain Admin Created"],
  "findings": [{"finding_id": "...", "detection_name": "AD: Domain Admin Created"}],
  "findings_total_count": 1,
  "resolution": null,
  "resolution_notes": null,
  "status_changed_at": null,
  "severity_changed_at": null,
  "actor": {"kind": "blumira_system"},
  "owners": []
}

Expires_at shows when the case expires per Blumira's current expiry policy. This field is only meaningful when the case status is active (i.e., new or investigating). Use the expires_at value we provide. Do not compute expiry from created_at.

Blumira caps findings at 10 items. Findings_total_count is the full uncapped count. Use findings_total_count for routing rules such as "escalate when a case has 5 or more findings."

Case status changed

{
  "case_id": "550e8400-e29b-41d4-a716-446655440000",
  "case_web_url": "https://torch.blumira.com/cases/case?id=550e8400-...",
  "title": "Suspicious admin account creation",
  "status": "investigating",
  "severity": "high",
  "status_change": {"previous": "new", "current": "investigating"},
  "created_at": "2026-05-13T14:22:31.000000Z",
  "expires_at": "2026-05-20T14:22:31.000000Z",
  "customer_summary": "...",
  "detection_names": ["AD: Domain Admin Created"],
  "findings": [{"finding_id": "...", "detection_name": "AD: Domain Admin Created"}],
  "findings_total_count": 1,
  "resolution": null,
  "resolution_notes": null,
  "status_changed_at": "2026-05-13T15:10:00.000000Z",
  "severity_changed_at": null,
  "actor": {"kind": "customer_user", "person_id": "...", "email": "responder@example.com", "name": "Jane Responder"},
  "owners": []
}

Resolution and resolution_notes are null until the status transitions to a resolved state.

Case severity changed

{
  "case_id": "550e8400-e29b-41d4-a716-446655440000",
  "case_web_url": "https://torch.blumira.com/cases/case?id=550e8400-...",
  "title": "Suspicious admin account creation",
  "status": "investigating",
  "severity": "high",
  "severity_change": {"previous": "low", "current": "high", "direction": "escalated"},
  "created_at": "2026-05-13T14:22:31.000000Z",
  "expires_at": "2026-05-20T14:22:31.000000Z",
  "customer_summary": "...",
  "detection_names": ["AD: Domain Admin Created"],
  "findings": [{"finding_id": "...", "detection_name": "AD: Domain Admin Created"}],
  "findings_total_count": 1,
  "resolution": null,
  "resolution_notes": null,
  "status_changed_at": "2026-05-13T15:10:00.000000Z",
  "severity_changed_at": "2026-05-14T09:30:00.000000Z",
  "actor": {"kind": "blumira_system"},
  "owners": []
}

If a case's severity changes after it is closed or expired, Blumira will not deliver a case.severity.changed event. The severity on other case events (e.g., case.status.changed) will still show the current value.

Finding_web_url and case_web_url are browser links, not API endpoints. These links point to the Blumira web UI and return HTML for browser navigation, not JSON. To fetch current resource state programmatically, use the Blumira public API endpoints constructed from the finding_id or case_id in the payload.

Filtering webhook events

By default, Blumira sends all events to your webhook endpoint. Add webhook event filters to narrow delivery to specific event types, priorities, and severities. See the table below for what each filter option specifies.

Field Applies To Description
event_type All Required. Select the event type you want to filter for (e.g., finding.created).
finding_types Finding events only

Optional array. Filter by finding type: operational, risk, suspect, threat, system

If this field is empty, you will receive all finding types for the event you selected.

priorities Finding events only

Optional array. Filter by finding priority: P1, P2, P3

If this field is empty, you will receive all priorities for the type you selected.

severities Case events only

Optional array. Filter by case severity: critical, high, medium, low.

If this field is empty, you will receive all severities for the event you selected. 

Filter logic

Filters function in the following ways:

  • An endpoint with no filters receives all events (every event type, no dimension narrowing). This is the default for a newly created webhook with no filters added.
  • When a webhook has one or more filters, subscription becomes per-event-type. The endpoint receives only the event types for which it has a filter row. Adding your first filter narrows the events Blumira delivers to the endpoint from "all events" to "only these."
  • Within a single filter row, the dimension arrays (e.g., finding_types, priorities, severities) are conjunctive, so an event must match all specified dimensions to pass to the endpoint. An empty or omitted dimension array means "all values" for that dimension.
  • Multiple filter rows for different event types are disjunctive, so an event matching any filter passes.
  • You can remove all filters and return an endpoint to "all events" by sending filters: [] on update.
Important: Filters are fully replaced on update. Send the complete set of desired filters on every PUT, not a delta.

Security

HMAC signatures

Every delivery includes a message ({timestamp}.{raw_request_body}) that is signed with your webhook's HMAC signature. The signature’s format is:

t=<unix_timestamp>,v1=<hex_hmac_sha256_digest>

The signature is in the X-Blumira-Signature header.

Example:

X-Blumira-Signature: t=1715608951,v1=a1b2c3d4e5f6...

Verifying signatures with code

Use the following to verify HMAC signatures, depending on your preferred language:

Language Code
Python
import base64
import hashlib
import hmac
import time
def verify_webhook(payload_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    """Verify a Blumira webhook signature.
    Args:
        payload_body: The raw request body bytes.
        signature_header: The X-Blumira-Signature header value.
        secret: Your base64-encoded HMAC secret.
        tolerance_seconds: Max age of the signature in seconds (default 5 minutes).
    """
    parts = signature_header.split(",")
    if not parts[0].startswith("t="):
        return False
    timestamp = int(parts[0][2:])
    # Reject stale or future-dated signatures
    if abs(time.time() - timestamp) > tolerance_seconds:
        return False
    # Compute the expected signature
    key = base64.b64decode(secret)
    message = f"{timestamp}.".encode("utf-8") + payload_body
    expected = hmac.new(key, message, hashlib.sha256).hexdigest()
    # Accept if any v1= segment matches (supports dual-signing during rotation)
    for part in parts[1:]:
        if part.startswith("v1=") and hmac.compare_digest(part[3:], expected):
            return True
    return False

 

Node.js
const crypto = require('crypto');
function verifyWebhook(payloadBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = signatureHeader.split(',');
  if (!parts[0].startsWith('t=')) return false;
  const timestamp = parseInt(parts[0].substring(2), 10);
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const key = Buffer.from(secret, 'base64');
  const message = `${timestamp}.${payloadBody}`;
  const expected = crypto.createHmac('sha256', key).update(message).digest('hex');
  return parts.slice(1).some(part => {
    if (!part.startsWith('v1=')) return false;
    const sig = Buffer.from(part.substring(3));
    const exp = Buffer.from(expected);
    if (sig.length !== exp.length) return false;
    return crypto.timingSafeEqual(sig, exp);
  });
}
Go
package main
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "math"
    "strconv"
    "strings"
    "time"
)
func VerifyWebhook(body []byte, signatureHeader, secret string, toleranceSec int64) bool {
    parts := strings.Split(signatureHeader, ",")
    if !strings.HasPrefix(parts[0], "t=") {
        return false
    }
    ts, err := strconv.ParseInt(parts[0][2:], 10, 64)
    if err != nil {
        return false
    }
    if int64(math.Abs(float64(time.Now().Unix()-ts))) > toleranceSec {
        return false
    }
    key, err := base64.StdEncoding.DecodeString(secret)
    if err != nil {
        return false
    }
    msg := fmt.Sprintf("%d.", ts)
    mac := hmac.New(sha256.New, key)
    mac.Write([]byte(msg))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    for _, part := range parts[1:] {
        if strings.HasPrefix(part, "v1=") {
            if hmac.Equal([]byte(part[3:]), []byte(expected)) {
                return true
            }
        }
    }
    return false
}

Replay protection

The t= timestamp in the signature header is the wall-clock time when Blumira signed the delivery. This delivery timestamp changes on every retry attempt, unlike event_created_at, which is stable.

We recommend that you reject signatures older than five minutes (|now - t| > 300 seconds). This prevents captured payloads from being replayed against your endpoint later.

Rotating the secret

You can rotate your endpoint’s HMAC signing secret any time. To rotate your endpoint's secret, do the following:

  1. Use the UI method.
  2. Blumira generates a new 32-byte secret and returns it once in the response. Copy and store the HMAC signing secret immediately.

A dual-sign overlap window opens (default is 24 hours, configurable up to 7 days). Blumira sends a webhook.secret.rotated synthetic event to your endpoint. During the overlap window, the event is dual-signed. To verify that your handler has switched to the new secret, send a test delivery after you deploy the new key.

Dual-sign overlap window

During the overlap window, every delivery includes two v1= signatures in the X-Blumira-Signature header, one computed with the new secret, one with the previous secret.

Example:

X-Blumira-Signature: t=1715608951,v1=<new_sig>,v1=<old_sig>

Your verification code, which loops over v1= values and accepts on any match (as shown in the example above), handles rotation transparently. You do not need to change the code.

You can use the following to verify which secret matched, which will indicate whether your handler is still using the old key:

Language Code
Python
import base64, hashlib, hmac
def verify_dual_signed(payload_body: bytes, signature_header: str,
                       new_secret: str, old_secret: str) -> str:
    """Return 'new', 'old', or 'none' indicating which secret verified."""
    parts = signature_header.split(",")
    if not parts[0].startswith("t="):
        return "none"
    timestamp = parts[0][2:]
    signatures = [p[3:] for p in parts[1:] if p.startswith("v1=")]
    for label, secret in [("new", new_secret), ("old", old_secret)]:
        key = base64.b64decode(secret)
        expected = hmac.new(
            key, f"{timestamp}.".encode("utf-8") + payload_body, hashlib.sha256
        ).hexdigest()
        if any(hmac.compare_digest(sig, expected) for sig in signatures):
            return label
    return "none"

 

Node.js
const crypto = require('crypto');
function verifyDualSigned(payloadBody, signatureHeader, newSecret, oldSecret) {
  // Returns 'new', 'old', or 'none'
  const parts = signatureHeader.split(',');
  if (!parts[0].startsWith('t=')) return 'none';
  const timestamp = parts[0].substring(2);
  const signatures = parts.slice(1)
    .filter(p => p.startsWith('v1='))
    .map(p => p.substring(3));
  for (const [label, secret] of [['new', newSecret], ['old', oldSecret]]) {
    const key = Buffer.from(secret, 'base64');
    const expected = crypto.createHmac('sha256', key)
      .update(`${timestamp}.${payloadBody}`).digest('hex');
    const exp = Buffer.from(expected);
    const matched = signatures.some(sig => {
      const buf = Buffer.from(sig);
      return buf.length === exp.length && crypto.timingSafeEqual(buf, exp);
    });
    if (matched) return label;
  }
  return 'none';
}

 

Go
func VerifyDualSigned(body []byte, signatureHeader, newSecret, oldSecret string) string {
// Returns "new", "old", or "none"
parts := strings.Split(signatureHeader, ",")
if !strings.HasPrefix(parts[0], "t=") {
return "none"
}
timestamp := parts[0][2:]
var signatures []string
for _, p := range parts[1:] {
if strings.HasPrefix(p, "v1=") {
signatures = append(signatures, p[3:])
}
}
for _, pair := range [][2]string{{"new", newSecret}, {"old", oldSecret}} {
label, secret := pair[0], pair[1]
key, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
continue
}
mac := hmac.New(sha256.New, key)
mac.Write([]byte(timestamp + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
for _, sig := range signatures {
if hmac.Equal([]byte(sig), []byte(expected)) {
return label
}
}
}
return "none"
}
Tip: Use the standard verifyWebhook function when you need to verify a single secret. Use one of the dual-verify methods above if you need to log which secret matched or gate deployment automation until you confirm the new secret is active.

After the overlap window expires, Blumira will only use the new secret for signing.

Rotation acknowledgment

When you successfully receive and verify the webhook.secret.rotated delivery, Blumira records the acknowledgement. If the synthetic delivery fails, ensure that you deployed the new secret. If you deployed the secret, you can trigger acknowledgment manually by sending a test delivery.

Caution: We do not recommend rotating again before the previous overlap window expires. A second rotation shortens the effective dual-sign window for the prior key. Users still verifying with the key from two rotations ago will start failing immediately. Complete the current rotation and confirm that your handler accepted the new secret before you rotate again.

Custom headers and authorization headers

You can configure additional headers to send with every delivery:

  • Custom headers: Arbitrary key-value pairs (e.g., X-API-Key: your-api-key). Blumira stores header values in GCP Secret Manager and never exposes them via the API. Only header names appear on GET responses.
  • Auth header: An optional Authorization header value, which is useful if your endpoint requires bearer token authentication.
Note: Blumira merges custom headers on updates, adding or overwriting present keys and keeping absent keys. To remove specific headers, use the remove_custom_headers field.

Delivery and retry behavior

Delivery statuses

See the table below for information about different delivery statuses.

Status Meaning
pending Queued for delivery (or scheduled for retry).
in_flight Currently being dispatched.
succeeded Your endpoint returned 2xx.
failed Permanently failed (non-retryable error, e.g., 4xx other than 408 or 429).
dead_lettered All 5 retry attempts were exhausted without success.

Retry schedule

Retryable conditions include timeouts, connection errors, DNS failures, TLS errors, HTTP 408, 429, and 5xx responses. Non-retryable conditions include HTTP 3xx (Blumira does not follow redirects) and 4xx responses other than 408 or 429 (these indicate a permanent configuration issue on your side).

If your endpoint returns a non-2xx response or the connection fails, Blumira retries the delivery according to this schedule:

Attempt Wait after previous attempt
1 Immediate
2 +30 seconds
3 +2 minutes
4 +10 minutes
5 +45 minutes
Dead-lettered

The waits are cumulative, so the fifth and final attempt is approximately 1 hour after the first. After 5 failed attempts, the delivery is dead-lettered, meaning it has permanently failed for this endpoint.

Auto-disable

Blumira automatically disables endpoints that experience sustained delivery failures. This protects both your systems and the delivery pipeline. We auto-disable endpoints when any of the following conditions are met:

  • 20 consecutive failures. The failure counter increments by 1 per failure and heals by 2 per success, so transient issues recover naturally. Only sustained failure reaches the threshold.
  • 80% failure rate over 1 hour (minimum 10 delivery attempts in the window).
  • 100% failure rate over 24 hours (minimum 3 delivery attempts in the window).

When Blumira auto-disables an endpoint, you receive an email notification with the reason. The endpoint stops receiving new deliveries until you re-enable it.

Note: Test deliveries (webhook.test) and rotation synthetics (webhook.secret.rotated) do not affect auto-disable counters. Test deliveries bypass all aggregate health tracking.

Re-enabling a disabled endpoint

To re-enable a disabled endpoint, do the following:

  1. Verify and fix the issue with your endpoint.
  2. Navigate to Settings > Webhooks.
  3. In the webhook’s row, click the ellipsis.
  4. In the context menu, click Edit.
  5. In the Edit webhook window, click the slider next to Enabled.

The endpoint enters a verification window called probation; it must successfully receive 3 consecutive real-event deliveries to fully clear probation. During probation, a single terminal failure (non-2xx on a real event) immediately disables the endpoint again.

Note: This probation period prevents re-enabled endpoints from immediately flooding a still-broken handler.

Idempotency

Deduplication

The envelope id field (evt_<UUID>) is the idempotency key. Always deduplicate on id. The same idis stable across all retry attempts of a single delivery and appears on deliveries to different endpoints for the same source event (cross-endpoint fan-out).

Your handler should maintain a seen-IDs cache and skip processing if the id has already been handled. A 24-hour cache window is typically sufficient.

Cross-endpoint fan-out

When a single source event matches multiple endpoints (for example, both an MSP-tier and a single-org endpoint), each endpoint receives its own delivery with the following:

  • The same envelope id
  • Different X-Blumira-Delivery-Id values
  • Different HMAC signatures (each endpoint has its own secret)

This is intentional fan-out. If you want cross-endpoint deduplication, hash by id and keep whichever delivery arrived first.

Ordering and staleness

Deliveries do not always arrive in source-event order. Retries, multi-pod processing, and per-endpoint retry chains can cause a later event to arrive sooner than an earlier event.

To keep your data organized, we recommend that you do the following:

  • Order by event_created_at, not arrival order. This is the upstream timestamp of when the event actually occurred.
  • Deduplicate on id to handle retries delivering the same event multiple times.
  • Always re-fetch the current state via the Blumira API if your automation needs the latest state. Webhook payloads are frozen at their enqueue time, so they reflect the state when the event occurred, not the current state.

Example: See below for safe state-machine handling.

def on_webhook(event):
    last_seen = state.get(event["data"]["case_id"], "1970-01-01T00:00:00Z")
    if event["event_created_at"] <= last_seen:
        return  # older event arrived late; skip
    state[event["data"]["case_id"]] = event["event_created_at"]
    process(event)

Delivery history

You can inspect delivery history for an endpoint using Blumira's UI. Navigate to the webhook’s detail page to see a list of recent deliveries with status, event type, attempt count, and timestamps.

You can expand each delivery to show individual attempts with response codes and timing.

Quotas and limits

See the table below for quota and limit information.

Resource Limit
Endpoints per organization 20
MSP-tier endpoints per MSP parent 20
Filters per endpoint 10
Custom headers per endpoint 16
Test deliveries per endpoint 10/hour

Troubleshooting

Endpoint is not receiving events

If your endpoint is not receiving events, do the following: 

  1. Verify that the endpoint is enabled. Blumira might have auto-disabled it. 
  2. Navigate to Settings > Webhooks (or MSP Portal > Webhooks for MSP-tier endpoints), and check the endpoint status.
  3. Verify your filters. If you have filters configured, Blumira only delivers an event if it matches a filter. Verify that you have a filter row for each event type and the matching dimensions you want delivered. An endpoint with no filters receives all events. If you expected to be filtering and are not, you might have removed all filters.
  4. Verify that your URL is reachable.

    Note: You must use HTTPS and a public IP. Blumira rejects private and internal URLs.
  5. Send a test delivery to verify connectivity and signature verification.

Deliveries are failing with 4xx errors

See the meaning of your error code, and follow the suggested action:

  • 403 or 401: Your endpoint is rejecting the request. Verify that any auth tokens configured as custom headers are correct.
  • 400: Your endpoint is rejecting the payload. Verify that your handler accepts POST with Content-Type: application/json.
Note: Non-retryable 4xx responses (except 408 and 429) cause immediate permanent failure. Blumira does not retry them.

Signature verification fails

If your signature verification fails, do the following:

  1. Verify that you are using the raw request body bytes and not parsed-then-re-serialized JSON.
  2. Verify that your secret is the correct current secret (or previous secret if within the dual-sign window).
  3. If you use replay protection, verify that your server clock is within tolerance.

Endpoint is auto-disabled

See the explanations of each auto_disable_reason field below:

  • consecutive_failures: 20 or more consecutive failed deliveries.
  • failure_rate_1h: 80% or higher failure rate over 1 hour.
  • failure_rate_24h: 100% failure rate over 24 hours.

Fix the underlying issue, and then re-enable the endpoint. The endpoint enters a 3-delivery verification window.