A webhook gives you a URL. Anything that posts to it starts a run of every deployment bound to it.
https://your-konnecthub-host/api/v1/webhooks/trigger/<token>
The token in that URL is the credential, so treat the URL as a secret. Any HTTP method is accepted — the body becomes the execution's payload, and the method, headers and query are recorded as the event's metadata.
| Status | Meaning |
|---|---|
201 | Recorded and enqueued — the normal fire-and-forget case |
200 | The flow's own streamed response (waitForResponse webhooks only) |
202 | Accepted but nothing ran: the webhook is off, no deployment is bound, or the target deployment is off and the delivery is held |
The 202 body says which of those it was (inactive, noTargets, held), so a caller that
cares can tell a misconfiguration from a discard.
The token proves the URL was known. To also prove the body was not tampered with, configure an HMAC:
| Setting | What it is |
|---|---|
| Secret | The shared key the sender signs with |
| Location | Whether the signature arrives in a header or a query parameter |
| Key | The name of that header or parameter (X-Shopify-Hmac-Sha256, signature, …) |
| Algorithm | SHA256, SHA512 or MD5 |
The signature is computed over the raw body and compared in constant time. Providers disagree on how the digest is spelled — GitHub sends hex, Shopify sends base64 — so both encodings are accepted and you do not have to configure which.
Most providers retry, and most send a delivery id when they do. Name that header as the
webhook's dedupe header (X-Shopify-Webhook-Id, X-GitHub-Delivery) and a repeat within the
dedupe window is dropped instead of re-dispatched.
Without it, a provider's retry is a second, independent run.
By default the call returns as soon as the delivery is recorded — your flow has not started yet. For an endpoint whose caller needs a real answer, turn on wait for response: the caller is held open and the flow streams the reply.
async respond() {
await this.sendResponseMeta({ status: 200, headers: { "content-type": "application/json" } });
await this.sendResponse(JSON.stringify({ ok: true }), true); // `true` = final chunk
}
sendResponseMeta must come before the first sendResponse. Chunks are streamed as you write
them, so a long answer starts arriving before the run finishes.
A webhook can be pointed at an event source rather than at deployments. The delivery is
converted into an event and fanned out to every subscription, and the caller gets 202.
This is what you want when several unrelated deployments should react to the same inbound delivery, and it is how platform services ingest their providers' webhooks.
Such a webhook has no bindings of its own — the subscriptions belong to the source.
A delivery has to pass both:
| Switch | Off means |
|---|---|
| The webhook's own | Nothing is read, nothing is recorded, 202 |
| The binding's | This deployment is unwired; the webhook still fires and still records |
Because an inactive webhook records nothing, its test delivery button is refused while it is off — a test that produced something a real delivery no longer would is a lie about the system.
A webhook with no bindings still records every delivery. That is the point: its statistics then read "3 deliveries, 0 runs", which is what tells you nobody was listening. It is not an error and does not stop the caller.