Skip to main content
Version: v5.7.0

Inbound webhook connection

Included from Basic tier

An instance-level connection object: the signed endpoint (POST /api/hooks/<code>) an external system fires OpsChain triggers through. The admin surface is InboundWebhookService (API v2, PostgreSQL-backed), gated on the /admin updatable determination; the CLI is opschain admin inbound-webhooks and the GUI page is Administration → Inbound webhooks (the secret is typed or generated in the browser and shown exactly once; reads serve only secret_set). The hook endpoint itself is unauthenticated by design except the HMAC signature — the signature is the authentication — and every firing a delivery causes executes as the connection's service_account, never as the matching trigger's owner.

Data dictionary​

FieldMeaning
idThe row id (UUID).
codeThe estate code (1-64 of a-z0-9_-, immutable): the /api/hooks/<code> address, and the data.connection value on every event the connection lands.
display_nameHuman-readable name.
service_accountThe username every trigger firing this connection causes executes — and is authorization-checked — as. A missing, disabled or unauthorised account settles firings failed by name; it never falls back to the trigger owner.
replay_window_secondsThe replay window (default 300, range 30-86400): a signed X-OpsChain-Timestamp outside it refuses stale_timestamp, and delivery ids dedup exactly-once (the ledger keeps ids far beyond the window).
rate_per_minuteThe per-connection request budget; requests beyond it answer 429 before any verification or refusal event is spent. 1-6000 is this connection's own budget; 0 (the create default) inherits the estate default webhooks.default_rate_per_minute, itself unset = 60; -1 is unlimited at this connection even when the estate default is bounded; anything else is refused.
secretWrite-only (create/update requests only, at least 16 characters): the HMAC-SHA256 signing secret, encrypted at rest and never echoed by any read, audit row or error.
secret_setWhether a signing secret is stored.
enabledWhether the endpoint accepts deliveries; a disabled connection refuses 403 connection_disabled, loudly.
hook_pathThe endpoint path deliveries POST to: /api/hooks/<code>.
git_remoteThe Git remote every verified delivery to this connection hints, as <project code>/<remote name> (must name an existing remote; "" on update clears it; unset names none): the remote is fetched now and the info:git_remote:ref_moved events that fetch records are what triggers match — the payload's own SHA is never believed. A GitHub push delivery additionally hints every active remote on the repository the payload names, whether or not this is set. See opschain docs resources/git_remote ("Ref-move events").
created_at / updated_atTimestamps.

The delivery wire contract​

A delivery is one POST /api/hooks/<code> with a JSON body of at most 1 MiB, signed one of two ways:

  • GitHub shape — X-Hub-Signature-256: sha256=<hex>, HMAC-SHA256 over the raw body; X-GitHub-Delivery is the dedup id and X-GitHub-Event the envelope event type. Provider webhooks configured with the connection's secret work unchanged.
  • Generic shape — X-OpsChain-Signature: v1=<hex>, HMAC-SHA256 over <timestamp>.<body> with X-OpsChain-Timestamp (unix seconds, enforced against the replay window) — the exact signature OpsChain's own outbound notification webhooks send. X-OpsChain-Delivery is the dedup id, X-OpsChain-Event the envelope event type.

Without a delivery-id header, the body's SHA-256 digest dedups. An accepted delivery answers 202 with delivery_id and event_id; every named refusal answers its typed 4xx (401 missing_signature | missing_timestamp | stale_timestamp | bad_signature, 403 connection_disabled | no_signing_secret, 404 unknown_connection, 409 replayed_delivery, 400 payload_not_json, 413 payload_too_large, 429 rate_limited) and — rate limiting aside — a warn:webhooks:refused event. The accepted delivery's event is external:webhook:received; triggers bind to it as opschain docs triggers describes.