Skip to main content
For AI agents: a documentation index is available at https://docs.coverbase.com/llms.txt — this page is also available in markdown by appending .md to the URL.
Webhooks are the outbound half of an integration. This page documents how events are delivered, how to verify signatures, retry behavior, and the event catalog. To register, list, update, delete, test, or inspect the delivery history of a subscription, see the Webhooks API. For the audit trail of every attempt, see Webhook delivery history.

When deliveries happen

When a subscribed event occurs, Coverbase POSTs a JSON envelope to every active webhook subscribed to that event type. Deliveries originate from three sources, and all of them are recorded in delivery history:
  • Domain-event fan-out — a domain event (e.g. a vendor is created) fans out to every subscription for that event type.
  • Workflow send_webhook action — a workflow automation explicitly delivers its triggering event. See Workflow engine → Actions.
  • Manual test — the test endpoint sends a one-off delivery to a single subscription.
For one source event, the same event_id is shared across all webhooks in the fan-out. Receivers can use it to dedupe a fan-out and to correlate a multi-subscription delivery.

Delivery format

A delivery is an HTTP POST with a JSON body in this exact envelope:
{
  "event_id": "cbevt_8b3c1a4d9e7f2c5b6a8d1e3f9c7b4a2e",
  "event_type": "Vendor.Created",
  "occurred_at": 1747400000,
  "data": { "event_type": "Vendor.Created", "vendor_id": "cbvndr_e448ba62882143f3ba0c140bb2e30162" }
}
FieldTypeNotes
event_idstringShared delivery/event identifier (cbevt_...). The same value for every webhook in one fan-out.
event_typestringThe event type string (see the event catalog).
occurred_atintegerUnix timestamp in seconds, generated at send time.
dataobjectEvent-specific payload. For real events these are the catalog fields for that event type (and data echoes event_type). For a manual test, data is your custom payload (default {"test": true}).
The envelope is exactly these four keys — there is no top-level workflow_run_id or any other field.

Request headers

HeaderValue
Content-Typeapplication/json
Coverbase-SignatureLowercase hex HMAC-SHA256 of the raw request body, keyed by your webhook secret.
X-Coverbase-Event-IdSame value as event_id.
X-Coverbase-Event-TypeSame value as event_type.

Signature verification

The signature is HMAC-SHA256(secret, raw_request_body) rendered as a lowercase hex string, sent in the Coverbase-Signature header. The key is the secret you registered with the subscription. Verify it against the raw bytes of the request body, before parsing JSON, with a constant-time comparison.
Python
import hashlib
import hmac

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# Flask example
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_a1b2c3d4e5f6a7b8c9d0e1f2"  # the secret you registered

@app.post("/coverbase")
def receive():
    raw = request.get_data()  # raw bytes, do not re-serialize
    sig = request.headers.get("Coverbase-Signature", "")
    if not verify(raw, sig, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # dedupe on request.headers["X-Coverbase-Event-Id"] before handling
    return "", 200
Node.js
const crypto = require("crypto");

function verify(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express example (use express.raw so rawBody is the unparsed buffer)
app.post(
  "/coverbase",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("Coverbase-Signature") || "";
    if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
      return res.status(401).end();
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // dedupe on req.header("X-Coverbase-Event-Id") before handling
    res.status(200).end();
  },
);

Delivery & retries

PropertyValue
HTTP methodPOST
Connection timeout10 seconds per attempt
RetriesUp to 5 attempts on failure (timeout or connection error)
Retry backoffNone — Hatchet retries immediately; no exponential backoff, no jitter
OrderingNot guaranteed — deliveries are unordered
GuaranteeAt-least-once — the same event_id may be delivered more than once
Endpoint requirementPublic HTTPS endpoint (see URL restrictions)
occurred_at and event_id are generated at send time. Make your receiver idempotent: dedupe on X-Coverbase-Event-Id and process the same event id at most once. Respond with a 2xx quickly to acknowledge; non-2xx, timeouts, and connection errors are retried up to 5 times in rapid succession.

Source IP

Coverbase does not publish a fixed egress IP range for outbound webhook deliveries. Authenticate the sender by HMAC signature verification (see Signature verification) — not by source IP. The compute fleet is autoscaled and the public IPs that originate deliveries change frequently. If you operate behind a network perimeter that requires a static allow-list, route deliveries through a customer-controlled HTTPS proxy with a stable egress IP and forward to the real receiver after re-verifying the signature.

Delivery history retention

Every delivery attempt is recorded in delivery history for 90 days. Older rows are deleted by a daily background job; the API still serves the window inside retention via GET /v1/webhooks/{id}/deliveries. Capture deliveries you need beyond that window from your receiver side at delivery time.

URL restrictions

Webhook URLs must be public HTTPS endpoints. A URL is rejected at create/update time (400 invalid_url) if it is not HTTPS, carries credentials, uses a non-standard port, or resolves to a private, loopback, link-local, or metadata address (e.g. 169.254.169.254). The same check is re-applied with DNS resolution at delivery time, so a hostname that is repointed to an internal address after registration (DNS rebinding) is still blocked. A delivery blocked this way is recorded with status error in delivery history.

Event catalog

These are the only values valid in a subscription’s events array and the only event types emitted. The events field is strictly validated against this set — see the behavior change below. Use the exact, case-sensitive strings.
event_typedata fields
Assessment.Createdassessment_id
Assessment.AssigneeChangedassessment_id, old_assignee_id (nullable), new_assignee_id (nullable)
Assessment.StatusChangedassessment_id, old_status, new_status
QuestionnaireResponse.Createdquestionnaire_response_id
QuestionnaireResponse.StatusChangedquestionnaire_response_id, old_status, new_status
Review.StatusChangedreview_id, old_status_id (nullable), new_status_id (nullable)
WorkQueueItem.Createdwork_queue_item_id
WorkQueueItem.Completedwork_queue_item_id
Vendor.Createdvendor_id
Vendor.Updatedvendor_id
Procurement.Createdprocurement_id
Procurement.StatusChangedprocurement_id, old_status_id (nullable), new_status_id
Service.Createdservice_id
Service.Updatedservice_id
OrgIntake.QuestionnaireRemovedquestionnaire_id, intake_disabled (bool)
OrgIntake.Disabledremoved_questionnaire_id
IntakeSession.Createdintake_session_id
IntakeSession.Submittedintake_session_id
RadarEvent.Createdradar_event_id
RadarEvent.Updatedradar_event_id, field_diffs
RadarEvent.Deletedradar_event_id
RadarDetectorResult.Createdradar_detector_result_id
RadarDetectorResult.Updatedradar_detector_result_id, field_diffs
RadarDetectorResult.Deletedradar_detector_result_id
Reassessment.Createdreassessment_id
Reassessment.Updatedreassessment_id, field_diffs
Reassessment.Deletedreassessment_id
A radar alert is a RadarDetectorResult — subscribe to RadarDetectorResult.Created to be notified of new alerts and RadarDetectorResult.Updated (whose field_diffs will include is_dismissed) for triage activity. *.Updated events carry a field_diffs object mapping each changed field to its { "old_value", "new_value" }. A soft-archive (is_archivedtrue) surfaces as *.Deleted, not *.Updated. See the Radar and Reassessments APIs.
All IDs are opaque prefixed strings (e.g. cbvndr_..., cbqsrw_...); see API conventions → IDs. Every event’s data also echoes its own event_type.
webhook.test is a valid value only for the test endpoint’s event_type field. It is not a subscribable event type — it will be rejected if used in a subscription’s events array.

Strict event-type validation

Breaking change. events is now strictly validated against the catalog above. Previously any free-form string was accepted. A POST /v1/webhooks or PATCH /v1/webhooks/{id} whose events contains a value not in the catalog now fails with 422 invalid_event_types. Integrations that registered non-canonical strings (e.g. lowercase vendor.created) must switch to the exact catalog value (e.g. Vendor.Created).