Webhooks
Field-level reference for webhook event types, the delivery envelope, Standard Webhooks signing, delivery and retry behavior, and delivery records.
A webhook pushes a namespace's events to your own systems as they happen, instead of you polling the logs: alert the team when a rule blocks a build, stream audit events into a SIEM, diff configuration changes against the git copy, or track successful publishes in an inventory.
A webhook subscribes an HTTPS endpoint to the event types you choose: firewall log decisions, audit events, configuration changes, and successful publishes. Every delivery is signed so the receiver can verify it, and failed deliveries are retried with at-least-once delivery from buffering to retry exhaustion.
This page is the exhaustive reference for what a receiver sees on the wire and how delivery behaves; for setup, see Receive webhook events. Management endpoints and permissions: API.
Event types
| Type | Fires when |
|---|---|
firewall.blocked | A firewall rule blocked a download or publish |
firewall.exception-allowed | A rule matched but an exception let the request through |
firewall.allowed | A log-only rule matched an allowed request |
audit.<action> | The audit event with that action was recorded, e.g. audit.firewall.rule.upsert; one type per audit action |
config.firewall.created, config.firewall.updated, config.firewall.deleted | A firewall was created, changed (config, rule, exception, credentials, or rollback; one event per change), or deleted. Carries the complete config after the change |
config.webhook.created, config.webhook.updated, config.webhook.deleted | A webhook was created, changed (including secret rotation and quarantine release), or deleted. Signing secrets are never included |
package.published | A package version passed the upload rules and was forwarded to the publish-target upstream (npm, Maven, PyPI, NuGet) |
package.unlisted | A NuGet package version was unlisted (DELETE) on the publish-target upstream |
vulnerability.* | Reserved, not emitted yet (shown as coming soon and disabled in the dashboard picker) |
webhook.rate_limited | System event: the webhook's buffer overflowed (reason: "buffer_overflow", with droppedEvents) or deliveries are being paced against a backlog (reason: "rate_limited") |
webhook.test | System event: a test send |
A subscription lists the event types to receive, either as exact types (firewall.blocked) or as a prefix with a trailing .* (firewall.*, audit.*, audit.team.*, config.*, package.*):
- A bare
*is rejected; name the families you want. - Exact
firewall.*,config.*, andpackage.*names must be one of the types in the table above. Exactaudit.andvulnerability.names are not checked against a list, so a subscription to a specific action keeps working when new actions are added. webhook.*types cannot be subscribed to: the system events are always delivered and cannot be turned off.- Firewall events come from
downloadanduploaddecisions only. Version filtering on a metadata request never produces events, however many versions it removes. A rule on theversionsexecution phase that blocks an artifact fetch counts as adownloaddecision and is delivered.
audit.* and config.* deliberately overlap: the audit event is the security log (successes and failures, thin metadata), the config event is the automation feed (successes only, carrying the full resource state after the change).
Firewall scope
A webhook can optionally restrict the data-plane events, firewall.* and package.*, the only types that name a firewall, to a selection of the namespace's firewalls. An empty selection means every firewall. audit.*, config.*, and webhook.* events are never filtered by the selection. Selecting an unknown firewall is rejected at create/update; a firewall deleted after being selected simply stops matching and does not block later edits.
Envelope
Every delivery is one JSON object with a batch of events, oldest first, up to 100 events or 512 KB per delivery:
{
"version": 1,
"events": [
{
"id": "evt-01JZX2M8Q0V5T2C7N9R4W6YB3D",
"type": "firewall.blocked",
"time": "2026-07-13T09:30:00Z",
"namespace": "ns-01KYA296JWEE36NDNRTJ4KM82Q",
"data": {
"firewall": "3e7f5cb806f26e3beae2",
"executionPhase": "download",
"blocked": true,
"ecosystem": "npm",
"packageName": "left-pad",
"version": "1.3.0",
"rule": "log-install-scripts-download",
"message": "Install scripts detected: postinstall"
}
}
]
}Event ids are stable: a retried delivery carries the same events with the same ids. Every payload shape is described by the published JSON Schema.
Event data
data holds the per-type payload, described below per event family. Fields marked "when set" are omitted when empty; every other field is always present (blocked is sent even when false).
firewall.*
One event per download or upload decision, carrying the fields of the firewall log entry, which documents each in detail:
| Field | Meaning |
|---|---|
firewall, firewallVersion | Firewall that made the decision, and the config version it evaluated under |
executionPhase | download or upload. The phase is a field, not a separate event type |
blocked | Whether the request was blocked |
ecosystem, packageName, version | The package the decision was about |
upstream | Upstream that served or would have served the package |
rule | Rule that matched |
user | Who made the request, as an opaque user ID |
request | Request ID, also shown in the client's blocked-install error message |
message | Rule description, or rule-specific message such as Malware detected in npm/example@1.0.0: ... |
sourceFirewall, sourceFirewallVersion | When set: the firewall the matched rule was inherited from, and its config version. Absent when the rule is the firewall's own |
exception, exceptionSourceFirewall, exceptionSourceFirewallVersion | When set, on firewall.exception-allowed: the exception that let the request through, and where it was inherited from |
published | When set: the version's publish date upstream |
output | When set: rule-specific detail, such as vulnerability IDs and scores or a malware finding's summary, scanner, confidence, and signals |
interaction | When set: interaction ID from the token suffix |
userAgent, remoteAddress | When set: client metadata |
audit.*
The fields of the audit event, which documents each in detail:
| Field | Meaning |
|---|---|
action | What was done, one of the audit actions |
outcome, statusCode | success or failure, and the HTTP status returned |
resourceType, resourceId | When set: the resource the action targeted |
actorUserId, actorTokenIdentifier | When set: acting user's ID, and a hash-derived identifier of the acting token (never token material) |
requestId, interactionId | When set: request ID, shared with firewall log entries from the same request, and interaction ID from the token suffix |
remoteAddr, userAgent, requestMethod, requestPath | When set: request metadata |
errorMessage | When set: short failure reason |
eventData | When set: action-specific detail, for example the missing permission on a denial |
config.firewall.*
| Field | Meaning |
|---|---|
firewall | The firewall that changed |
version | Config version after the change |
ecosystem | The firewall's ecosystem |
actor | Who made the change, see the actor object |
config | The complete firewall configuration after the change. config.firewall.deleted carries the last known config |
Upstream credentials inside config are always the redaction placeholder, never credential material.
config.webhook.*
| Field | Meaning |
|---|---|
webhook | The webhook that changed |
actor | Who made the change, see the actor object |
config | The webhook's configuration: id, name, url, enabled state, event types, firewall scope, secret-rotation and quarantine timestamps. Signing secrets have no field in this shape at all |
package.*
| Field | Meaning |
|---|---|
firewall | Firewall the publish went through |
ecosystem, packageName, version | The published package |
actor | Who published, see the actor object |
upstream | When set: the publish-target upstream the upload was forwarded to |
file | When set: the upload request path, for ecosystems that publish one file per request |
Maven and PyPI upload one file per request, so one release can emit several events; deduplicate on ecosystem, packageName, and version.
The actor object
config.* and package.* events carry an actor object identifying who caused the change. Every field is omitted when empty:
| Field | Meaning |
|---|---|
userId | Acting user's ID |
tokenIdentifier | Hash-derived identifier of the acting token, never token material |
requestId | Request ID of the request that caused the change |
remoteAddress, userAgent | Client metadata |
System events
webhook.rate_limited reports delivery pressure on the webhook itself:
| Field | Meaning |
|---|---|
webhook | The affected webhook |
reason | buffer_overflow: events were dropped and there is a gap in the stream. rate_limited: deliveries are paced against a backlog of more than 1000 buffered events, nothing lost yet |
droppedEvents | Events discarded at the buffer cap; they will never arrive |
bufferedEvents | Events waiting for delivery |
webhook.test carries the webhook id and a fixed message ("Test event").
JSON Schema
GET /schemas/webhook-events-v1.json on the dashboard origin returns a JSON Schema describing the envelope and every event payload, generated from the same types that render the events. It is a public contract document, served without authentication; use it to generate types or validate deliveries in the receiver.
Signing
Deliveries are signed per the Standard Webhooks convention. Three headers on every POST:
| Header | Value |
|---|---|
webhook-id | The delivery ID (whdel-...), stable across retries of the same batch; use it as the idempotency key |
webhook-timestamp | Unix time in seconds at the attempt |
webhook-signature | One or more space-separated v1,<base64> values |
Each value in webhook-signature is an HMAC over the delivery: proof that it was produced by someone holding the webhook's signing secret and that the body was not modified in transit. The delivery id and timestamp are signed together with the body, so a captured signature cannot be reused for a different delivery, and the timestamp gives the receiver a way to reject stale deliveries.
The signature is computed as:
base64( HMAC-SHA256( key, "{webhook-id}.{webhook-timestamp}.{body}" ) )key is the binary HMAC key inside the signing secret: the secret is whsec_ followed by the base64-encoded key. To verify a delivery:
- Base64-decode the signing secret after the
whsec_prefix; the result is the HMAC key. - Join the
webhook-idheader, thewebhook-timestampheader, and the request body with dots. Use the raw body bytes as received, before any JSON parsing. - Compute HMAC-SHA256 over that string with the key, and base64-encode the result.
- Compare it in constant time against each space-separated
v1,value; the delivery is authentic if any signature matches.
Standard Webhooks libraries implement exactly this, so a library call with the secret and the headers is usually all a receiver needs. The secret is returned on create and managers can reveal or rotate it later; see Receive webhook events.
For 24 hours after a secret rotation, deliveries carry two signatures, new and previous secret, so receivers on either secret keep verifying. Requests are sent with content-type: application/json and user agent bytesafe-d3-webhooks/1.
Delivery
| Behavior | Value |
|---|---|
| Success | Any 2xx response. Redirects are not followed and count as failures |
| Attempt timeout | 10 seconds |
| Retry schedule | 30s, 2m, 10m, 30m, 1h after the immediate attempt (6 attempts total) |
Retry-After | Honored on 429 and 503, capped at 1 hour |
| Ordering | Per webhook, oldest first, one delivery in flight at a time |
| Pacing | At most 5 deliveries per second per webhook |
| Batch size | Up to 100 events or 512 KB per delivery |
| Buffer | 5000 events per webhook; on overflow the oldest are dropped and a webhook.rate_limited event with reason: "buffer_overflow" reports the gap |
| Guarantee | At-least-once from buffering to retry exhaustion; deduplicate on webhook-id |
Quarantined webhooks
When a batch exhausts all retry attempts, it is recorded as failed and dropped; that batch is not delivered again. The webhook itself is quarantined for 24 hours.
During quarantine, no deliveries are attempted, but new events keep buffering up to the buffer cap. When the quarantine expires, delivery resumes automatically with the buffered backlog.
To resume earlier, release the quarantine manually: the dashboard shows the quarantine reason and a Release now action on the webhook, and the API equivalent is POST .../webhooks/{webhook}/unquarantine.
Quarantine is a state of the webhook's deliveries, not of any package: the firewall never holds a package back for review.
Delivery records
The last 128 deliveries per webhook are kept as a ring buffer for debugging, each with the signed payload and the endpoint's response:
| Field | Description |
|---|---|
id | Delivery ID, equals the webhook-id header |
test | true for test sends |
status | retrying, success, or failed (the dashboard shows success as "delivered") |
attempts | Attempts made so far |
eventCount, eventTypes | Batch contents summary |
payload, payloadTruncated | The delivered body, stored up to 64 KB |
responseStatus | HTTP status of the last attempt; 0 means no response (network error, timeout) |
responseBody | Endpoint response, stored up to 4 KB |
error | Last transport error, when any |
durationMillis | Last attempt duration |
createdAt, completedAt | First attempt and final outcome times |
Metrics
Delivery metrics answer whether a receiver is keeping up: how many deliveries succeed and fail, how many events get through, and whether any are lost. Rising deliveriesFailed or any eventsDropped is the signal to check the endpoint and the delivery records.
GET .../webhooks/{webhook}/metrics?periodMinutes=... returns them per webhook; the dashboard's Metrics tab charts the same data. periodMinutes defaults to 1440 (24 hours) and caps at 10080 (7 days). Buckets are per minute; unlike firewall metrics, longer periods have no coarser rollup.
The response carries a buckets array and a totals object summing the same counters over the whole period:
| Field | Meaning |
|---|---|
timestamp | Bucket start. Buckets only |
deliveriesSuccess | Delivery attempts answered with 2xx |
deliveriesFailed | Failed delivery attempts, including retries of the same batch |
eventsSent | Events contained in successful deliveries |
eventsDropped | Events lost: dropped at the buffer cap, or in a batch that exhausted its retries |
avgDeliveryMillis | Average attempt duration in the bucket. Buckets only, not in totals |
Endpoint requirements
The endpoint URL must be http or https with a hostname that is not on the firewall's rejected-domain list, and it must resolve to a public address: requests to private, loopback, and link-local addresses are refused. The delivery client never follows redirects.
Related
- How-to: Receive webhook events
- Reference: API, Logs, Audit log, Permissions
- Concept: Observability