Skip to content

Webhooks and public paths

A public path is a path on your app that anyone can reach without signing in or sending a token. Use one for webhooks. Stripe, GitHub and Slack can’t sign in, so you open the path they send to, and the rest of the app stays behind the edge.

Give the path a verifier that names the provider, and we check each delivery’s signature before your app sees it. The provider’s signing secret stays with us. Your app gets a token from us that names the provider, and checks it with the SDK’s requireWebhook.

Your workspace’s publish policy may keep opening paths to the people its owners name.

  1. Open the app in the dashboard and go to Sharing.

  2. Under Public paths, type the path your provider sends to, like /hooks/stripe. Choose the provider under Who sends to it?, and select Open it.

  3. For Standard Webhooks, choose whether its headers start with webhook- or svix-. For Other (HMAC-SHA256), give the header the signature is in, whether it’s hex or base64, and anything before it, such as sha256=. The other providers need nothing here.

  4. Paste the provider’s signing secret under Signing secret. The field is write-only: nobody can read the secret back, and it never reaches your app. For GitHub and Other, where you choose the secret yourself, Generate one makes one. Copy it and paste it at the provider as well, because once you save, nobody can see it here again.

  5. For Stripe, Slack and Standard Webhooks, choose How old a delivery may be. 5 minutes suits almost every sender.

  6. Copy the address under Give Stripe this address, or whichever provider you chose, into the provider’s webhook settings. Then select Open and verify.

  7. Give it a few seconds to go live, then send a test delivery from the provider. The path’s row says whether it got through, and why not if it didn’t.

Within a few seconds, deliveries to that path reach your app. The path’s row then reads like this:

/hooks/stripe Verified · Stripe
https://jiayang-apps.cloud/acme/hello/hooks/stripe · opened 3m ago by ana@example.com
Secret set 3m ago by ana@example.com. A delivery signed more than 5 min before it arrives is refused.
Last delivery 12s ago: let through.

How the app checks it, on the row, shows the call for each SDK.

Choosing Nobody I can verify opens the path with no check. Your app gets no token, and has to assume a request there could come from anyone. A path opened this way has a Verify button, so you can add a verifier later.

You can change paths and verifiers only in the dashboard, so a person always makes that decision. The CLI, and an agent using your CLI session, can list public paths but can’t open one or change a verifier. A bypass token can’t list them, because it only reaches the app it was made for. If you last signed in more than 15 minutes ago, the dashboard asks you to sign in again.

App owners, and the workspace’s owners and admins, can open and close paths and change their verifiers.

A public path is reached through the apps domain, with the workspace and the app in front:

https://jiayang-apps.cloud/acme/hello/hooks/stripe

Your app sees the request at /hooks/stripe. The app’s own address, https://hello--acme.jiayang-apps.cloud, still asks everyone to sign in, public paths included, so a webhook sent there never arrives. A request there for a path you opened is answered 401 (or sent to sign in, from a browser) with the address where that path is public, so a sender pointed at the wrong one says where to point it.

Pattern Matches
/hooks/stripe Exactly /hooks/stripe.
/hooks/* Every path under /hooks/, like /hooks/stripe or /hooks/github/push. Not /hooks itself.

A pattern is one or more parts separated by /. Each part uses letters, digits and . _ ~ @ = + $ -. A * can only be the whole last part. A pattern can’t have . or .. parts, empty parts or percent-encoded characters, and can be up to 200 characters long.

/* on its own is refused, because a whole app can’t be public. You can’t open anything under /.jiayang, in any letter case, because we use that path on every app for sign-in. You can open /.well-known/*, for an ACME challenge or an app-site association file.

Anything else is refused, with what’s wrong with it:

/hooks/*/x has a * that isn't at the end. A * can only end a path, after a slash, as in /hooks/*.
The whole app can't be public. Open one path, like /hooks/stripe, or everything under one, like /hooks/*.

When a path matches two open patterns, the more specific one decides, and so does its verifier: an exact pattern beats a prefix, and a longer prefix beats a shorter one. With /hooks/* verified for GitHub and /hooks/stripe verified for Stripe, a delivery to /hooks/stripe is checked as Stripe’s.

A verified exact pattern also covers its spelling with a trailing slash, /hooks/stripe/. An exact pattern with no verifier opens that one path and not its trailing-slash spelling. Some frameworks, Express among them, ignore case when they match a route, so /hooks/STRIPE can reach your /hooks/stripe handler.

Rails, and older versions of Spring, send /hooks/stripe.json there too, reading .json as the format. So we check those spellings too. A path that matches a verified pattern once you ignore case, or once you drop an extension from a verified exact pattern, is checked by that pattern’s verifier, even if it got in under another pattern.

With /hooks/* open and /hooks/stripe verified, /hooks/Stripe, /hooks/STRIPE/ and /hooks/stripe.json are checked as Stripe’s. Neither case nor an extension opens a path: with only /hooks/stripe open, /Hooks/Stripe and /hooks/stripe.json stay shut.

Provider Headers the check reads Signed id (delivery) Signed time
Stripe Stripe-Signature the event id yes
GitHub X-Hub-Signature-256 none no
Slack X-Slack-Signature and X-Slack-Request-Timestamp event_id, for Events API deliveries yes
Shopify X-Shopify-Hmac-Sha256 none no
Standard Webhooks webhook-id, webhook-timestamp and webhook-signature, or the same three starting svix- the message id yes
HMAC-SHA256 the one you name none no

Each needs its signing secret, and the dialog checks it looks right before you save:

Provider Where the secret is What it looks like
Stripe Workbench, Webhooks, your endpoint, Reveal secret starts with whsec_
GitHub the webhook’s Secret field, which you choose 16 to 512 characters
Slack your app’s settings, Basic Information, Signing Secret 16 to 512 characters
Shopify the app’s client secret, or the key under Settings, Notifications, Webhooks 16 to 512 characters
Standard Webhooks the endpoint’s secret, on the sender’s page for it whsec_ and then base64 of 24 to 64 bytes
HMAC-SHA256 whatever the sender signs with 16 to 512 characters

The 16 to 512 characters are letters, digits, spaces and punctuation. Spaces and line breaks a paste picks up at either end are dropped.

Standard Webhooks covers Svix and the senders built on it, Resend among them. Those send svix-id, svix-timestamp and svix-signature. The spec’s own names are webhook-id and the rest. The dialog asks which.

For any other sender, choose Other (HMAC-SHA256) and say which header holds the signature, whether it’s hex or base64, and what comes before it, such as sha256=. It has to be HMAC-SHA256 over the raw body.

Everyone who can see the app can read the header name and the prefix, so don’t put the secret, or anything shaped like a key, in either.

Shopify deletes a subscription made through its Admin API after 8 failed deliveries in a row, so send a test delivery a minute after the verifier is saved, once it’s live.

For a provider that signs a time, we refuse a delivery signed more than 5 minutes before it arrives, and one whose signed time is more than 5 minutes ahead of our clock. You can set this from 1 to 10 minutes with How old a delivery may be, in the menu on the path’s row.

Point one provider endpoint at each path. Two endpoints at the same provider sign with two different secrets, and a verifier holds one secret, or two while you rotate it.

The delivery arrives as the provider sent it: the method, the path after /acme/hello, the query string, the headers and the body, byte for byte. With it comes an identity token of kind webhook in X-Jiayang-Identity. Check it with the SDK and answer 401 when it refuses:

import { requireWebhook, Unauthorized } from "@jiayang-cloud/sdk";
try {
const hook = await requireWebhook(request, env, { provider: "stripe" });
// hook.delivery is Stripe's event id
} catch (err) {
if (err instanceof Unauthorized) return err.toResponse(); // 401
throw err;
}

Name only the providers that send to that route. A delivery from any other is refused, so a Stripe route can’t be handed a GitHub delivery after a pattern is widened.

requireUser refuses a webhook’s token and requireWebhook refuses a person’s, so mount the route where app-wide user checks never see it: before app.use(requireUser()) in Express, outside jiayang.Middleware in Go, and in a router of its own in axum.

Each language’s page shows its frameworks: Node.js, Python, Go and Rust.

Parse the body as you normally would. Nothing needs the raw bytes.

The token holds:

Claim Value
kind webhook
iss The edge, https://edge.jiayang.cloud
aud webhook:<app id>, which no check for a person accepts
sub webhook:<verifier id>
provider stripe, github, slack, shopify, standard_webhooks or hmac_sha256
pattern The public path it came in on, such as /hooks/stripe or /hooks/*
delivery The provider’s signed id, where there is one
signed_at The signed time in unix seconds, where the provider signs one
wid The app’s workspace id
iat, exp When it was made, and 60 seconds later
jti A random id for this token

It has no email and no role. Its header’s typ is webhook+jwt, where a person’s token has JWT. It’s signed with the same key as a person’s token, so the same key set checks it.

The request arrives as the sender made it: the method, the path after /acme/hello, the query string, the body, and headers like Stripe-Signature. If the sender puts its own credential in Authorization, that passes through too.

It arrives with no identity token, unless it carries one of our tokens, as below. The edge strips every X-Jiayang-* header the sender set and adds none of its own, so the SDKs’ requireUser() refuses the request. Don’t call it in your webhook handler.

Take an app that calls requireUser() on every path, like the one in the SDK overview. Once /hooks/stripe is open with no verifier, the edge lets the request through and the app itself refuses it:

Terminal window
curl -X POST https://jiayang-apps.cloud/acme/hello/hooks/stripe -H 'Stripe-Signature: t=1,v1=abc' -d '{}'
unauthorized

The status is 401, and it came from the app. The hello app from your first deploy doesn’t call requireUser(), so it answers 200 with Hello, there. instead. It reads X-Jiayang-Email, and the edge didn’t set one. Either way, the log shows the edge let the request through with no caller:

20:55:11 ALLOW - hello POST /acme/hello/hooks/stripe -

Route the webhook path to a handler that skips requireUser() and checks the provider’s signature itself, using a signing secret your app has to hold. With a verifier, you need neither.

On either kind of path, the edge removes cookies from the request and Set-Cookie from the response, as it does for tokens.

It also removes Sec-Fetch-Mode and Sec-Fetch-Dest. A request with no credential isn’t a browser loading one of your pages, and on an app that ships files with a single-page index.html, Cloudflare answers a request that says it is with that index.html before your code sees it. Without the two headers, a path with no file reaches your code.

A request with a bypass token or CLI session

Section titled “A request with a bypass token or CLI session”

A request with a bypass token or a CLI session in Authorization is treated as coming from that caller, on a public path as anywhere else. The edge checks the token as it would on any path.

If the token can use the app, the request arrives with that caller’s identity token, and a verified path’s signature check doesn’t apply. If it can’t, the request is refused, even on an open path. A bypass token for another app gets 403, and a person gets 403 or 404 as they would anywhere.

Only a credential that isn’t one of ours reaches your app untouched.

A static site has no code to answer for itself, so we answer for it. On a public path it serves only real files under the pattern, read with GET or HEAD. Anything else answers 404 with an empty body:

  • a missing file, even though the site serves its index.html or 404.html for one when you’re signed in
  • a path that only a _redirects rule answers, with no file of its own
  • a file whose _redirects rule sends it, or serves something else in its place, from outside the pattern
  • a directory with no index.html of its own

With /hooks/* open, /hooks/ping.txt is served if the site has that file, and /hooks/nope.txt is a 404. Signed in, /hooks/nope.txt still gets the site’s index.html, as it always did. A redirect between spellings of one file, like /hooks/page.html to /hooks/page, still works while it stays under the pattern.

A file whose name has @, =, + or $ in it can’t be reached on a public path: the site sends it to an escaped spelling, which a public path doesn’t take.

For up to a minute after an app with code becomes a static site, its public paths can still answer as they did for the code, until the edge picks up the change. A missing file there can get the site’s index.html in that minute. If that page is private, close the app’s public paths before you deploy the site, and open them again once it’s live.

The public paths on a static site serve up to 1,000 files between them. Opening a path that would cover more, or deploying a site with more under the paths already open, is refused with a message saying which path. Open a narrower one.

A framework build often ships files beside its code. On a public path, a request for one of those files under the pattern gets the file, and anything else goes to your code.

Three things would answer some of those requests before your code does, so we don’t allow them while the path is open:

  • a _redirects rule for a path under the pattern
  • run_worker_first set to a list of paths that leaves the pattern out, with not_found_handling set to single-page-application or 404-page. A path there with no file would get your index.html or 404.html
  • a page beside the pattern. With /hooks/* open and a /hooks.html but no /hooks/index.html, a request for /hooks/ is redirected to /hooks, which tells anyone that page is there. The same goes for /status/index.html with /status open and no /status.html

Opening such a path is refused, and so is deploying or rolling back to a version like that while the path is open. The message names the rule, the page or the path. Move the redirect into your code, add the path to run_worker_first, set not_found_handling to none, or rename the page.

An app that was already live like that when these checks came in had the path closed for it. The workspace’s owners got an email naming the path and why, and the activity log shows it. Fix the version, then open the path again from the app’s Sharing tab.

A path with a verifier stays open, since it only takes a signed delivery. If you remove the verifier from a path on an app that ships files, the app’s paths with no verifier ask for a sign-in for a minute or two while we check them, and one the files would answer first is closed.

We refuse a delivery whose signature doesn’t match. Keep requireWebhook() in the route anyway. Without it, the route takes unchecked requests:

  • for up to a minute after you add a verifier
  • from the moment you remove one
  • on any path where a more specific pattern with no verifier wins

Make a repeated delivery harmless:

  • On Stripe, Slack and Standard Webhooks, we refuse a copy of a delivery your app answered with a 2xx. A delivery your app answered with anything else can arrive again within the time allowed, so skip a delivery you’ve already handled.
  • GitHub, Shopify and plain HMAC senders don’t sign a time. Anyone with a copy of a delivery can send it again at any time, and change the query string or any header except the signature. Act on what the signed body says, never on X-GitHub-Event, X-Shopify-Topic or another header, and make sure handling a delivery twice is safe.
  • A Stripe-Signature header sent twice, where the second has no t=, reads as one header with two signatures. Every other provider’s header sent twice is refused.

On your machine, jiayang dev --as webhook --provider stripe --pattern '/hooks/*' sends every request under /hooks/ to your app as a Stripe delivery, so you can work on what the route does with one. It checks no provider’s signature, since no provider sent one. To test that part, deploy, send a test delivery from the provider, and watch it arrive.

Terminal window
jiayang watch acme/hello
20:58:02 ALLOW webhook:stripe hello POST /acme/hello/hooks/stripe -

jiayang watch is for the workspace’s owners and admins. Anyone who can see the app can check the path’s row, or jiayang app public-paths acme/hello.

  1. On the verified path’s row, open the menu and select Rotate secret.

  2. Paste the new secret. For GitHub and Other, Generate one makes one to paste at the sender too.

  3. Under Keep accepting the old secret for, choose how long deliveries signed with the old one still get through: None, stop now, 1 hour, 24 hours or 7 days. Shopify can take up to an hour to start signing with a new secret, so None, stop now isn’t offered for Shopify.

  4. Select Rotate.

Stripe signs with both secrets while you roll one there, so roll it in Stripe first and paste the new one here. For the others, change the secret at the sender within the time you chose. A new secret can take up to five seconds to be accepted everywhere.

Until then the row says The previous secret is accepted until and a time, with Stop accepting it to end the old secret early. A delivery the old secret let through reads let through, signed with the previous secret, so you can see when the provider has moved to the new one. Rotating again drops a previous secret that’s still being kept.

The row’s menu also has:

  • Replace verifier: changes the provider, its settings and the secret at once, and keeps no old secret.
  • Remove verifier: leaves the path open with no check. Within a minute, deliveries reach your app with no token, so an app using requireWebhook() refuses them.
  • How old a delivery may be: changes only the time allowed, from 1 to 10 minutes, and keeps the secret.
Terminal window
jiayang app public-paths acme/hello
Anyone can reach these paths without signing in:
/hooks/github verified: GitHub, secret set 2026-09-20 by ana@example.com, last delivery 2026-09-23 20:40 UTC refused because the signature didn't match the secret
https://jiayang-apps.cloud/acme/hello/hooks/github
/hooks/other not verified, opened by ana@example.com
https://jiayang-apps.cloud/acme/hello/hooks/other
/hooks/stripe verified: Stripe, secret set 2026-09-23 by ana@example.com, previous secret accepted until 2026-09-24 20:58 UTC, last delivery 2026-09-23 20:58 UTC let through
https://jiayang-apps.cloud/acme/hello/hooks/stripe
Send requests to the address under each path. The app's own address asks everyone to sign in, public paths included.

Under each path is the address to send to. A prefix such as /hooks/* shows https://jiayang-apps.cloud/acme/hello/hooks/, with the rest of the path to go on the end. A verified path shows its provider, who set its secret and when, how long a previous secret is still accepted, and whether the latest delivery in the last 30 days got through. Only people who can change the verifier see which check failed or which secret matched: workspace owners and admins, and app owners. The secret itself is never shown. --json gives the whole record, and an agent’s list_public_paths gives the same.

With nothing open:

Nothing on acme/hello is public. Open a path in the dashboard if a webhook needs one.

jiayang env set refuses a whsec_ value and says to add a verifier instead, and --allow-sensitive-name doesn’t override that. A name with WEBHOOK in it that reads like a credential gets the same advice, but --allow-sensitive-name sets it if the app has to hold it.

In Sharing, select the remove button next to the path. For a verified path you’re asked to confirm, because closing it also removes its verifier and secret. Select Close it. Within a minute the path answers 401 again, and any response still streaming from it is cut off.

On every public path:

Status Body Why
401 unauthorized The path isn’t open. An app that doesn’t exist, or has nothing deployed, gets the same answer.
401 unauthorized The request path has a percent-encoded character or an empty part like //.
401 unauthorized It asks to open a WebSocket. Public paths take plain requests only.
429 too many requests One client sent more than 600 requests in a minute under the same open pattern, or to paths the app hasn’t opened. A verified path isn’t held to this.
429 too many requests While we couldn’t get the list of apps, one client made us look up more than 600 names in a minute.
429 too many requests One client made us look up the same app more than 600 times in a minute. This only happens while the app’s workspace can’t serve requests, or we can’t look the app up. It applies on a verified path too.
402 or 403 A page The workspace can’t serve requests right now. See Limits.

A . or .. part isn’t refused, whether it’s written out or as %2e. It’s resolved before the edge reads the path, as any URL parser does, and your app gets the resolved path, so the edge and your app see the same one. /acme/hello/hooks/./stripe and /acme/hello/hooks/%2e/stripe both reach /hooks/stripe. A .. that climbs out of an open pattern lands on the path it names, and that path has to be open itself.

A provider sends for all its customers from a few shared addresses, so someone else’s endpoint at the same provider, or any Worker, can use up the count on your open path. Past 600 in a minute, your own deliveries from that address get 429 until the minute is up.

If the sender signs its deliveries, give the path a verifier. Deliveries to a verified path don’t count toward this limit. They have their own limits, described below.

How the limit counts, and how the edge looks an app up

The limit counts per client IP address, app and open pattern, at each Cloudflare location. Everything under /hooks/* shares one count. Requests to paths the app hasn’t opened share a separate count, so someone probing for paths doesn’t use up an open path’s limit. What the same address sends to other apps doesn’t count toward yours.

To know which count a request goes toward, and whether its path is verified, the edge first looks the app up and caches the answer for up to a minute. These lookups are limited too, so nobody can force a lookup with every request by naming a made-up app.

Each Cloudflare location keeps a list of the names that have an app, refreshed every minute or so. Looking up a name on the list counts only toward that app, 600 a minute per client IP address. Because the answer is cached, a sender delivering to your app doesn’t come close to this limit. Looking up a name that isn’t on the list counts toward a separate 600 a minute per address. Past that, those names get the answer for an app that doesn’t exist, without a lookup.

Refusals and failed lookups aren’t cached. While your workspace can’t serve requests, or we can’t look your app up, every request is a lookup, and past 600 from one address, requests get 429 until the minute is up. The same goes for a location that can’t get the list. An app made in the last few minutes may not be on the list yet. Until it is, it’s looked up only while the address is within its 600.

A verified path also refuses:

Status Body Why
401 unauthorized The method is anything but POST, or a header or _method in the query string asks for another.
401 unauthorized A signature header is missing, over 4 KiB, or not what the provider sends.
401 unauthorized It was signed further from now than the tolerance allows, either way.
401 unauthorized No signature matches.
401 unauthorized It’s a copy of a delivery already let through.
401 unauthorized The verifier, its secret or the app’s public paths changed while it was being checked.
401 unauthorized The path after /acme/hello is over 2,048 characters.
429 too many requests The workspace’s verified deliveries are over their limit, or this path has had more than its share of them.
413 payload too large The body is over 1 MiB.
503 service unavailable We couldn’t check it in time. Providers send it again.

Every 401 says unauthorized, so a stranger learns nothing about which check failed. The audit log says which. On a verified path, the 402 or 403 page, and the 429 for the workspace’s limit, go only to a delivery with a valid signature. A stranger gets 401 whatever state the workspace is in.

A request with no signature, sent to a path verified for Stripe:

Terminal window
curl -X POST https://jiayang-apps.cloud/acme/hello/hooks/stripe -d '{}'
unauthorized

This time the 401 came from us, and your app never saw the request. The log says why:

20:58:40 DENY - hello POST /acme/hello/hooks/stripe 401 webhook_refused (missing_header)

The path’s row in the dashboard says the same in words, to those who can change the verifier:

Last delivery 5s ago: refused, it had no signature header.

The provider’s own log shows a failed delivery with status 401 and nothing more. If your app answers with anything but a 2xx, the provider also sees a failed delivery. We don’t count it as already delivered, so the provider’s retry gets through. The one exception is a path under a flood, described below, where a copy sent within a minute is refused.

Verified deliveries count toward the workspace’s requests like any other. A workspace can take bursts of 600 verified deliveries, then 10 a second. The busiest verified path gets at most half of a burst, 300, so a flood at one path gets 429 for that path’s deliveries and leaves room for the others. A copy of a delivery that already got in doesn’t count again.

While a flood of forged deliveries hits a path, that path’s real deliveries can get 503. Most providers retry them. GitHub doesn’t, so redeliver those from the webhook’s Recent Deliveries.

Why one path can’t take it all, and what counts as a flood

A delivery can pass verification and still not be meant for you, because anyone can install your GitHub App or Slack app and have the provider sign their own events to your path. So half of a burst is kept for paths that have had fewer than half as many deliveries in the last minute or so.

Nothing sent to a verified path counts against the address it came from. Providers send for all their customers from a few shared addresses, so limiting the address would turn away your real deliveries along with anyone else’s. Each delivery is judged on its own.

A path is under a flood once it has refused about 60 deliveries in a minute, and more than it let through, so an occasional forged delivery doesn’t trigger it. A copy of a delivery it let through in the last minute counts as a refusal. The copy still gets through, unless the path is under a flood. Then it’s refused as soon as its signature header is read.

Before answering 503, a delivery waits up to a second and a half for a free check, so a short burst turns nothing away. Paths under a flood share half of the checks, and one workspace’s flooded paths can use only one of those at a time. Outside a flood, one workspace’s paths can use only a few checks at once, so no single customer can take them all.

An app can have 3 public paths on Free, and 10 on Team and Business. Ten is the most on any plan, because the edge keeps each app’s open paths in memory between requests and more wouldn’t fit. Going past it in the dashboard shows:

That's 4 public paths, and the Free plan allows 3 per app. Team allows 10 public paths per app.

A /hooks/* pattern counts as one, however many providers send under it. A verifier belongs to its path, so there’s no separate limit on verifiers, and every plan can have them.

Each change is logged as app.public_paths, with who made it and every path open after it. Each request to a path with no verifier is logged with - as the caller, allowed or refused.

A delivery a verifier let through names the provider as its caller, webhook:stripe in jiayang watch, jiayang top and jiayang audit. A refused one has - as the caller, the reason webhook_refused, or webhook_too_large for the 413, and the check that failed:

20:58:02 ALLOW webhook:stripe hello POST /acme/hello/hooks/stripe -
20:58:40 DENY - hello POST /acme/hello/hooks/stripe 401 webhook_refused (signature)

The check is one of method, missing_header, malformed, stale, signature, replayed, unconfigured, rate_limited or too_large, and --json has it in detail.webhook.failure. For a delivery let through, detail.webhook.matched says which secret matched, current or previous, and detail.webhook.delivery has the provider’s signed id where there is one. For the workspace’s owners and admins, See deliveries on a verified path’s row opens the app’s Activity with that path’s deliveries.

Changes to a verifier are logged as webhook.set, webhook.update, webhook.rotate, webhook.retire and webhook.remove, with the path and the provider and never the secret. See Audit log.

On a verified path, the edge sends the delivery’s body and its signature headers to our control plane, which checks them and keeps neither. What we keep:

  • The delivery’s entry in the audit log, as above. The provider’s id for the delivery is the only thing in it read from the body or the signature headers.
  • For Stripe, Slack and Standard Webhooks, a hash of the delivery’s signature, so a copy can be refused. It’s deleted within an hour.
  • The signing secret, encrypted. We decrypt it only to check a delivery to its path, and delete it when you remove the verifier or close the path.