These endpoints configure where signed vendor evidence comes from and which key verifies it. They do not receive webhooks. Vendors post to the gateway, which verifies original bytes, timestamps, and active key versions against a closed signature registry before anything is admitted (ADR-0021, ADR-0041):
Control accepts only already encrypted signing material. It never holds a signing key in plaintext, never verifies a signature, and neither ingestion nor persistence changes a receipt or an effect conclusion.

Authentication

Authorization: Bearer <session token>, verified as /api-reference/overview describes. Reading subscriptions needs workspace.read; writing and deleting need connection.manage. The write endpoints deliberately do not use the framework’s own validation errors. FastAPI’s 422 body echoes the rejected input, which would reflect encrypted material and URLs back to the caller, so the body is parsed only after authorization and every refusal collapses to one message:

List subscriptions

GET /api/v1/workspaces/{workspace_id}/connections/{connection_id}/webhooks
string
required
Connection to read. 404 connection not found when it is not in this workspace.
array
Public subscription projections. Encrypted credential material is never returned.
string
wsub_-prefixed id.
string
The exact registered HTTPS callback, or null. It is configuration, never derived from Host or forwarded headers.
object
The bundle-declared shape: signature_profile, header, timestamp_tolerance_s, event_types, provider_event_id, and optional upstream_id.
object
Metadata only — {version, not_before, not_after}. The ciphertext, wrapped DEK, and scheme are dropped on the way out.
object
The same metadata for the outgoing key during rotation, or null.

Create or replace a subscription

PUT /api/v1/workspaces/{workspace_id}/connections/{connection_id}/webhooks/{subscription_id} The body is a WebhookSubscription document (packages/proto/webhook_subscription.schema.json), validated with unknown properties closed off.
string
required
Must match the path segment and the ^wsub_ pattern, and must be 1–128 characters of [A-Za-z0-9_.-].
object
required
Must equal the expected spec exactly — the connector bundle’s webhook block for a native connection, or the reviewed profile’s spec for a federated one. signature_profile must exist in the reviewed registry (packages/connectors/catalog/webhook-profiles.json) and its provider must match the connector slug, or the broker for a federated connection. timestamp_tolerance_s must be greater than 0 and at most 900, and event_types must hold 1–32 entries matching [A-Za-z0-9_.:/-]{1,128}.
object
required
{version, credential, not_before, not_after}. version is a 1–128 character safe label, the validity window is greater than zero and at most 30 days, and credential is an EncryptedCredential whose scheme is exactly {"type": "bearer"}, whose last4 is at most 4 characters, and whose ciphertext_b64 and dek_wrapped_b64 decode to 28–16384 bytes.
object
The outgoing key. Its version must differ from the current key, it must start before the current key does, it must not outlive it, and its overlap past the current key’s start is capped at 24 hours.
string
Required for the square_sha256, hubspot_v3, and contentful_sha256 profiles, which sign the destination. Must be HTTPS, at most 2048 characters, with no userinfo, query, or fragment.

Constraints

  • Bodies over 128 KiB are 413 webhook configuration is too large.
  • A connection holds at most 16 subscriptions.
  • A native connection must be obol custody with an open_api target; a federated connection must carry a nango or composio broker and a well-formed upstream connection id.
  • Replacing an existing subscription cannot change its notification_url, redefine retained key versions or their encryption envelopes, move a validity start, or extend a validity end. Rotate with new secret material rather than re-encrypting the same plaintext.
Answers 200 with the connection’s full subscription list, in the same public projection the read returns. A webhook.configure audit event is recorded.
Sealing happens outside this API. The gateway ships an obol-gateway seal-webhook-key command that seals bounded, non-interactive stdin under the gateway KEK without starting a service; its output is what goes in credential. A dashboard secret-entry flow and remote sealing are separate work (ADR-0041).

Delete a subscription

DELETE /api/v1/workspaces/{workspace_id}/connections/{connection_id}/webhooks/{subscription_id} Removes one subscription and answers 200 with the remaining list. Records a webhook.delete audit event.

What ingest does with this

The gateway loads the published subscription from the snapshot, rate-limits known-subscription traffic, caps bodies at 256 KiB, decrypts the signing key in-process, and verifies the signature before parsing. Verified events are appended to a workspace-qualified evidence stream with replay markers; invalid signatures and replay collisions produce bounded telemetry and never evidence or a contradiction. Keys and raw payloads never enter the evidence stream, a receipt, or control-plane memory. Because GitHub authenticates neither its event-family header nor a timestamp, the gateway derives supported families from the signed payload structure, rejects conflicting unsigned family headers, requires a UUID-shaped delivery id, and deduplicates the signed body digest as well as the provider id. That does not prove freshness of a previously unseen payload. See /receipts/webhooks for the evidence model and /receipts/evidence for what each trust class means.

Receipt delivery recovery

Webhook evidence and receipts reach control over a durable at-least-once audit stream. ADR-0042 makes that recovery lossless: pending deliveries are reclaimed in bounded pages from a persisted scan position, each receipt is projected with a single upsert conditioned on a strictly greater revision, and receipt listings page on a versioned opaque cursor over (occurred_at, receipt_id) rather than a timestamp alone. There is no HTTP endpoint for that recovery — it is a worker. What you see of it is the next_cursor and next_before contract on /api-reference/operator and /api-reference/routing.