OpenWOP openwop.dev
Status: Active · RFC details
FieldValue
RFC0221
Titlea webhook secret the host generates is returned once
StatusActive
Author(s)David Tufts (@davidscotttufts)
Created2026-09-27
Updated2026-09-27 — filed and moved Draft → Active in the filing PR. Comment window waived by the steward. STEWARD OVERRIDE of RFC 0147 §A.6, which forbids a bootstrap waiver from shortening the window for RFCs affecting external effects; a webhook subscription's signing secret is part of that surface, as RFC 0201 and RFC 0215 were. Logged in MAINTAINERS.md §"Bootstrap-phase RFC waivers". The evidence gate is not waived.
Affectsspec/v2/core/webhooks.md §Surfaces (the 201 cell and one rule) · api/v2/openapi.yaml registerWebhook (the 201 gains an optional secret; generated by scripts/derive-v2-api.py) · conformance: v2-webhook-generated-secret.test.ts (suite 2.42.8)
Compatibilityadditive — a new optional response member and a host obligation for a request the v2 text left unanswered (COMPATIBILITY.md §4). No request shape, error code or status changes
Supersedes—
Superseded by—

Summary

registerWebhook makes secret optional, and v2 returns only { webhookId }. A client that omits the secret therefore gets a subscription whose deliveries it can never verify: the host signs every delivery with a secret nobody else knows. v1 answered this: the host generates the secret and returns it once at registration (spec/v1/webhooks.md). v2 lost that sentence. This RFC restores it for v2: when the request omits secret, the host MUST generate one and return it as secret in the 201, the only response that carries it. A supplied secret is still never echoed (RFC 0201 §B.6).

Motivation

  • The v2 text had no answer. webhooks.md §Surfaces gave the response as 201 { webhookId }, and the OpenAPI request description says the secret is "never echoed in any response". Read together, a secret-less registration produces an unverifiable subscription, and the text did not say whether that was allowed.
  • Hosts split. openwop-app followed v1 and returned the generated secret once. The v2 reference host generated one and never returned it, so every delivery to a secret-less subscription was unverifiable (openwop-examples #98 fixes it).
  • Found by a reader. Rewriting the quickstarts for v2 (openwop #1654) surfaced it: the guide had to tell readers to always send their own secret, because the spec could not promise they would ever see one otherwise.

Proposal

§A. The generated secret

One rule in spec/v2/core/webhooks.md §Surfaces, with the table cell becoming 201 { webhookId, secret? }:

When registerWebhook omits secret, the host MUST generate one and return it as secret in the 201. That response is the only one that carries it. A supplied secret MUST NOT be echoed.

  • Why return it, not require the client to send one. Making secret REQUIRED would refuse registrations v2 accepts today, which is a breaking change. Returning the generated secret is additive, matches v1 and matches openwop-app.
  • Why only once. There is no v2 read of a subscription, and rotateWebhookSecret (RFC 0201 §E) already returns no secret. The 201 is the one moment the subscriber can capture it.
  • Why no fingerprint. v1 also returned a secretFingerprint. Nothing in v2 reads it, so it is not carried over.

Examples

Conforming. POST /webhooks { url, events: ["run.completed"] } → 201 { webhookId, secret: "…" }, and each delivery's OpenWOP-Signature verifies under that secret. POST /webhooks { url, events, secret: "s3cr3t…" } → 201 { webhookId }.

Non-conforming. 201 { webhookId } for a secret-less registration. A returned secret that the deliveries are not signed with. A 201 that echoes a supplied secret.

Compatibility

additive. The 201 schema gains an optional secret member, and clients that ignore unknown members are unaffected. A host that already returns the generated secret (openwop-app) conforms unchanged. A host that generates and withholds it (the v2 reference host before #98) was producing unverifiable subscriptions, and now fails the row. No request, error code or status changes.

Conformance

v2-webhook-generated-secret.test.ts, gated on the webhooks family and the conformance-noop fixture:

  1. Register without a secret: the 201 must carry a non-empty secret.
  2. Run the noop fixture: the run.completed delivery must verify (OpenWOP-Signature, scheme v1) under that secret.
  3. Register with a secret: the 201 must not echo it.

Witnessed on the v2 reference host: executed-pass (6 assertions) with #98, and executed-fail on its previous code, which withheld the secret.

Falsifiability — one row per normative requirement

RequirementObservable — what an outside party seesWho can cause the conditionVerdict
§A the generated secret is returned, deliveries are signed with it, and a supplied one is never echoed (openwop.requirement.0221.generated-secret-returned)the 201 of a secret-less registration carries secret; a real delivery verifies under it; the 201 of a registration with a secret does not contain itthe suite, gated on the webhooks familywitnessable — gated

Alternatives considered

  1. Make secret REQUIRED in v2. It is simpler to state, but it refuses registrations v2 accepts today (breaking), and it discards the v1 behaviour openwop-app already implements.
  2. Allow unverifiable subscriptions and document it. Rejected: a signed delivery nobody can verify is a signature in name only, and the subscriber has no way to tell that from a forgery.
  3. Do nothing. Hosts stay split, and the quickstart has to keep telling readers to send their own secret.

Unresolved questions

None.

Implementation notes (non-normative)

  • openwop-app: already conforms (routes/webhooks.ts returns the generated secret when the request omitted one).
  • v2 reference host: openwop-examples #98.
  • MyndHyve: not yet measured; the row will say so on its next cut.

Acceptance criteria

  • [x] Active: the webhooks.md rule, the 201 schema member, and the scenario (suite 2.42.8), sabotage-proved on the v2 reference host.
  • [ ] openwop.requirement.0221.generated-secret-returned executed-pass on a committed certified host bundle.

References

  • spec/v1/webhooks.md (the secret "is returned once at registration time").
  • RFC 0201 §B.6 (a supplied secret is never echoed) and §E (rotation returns no secret).
  • openwop #1654 (the v2 quickstarts), openwop-examples #98.