Provider Webhook Secrets

Some services you depend on also call you back with signed webhooks — PolicySign today; Relay and lob-send have the same shape. To verify those webhooks your receiver needs the provider's signing secret. Declare where you receive them and the builder provisions the secret on every deploy. No copying whsec_… values into tawa config.

The Short Version

# Consumer — catalog-info.yaml (catalog 0.4.0+)
spec:
  dependencies:
    - service: policysign
      webhooks:
        path: /api/webhooks/policysign   # where you receive PolicySign's webhooks

On deploy the pod gets POLICYSIGN_WEBHOOK_SECRET, the secret of your PolicySign webhook endpoint for that URL. Verify with it as usual (Standard Webhooks).

No webhooks.path → nothing happens. A plain - service: policysign behaves exactly as before.

What the builder does

After dependency resolution, for each dependency with webhooks.path:

  1. Reads the provider's contract from Koko (metadata.webhookProvider, published by the provider's own deploy). No contract → a warning, nothing provisioned.
  2. Computes your receiver URL for this environment: your verified custom domain for the environment if you have one, else the platform hostname ({service}.tawa.pro, {service}.sandbox.tawa.pro, …) — plus webhooks.path.
  3. Calls the provider at https://{provider platform host}{provisionEndpoint} with the builder's Bio-ID client_credentials token and { orgSlug: spec.owner, url, consumerService, environment }.
  4. The provider finds or creates the endpoint in your org and returns its secret.
  5. The builder injects the secret as a managed secret under the contract's secretEnv (it lands in {service}-managed-secrets, never on the helm command line).
EnvironmentReceiver URL (no custom domain)Endpoint
prodhttps://my-app.tawa.pro/api/webhooks/policysignits own
sandboxhttps://my-app.sandbox.tawa.pro/api/webhooks/policysignits own
uathttps://my-app.uat.tawa.pro/api/webhooks/policysignits own

Each environment has a different URL, therefore a different endpoint and a different secret.

Adopt, never duplicate, never rotate

The provider must be safe to call on every deploy, forever:

  • Adopts an existing endpoint. PolicySign matches an active endpoint in your org on the exact URL, else the oldest one on the same origin — the same rule it uses to pick the secret that signs a delivery. An endpoint you registered by hand in the PolicySign console is adopted, and its current secret returned.
  • Never rotates. Rolling a secret is a deliberate action in the provider's console. The next consumer deploy picks the new value up — so redeploy the receiver inside the roll's grace window.
  • Never creates duplicates on redeploy, and never reads another org's endpoints.

A value you set by hand wins

If secretEnv is already set on your service — tawa config set --secret or plain tawa config set — the builder does not overwrite it:

Your value vs the provider'sBuild log
samePOLICYSIGN_WEBHOOK_SECRET: your tawa config value is kept; it matches …
different[warn] … DIFFERS from the secret policysign signs … with + how to switch

The comparison happens in memory; neither value is ever logged. To move to the automatic secret:

tawa config unset POLICYSIGN_WEBHOOK_SECRET
tawa deploy --prod

Fail open

Provisioning never blocks a deploy. Each of these is a [warn] line and no injection (a manually set value is still used):

  • the provider publishes no contract, or an invalid one
  • the builder cannot mint its Bio-ID service token
  • the provider is unreachable, times out (10s), or returns an error
  • secretEnv is a reserved platform variable or was already provisioned this deploy

Warnings link here.

Where the secret does NOT go

Never to the build log, Koko, the build record, or the deploy snapshot — the manifest lists the key with isSecret: true and no value.

Being a provider

Publish the contract in your catalog:

spec:
  webhookProvider:
    provisionEndpoint: /api/platform/webhook-endpoints   # absolute path on your public host
    secretEnv: POLICYSIGN_WEBHOOK_SECRET                 # must end _SECRET, _KEY, _TOKEN or _PASSWORD

Deploy once; the builder syncs it to Koko (metadata.webhookProvider). Remove it and redeploy to withdraw it. An invalid contract is dropped with a build warning. secretEnv may not be a reserved platform variable (BIO_CLIENT_SECRET, MONGODB_URI, …).

Your endpoint must:

RequirementWhy
POST, JSON body { orgSlug, url, consumerService, environment }the builder's call
respond { success: true, data: { endpointId, secret, match } }match is shown in the build log
authenticate the builder: verify the Bio-ID token by JWKS, require token_type: client_credentials and client_id on an allowlist (builder-prod)an org check is not enough — every insureco service's token carries orgSlug: insureco
be idempotent: adopt, never rotate, never duplicateit runs on every consumer deploy
scope everything to orgSlugnever return another org's secret
validate url against your SSRF guardthe URL becomes a delivery target
audit with a hint only, and send Cache-Control: no-storeit returns a credential

The builder reaches you on your public platform hostname — it runs off-cluster, so there is no Janus in front and /internal/* conventions give no protection. Authenticate in the handler.

PolicySign's implementation: app/api/platform/webhook-endpoints/route.ts, lib/services/webhookProvisioning.ts, lib/platformAuth.ts.

Common mistakes

  • Setting webhooks.path without the provider having deployed its contract — warns "publishes no webhook provider contract". Deploy the provider first.
  • Leaving an old manual value in place after a roll — the manual value wins, so the DIFFERS warning appears and verification fails. Unset it.
  • A path that isn't your receiver — the provider registers that exact URL as a delivery target.
  • Confusing the consumer's POLICYSIGN_WEBHOOK_SECRET with PolicySign's own config of the same name — PolicySign's is the legacy platform-wide X-PolicySign-Signature key and is unrelated.

Related

The same idea for a secret that lives in the Locker rather than at a provider: tawa config set --secret KEY --from-locker lk://insureco/<vault>/<item>, where the builder fetches the value itself. See config.

Last updated: October 6, 2026