For the complete documentation index, see llms.txt. This page is also available as Markdown.

Why is x-webhook-signature missing from some webhook deliveries?

Short answer: the terminal that produced the event has no signingSecret configured. This happens when a terminal has only ever received webhookUrls deliveries and has never had a terminal level default webhook set. Deliveries in this state are sent without an x-webhook-signature header, silently, with no error or warning on either side.

Why this happens

Bead has two payment webhook delivery paths for a given terminal:

  • The terminal's default webhook, set via PUT /Terminals/{id}/webhook

  • Per payment webhookUrls passed on POST /Payments (or the deprecated POST /Payments/crypto)

Both paths are signed using the same terminal signingSecret, and that secret is only created the first time PUT /Terminals/{id}/webhook succeeds for that terminal. If that call has never been made for a terminal, the terminal has no secret to sign with. Per payment webhookUrls deliveries for that terminal will still arrive, since delivery does not depend on having a default webhook URL configured, but they will not carry an x-webhook-signature header.

This most commonly affects integrators who rely entirely on webhookUrls for routing and never call the terminal webhook endpoint at all, since nothing in that flow requires it to work.

How to tell if this is happening to you

  • Check whether PUT /Terminals/{id}/webhook has ever been called successfully for the affected terminal. If it has not, this is the cause.

  • Inspect an incoming delivery for the x-webhook-signature header. If it is absent on every delivery for a terminal, rather than only on malformed or retried ones, this is almost always the explanation.

  • This is not a per event issue. It is a per terminal, all or nothing state: either the terminal has a secret and every delivery for it is signed, or it does not and none are.

How to fix it

Call PUT /Terminals/{id}/webhook once for the affected terminal to establish a signingSecret, even if the URL you set there is not the one you actually use for routing. The response returns the secret:

{
  "url": "https://yourapp.com/webhooks/payment-status",
  "signingSecret": "{base64EncodedSecret}"
}

Store that secret securely. Once it exists, both the terminal's default webhook and every webhookUrls delivery for that terminal will be signed and verifiable using it. See Webhook Management for the full request and response reference, and Webhook Event Reference for the verification steps.

Related pages

Last updated