BytesafeDependency Firewall

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

TypeFires when
firewall.blockedA firewall rule blocked a download or publish
firewall.exception-allowedA rule matched but an exception let the request through
firewall.allowedA 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.deletedA 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.deletedA webhook was created, changed (including secret rotation and quarantine release), or deleted. Signing secrets are never included
package.publishedA package version passed the upload rules and was forwarded to the publish-target upstream (npm, Maven, PyPI, NuGet)
package.unlistedA 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_limitedSystem event: the webhook's buffer overflowed (reason: "buffer_overflow", with droppedEvents) or deliveries are being paced against a backlog (reason: "rate_limited")
webhook.testSystem 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.*, and package.* names must be one of the types in the table above. Exact audit. and vulnerability. 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 download and upload decisions only. Version filtering on a metadata request never produces events, however many versions it removes. A rule on the versions execution phase that blocks an artifact fetch counts as a download decision 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:

FieldMeaning
firewall, firewallVersionFirewall that made the decision, and the config version it evaluated under
executionPhasedownload or upload. The phase is a field, not a separate event type
blockedWhether the request was blocked
ecosystem, packageName, versionThe package the decision was about
upstreamUpstream that served or would have served the package
ruleRule that matched
userWho made the request, as an opaque user ID
requestRequest ID, also shown in the client's blocked-install error message
messageRule description, or rule-specific message such as Malware detected in npm/example@1.0.0: ...
sourceFirewall, sourceFirewallVersionWhen set: the firewall the matched rule was inherited from, and its config version. Absent when the rule is the firewall's own
exception, exceptionSourceFirewall, exceptionSourceFirewallVersionWhen set, on firewall.exception-allowed: the exception that let the request through, and where it was inherited from
publishedWhen set: the version's publish date upstream
outputWhen set: rule-specific detail, such as vulnerability IDs and scores or a malware finding's summary, scanner, confidence, and signals
interactionWhen set: interaction ID from the token suffix
userAgent, remoteAddressWhen set: client metadata

audit.*

The fields of the audit event, which documents each in detail:

FieldMeaning
actionWhat was done, one of the audit actions
outcome, statusCodesuccess or failure, and the HTTP status returned
resourceType, resourceIdWhen set: the resource the action targeted
actorUserId, actorTokenIdentifierWhen set: acting user's ID, and a hash-derived identifier of the acting token (never token material)
requestId, interactionIdWhen set: request ID, shared with firewall log entries from the same request, and interaction ID from the token suffix
remoteAddr, userAgent, requestMethod, requestPathWhen set: request metadata
errorMessageWhen set: short failure reason
eventDataWhen set: action-specific detail, for example the missing permission on a denial

config.firewall.*

FieldMeaning
firewallThe firewall that changed
versionConfig version after the change
ecosystemThe firewall's ecosystem
actorWho made the change, see the actor object
configThe 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.*

FieldMeaning
webhookThe webhook that changed
actorWho made the change, see the actor object
configThe 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.*

FieldMeaning
firewallFirewall the publish went through
ecosystem, packageName, versionThe published package
actorWho published, see the actor object
upstreamWhen set: the publish-target upstream the upload was forwarded to
fileWhen 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:

FieldMeaning
userIdActing user's ID
tokenIdentifierHash-derived identifier of the acting token, never token material
requestIdRequest ID of the request that caused the change
remoteAddress, userAgentClient metadata

System events

webhook.rate_limited reports delivery pressure on the webhook itself:

FieldMeaning
webhookThe affected webhook
reasonbuffer_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
droppedEventsEvents discarded at the buffer cap; they will never arrive
bufferedEventsEvents 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:

HeaderValue
webhook-idThe delivery ID (whdel-...), stable across retries of the same batch; use it as the idempotency key
webhook-timestampUnix time in seconds at the attempt
webhook-signatureOne 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:

  1. Base64-decode the signing secret after the whsec_ prefix; the result is the HMAC key.
  2. Join the webhook-id header, the webhook-timestamp header, and the request body with dots. Use the raw body bytes as received, before any JSON parsing.
  3. Compute HMAC-SHA256 over that string with the key, and base64-encode the result.
  4. 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

BehaviorValue
SuccessAny 2xx response. Redirects are not followed and count as failures
Attempt timeout10 seconds
Retry schedule30s, 2m, 10m, 30m, 1h after the immediate attempt (6 attempts total)
Retry-AfterHonored on 429 and 503, capped at 1 hour
OrderingPer webhook, oldest first, one delivery in flight at a time
PacingAt most 5 deliveries per second per webhook
Batch sizeUp to 100 events or 512 KB per delivery
Buffer5000 events per webhook; on overflow the oldest are dropped and a webhook.rate_limited event with reason: "buffer_overflow" reports the gap
GuaranteeAt-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:

FieldDescription
idDelivery ID, equals the webhook-id header
testtrue for test sends
statusretrying, success, or failed (the dashboard shows success as "delivered")
attemptsAttempts made so far
eventCount, eventTypesBatch contents summary
payload, payloadTruncatedThe delivered body, stored up to 64 KB
responseStatusHTTP status of the last attempt; 0 means no response (network error, timeout)
responseBodyEndpoint response, stored up to 4 KB
errorLast transport error, when any
durationMillisLast attempt duration
createdAt, completedAtFirst 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:

FieldMeaning
timestampBucket start. Buckets only
deliveriesSuccessDelivery attempts answered with 2xx
deliveriesFailedFailed delivery attempts, including retries of the same batch
eventsSentEvents contained in successful deliveries
eventsDroppedEvents lost: dropped at the buffer cap, or in a batch that exhausted its retries
avgDeliveryMillisAverage 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.

On this page