Add 1,625 doc-sourced sample payloads across 102 providers - #25
Conversation
Covers 103 providers: 92 this repo has no entry for at all, and 11 it
already carries, where these fill in topics that have never been
captured. Nothing captured is overwritten — every file here is a topic
with no existing sample at that version.
None of them are captured requests. Each is the example the vendor
publishes in their own documentation, and each says so:
"source": {
"type": "vendor-documentation",
"url": "<the doc page the example was read from>",
"retrieved": "2026-07-29"
}
Existing entries have no `source` key, so its absence is what marks a
sample as observed. A capture must always beat a doc example: when real
traffic for one of these provider+topic pairs is sampled, it replaces
the file rather than sitting beside it. 248 doc samples were dropped for
exactly this reason before the PR was cut — shopify alone had 174 topics
already captured here.
Doc samples for an existing provider go in that provider's own version
directory, not a new `latest/`: shopify's in 2024-10/, github's in
2022-11-28/, checkout's in 1.0.25/, paypal's in 1.0/.
Three existing files change, each for a reason checked against the
vendor's own documentation:
- providers/monday/index.json — topic_identifier `type` -> `event.type`.
monday's payload is {"event": {"type": ...}} with no top-level `type`.
Confirmed by developer.monday.com ("Every webhook sent to your
endpoint will have an `event` field containing the payload with the
event's data") and by all four captured samples already here.
- providers/pipedrive/index.json — latest_version `1` -> `2`, and the
doc samples land in a new `2/`. The 12 captured samples are Webhooks
v1, which carries a top-level `event` field. Pipedrive removed that
field in v2 and replaced it with meta.action + meta.entity; v2 has
been the default since 2025-03-17. topic_identifier stays `event`
because that is what resolves for the v1 captures, and one identifier
has to serve both.
- providers/scrapfly/README.md — documents the Crawler API as a fourth
webhook family whose event name is in X-Scrapfly-Crawl-Event-Name,
since X-Scrapfly-Webhook-Resource-Type is "always `crawler`" there and
would collapse all seven crawler events into one topic.
GitLab's samples are named for the X-Gitlab-Event header values
("Job Hook") rather than the object_kind body field ("build"), matching
the seven already captured here and GitLab's own published table.
Follows the contributing guide: display-name `label`,
`configs.latest_version`, `configs.topic_identifier` where the provider
has one, and one `<topic>.json` per topic holding `headers` and `body`.
`yarn compile` produces all 111 providers with no topic collisions.
Source: hookdeck/webhook-registry `samples-doc/`, which records the
vendor URL and retrieval date behind every file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@garethx is attempting to deploy a commit to the Hookdeck Team on Vercel. A member of the Team first needs to authorize it. |
Both found by regenerating the source tree from scratch and diffing it against what was committed — the export is additive and never overwrites, so a file can go stale without anything noticing. - cloudinary: 1 sample (`upload`). The provider gained a webhook skill after the tree was last exported, so it had never been picked up. - coinbase-commerce/charge.confirmed: `source.url` said `api-arcitecture`, which 404s. The registry corrected that typo in its own evidence layer and this copy was missed. A citation nobody can open is the one thing a `source` key must never be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@garethx - Phil asked me to review this one. Most of it looks right: the 93 new providers, the 1. Shopify - remove the 29 files
The files declare their source as The captured samples in that directory all carry The dedupe rule made this worse rather than better - it dropped the 174 Shopify doc samples that already had captures, and what survived was the set with no capture, which skews to topics that postdate 2024-10. There's also 2. Scrapfly - remove the 7 crawler files
The blocker you found is real - Also, Scrapfly documents 8 crawler events - 3. latest_versionAll three of these say
Worth sweeping the other 90 for the same thing. Where the vendor stamps a version into the payload, that's the directory name. Rules that might help the agent
One unrelated thing
On your open question about the dotted Generated by Claude Code |
All three findings from @leggetter's review, plus the sweep it asked for. **shopify — 29 files removed.** They went in `2024-10/` because that is the provider's latest_version, but they were read from shopify.dev/docs/api/webhooks/latest, which serves the current stable version. 2024-10 stopped being accessible on 2025-10-16, so they could not have been sourced at it, and 15 of the 29 are inventory_transfers/* and inventory_shipments/* topics that postdate it. The 181 captures in that directory all carry `x-shopify-api-version: 2024-10`; these carried no version header at all. `scripts/shopify/` can capture properly. **scrapfly — 7 crawler files removed**, and providers/scrapfly/README.md is back to its original text. `scripts/scrapfly/` can capture these once requestReceiver.ts resolves an ordered topic_identifier list, which hookdeck#26 adds. Scrapfly documents 8 crawler events, not 7 — `crawler_url_visited` was missing here, which is its own argument for capturing rather than hand-writing them. **Six providers move out of `latest/`:** bridge-xyz `2024-01-01`, chargebee `v2`, docusign `v2.1`, klaviyo `2025-07-15`, microsoft-graph `v1.0`, smartcar `4.0`. The sweep across all 104 found that "has a version field" is too broad a rule — most providers have one and most do not qualify. Object versions (square, courier, github's `package_version`), event-schema versions (checkout's 1.0.0/1.2.0/2.0.0, zendesk, twitch's `subscription.version`) and asset versions (cloudinary's unix timestamp) version the thing carried, not the contract carrying it. The test that holds is whether the field versions the webhook API itself: `api_version`, `apiVersion`, or a field the vendor documents as such — Smartcar's `meta.version` is "Webhook API version" in their docs. Two left in `latest/` deliberately: huggingface's bare `version: 3` has no doc statement of what it versions, and notion's `api_version` appears on 4 of 23 samples. 1,661 -> 1,625 samples across 102 providers. `yarn compile` passes with 112 providers, 0 topic collisions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — all three are fixed, and the sweep turned up a correction to the rule itself. Pushed as 1. Shopify — removedConfirmed everything you described before removing them. All 29 carry Your point about the dedupe making it worse is the one I'd missed. The rule dropped the 174 topics that already had captures, so what survived was precisely the set with no capture — which skews to topics postdating 2024-10. The filter that was supposed to protect ground truth selected for the least placeable examples. 2. Scrapfly — removed, README revertedYou're right that hand-writing them cuts against the rule this PR adds. They're also incomplete: Scrapfly documents 8 crawler events and One thing to keep in view when #26's capture runs: those 7 samples are currently the provenance for scrapfly's resolved header discriminator in the registry ( 3. latest_version — swept all 104Applied your three, and the sweep found three more that qualify: The sweep also says the rule needs narrowing. "Where the vendor stamps a version into the payload" matches 27 of the 104, and most of them shouldn't move — you spotted this with discord and square, but it's broader than those two:
The test that holds: does the field version the webhook API, or the thing the webhook is about? Checkout is the clearest case for why it matters — its samples carry three different Two I left in The rest
All three of your rules are now written down in |
`doc_sourced` was read from index.json but never published until this branch. Nothing consumes it yet, so this is the cheapest moment to change the shape — after #25 lands and stamps 104 providers it becomes a migration. Three problems with the boolean: `config.doc_sourced === true` coerced absence to `false`, and `false` is not "unknown" — it is a positive claim that the samples were captured live. Every provider in the repo would have started asserting that the moment the field was published, including ones nobody has verified. `provenance` defaults to `unknown` instead, and an unrecognised `sourced_via` fails the build rather than reaching consumers looking authoritative. Provenance is a property of a version, not a provider. `providers/ scrapfly` alone is mixed: the crawler samples were captured today, the scrape and extraction samples in May, and `screenshot` can never be captured at all because Hookdeck rejects Scrapfly's content-type. One boolean on the provider cannot express that. And docs are not equivalent to capture. Scrapfly's crawler docs state the resource-type header is `crawler`; real deliveries send `crawl`. Samples transcribed from those docs encode a value that does not exist in production. Flattening both into one "verified" bit would hide exactly the discrepancy a consumer needs to see. `sourced_on` records when a version's samples were obtained, using the oldest date where a version is mixed so it never overstates freshness. Recorded for shopify/2026-07 and scrapfly/latest, both captured first-hand. shopify 2023-01 and 2024-10 are left `unknown` rather than inferred from commit dates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pce9oHWGjdwHsJNya1ovP
We've built on
|
…-6cqxsq Enable the captures PR #25 substituted with doc examples
hookdeck#26 replaced `doc_sourced` with a `provenance` block keyed by version. 93 index.json files move to it; no payload changes — the per-sample `source` key was already the version-independent half of the same idea. "provenance": { "latest": { "sourced_via": "docs", "sourced_on": "2026-07-29" } } All 1,625 samples here carry `retrieved: 2026-07-29`, so that is the `sourced_on` for every version, and the "oldest where files span dates" rule has nothing to disambiguate yet. The 11 providers that merge into an existing entry deliberately get no `provenance`. After merging, those versions hold captures and doc examples together, and `sourced_via` has no value for "both" — so no claim is the honest record, and it reads as `unknown` rather than as a false `capture`. Merged main, which brings hookdeck#26's captures. Re-checked the additive rule against it: 1,625 doc-sourced files across 102 providers, 0 of them sitting where main now has a capture. shopify and scrapfly were already removed from this branch in the previous commit, which is what hookdeck#26 then captured for real — including `crawler_url_visited`, the eighth crawler event this PR's hand-written set was missing. `yarn compile` passes under hookdeck#26's stricter compile.ts, which now validates `sourced_via` and fails an unrecognised value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewing hookdeck#25 surfaced two providers where doc-sourced samples were added despite this repo already having capture automation for them, and one publishing gap. This is the groundwork so those captures can be run instead. Scrapfly Crawler API: Crawler deliveries set X-Scrapfly-Webhook-Resource-Type to a constant `crawler` and carry the event name in X-Scrapfly-Crawl-Event-Name, so the receiver wrote every crawler event to one crawler.json. That is what made the events look uncapturable. topic_identifier now accepts an ordered list and requestReceiver takes the first key that resolves, so crawler deliveries resolve by event name while scrape and extraction still fall through to resource-type. capture.ts gains a /crawl trigger covering the four events a clean crawl emits; the four that only fire on a failed or interrupted crawl are excluded from the delete-then-wait cycle so a normal run can't destroy a sample it cannot recreate. Shopify 2026-07: providers/shopify is pinned to 2024-10, which stopped being accessible on 2025-10-16. Adds scripts/shopify/2026-07 alongside the 2024-10 harness: 217 topics extracted from the current docs (22 gone since 2024-10, 31 new), version taken from the directory name, app path and base URL via env instead of editing the file, and the off-by-one that triggered an undefined topic on the last iteration fixed. latest_version stays 2024-10 until the capture has actually run -- compile publishes latest_version and the version list separately, so pointing at an empty version would break consumers. compile.ts: doc_sourced was read from index.json but never published, so consumers could not tell a doc-sourced provider from a captured one without downloading a version file. It now reaches providers.json. Verified: yarn compile passes; receiver resolves crawler, scrape, shopify header and monday body identifiers correctly, and no longer throws on a null body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjdTHaFed88Xd6fBFbJTKf
|
Adopted Payloads unchanged, 93 Your absence-was-reading-as-a-claim point is the one that generalises. It's the same defect the registry calls absence is data — a missing fact and a negative fact must not look alike — and it had crossed a repo boundary without anyone noticing. Worth it landing on the schema rather than in a comment. The 11 providers that merge into an existing entry get no The Scrapfly
|
Adds 1,625 sample payloads across 102 providers — 91 this repo has no entry
for at all, and the rest filling in never-captured topics for providers it
already carries. Nothing captured is overwritten.
They are not captured requests. Each is the example the vendor publishes in their
own documentation, harvested and reviewed in
hookdeck/webhook-registry
(
samples-doc/), and each says so on its face:{ "headers": { "content-type": "application/json" }, "body": { "...": "the vendor's documented example" }, "topic": "payment.succeeded", "source": { "type": "vendor-documentation", "url": "https://vendor.example/docs/webhooks", "retrieved": "2026-07-29" } }Existing entries carry no
sourcekey, so absence of the key is what marks asample as observed. That is what keeps this additive rather than a downgrade.
Why
The Example Webhooks picker covers 19 providers. Hookdeck supports 162. For the
rest there is nothing to mock against, and for most there is no captured traffic
to draw on yet either. A vendor's published example is a worse sample than a real
request and a much better one than an empty picker.
A capture always beats a doc example
Enforced before the PR was cut, not left to review: the exporter reads this repo
and will not emit a topic already captured here at that version. 248 doc samples
were dropped on that rule — shopify 174, bigcommerce 32, paddlebilling 16,
github 10, adyen 6, gitlab 5, checkout 5. When real traffic for any remaining
provider+topic pair is sampled, it replaces the file rather than sitting beside it.
What's in it
yarn compileFor the 11 providers already here, the doc samples go in that provider's own
version directory, not a new
latest/:adyen1/bigcommerce2025-04/checkout1.0.25/github2022-11-28/gitlabcurrent/mondaycurrent/paddlebillingcurrent/paypal1.0/pipedrive2/Six more move out of
latest/into the version the vendor stamps into thepayload:
bridge-xyz→2024-01-01,chargebee→v2,docusign→v2.1,klaviyo→2025-07-15,microsoft-graph→v1.0,smartcar→4.0.The sweep behind that found "has a version field" is too broad a rule — most
providers have one and most don't qualify. Object versions (
square,courier,github's
package_version), event-schema versions (checkout's 1.0.0/1.2.0/2.0.0,zendesk, twitch'ssubscription.version) and asset versions (cloudinary's unixtimestamp) version the thing carried, not the contract carrying it. The test that
holds is whether the field versions the webhook API itself:
api_version,apiVersion, or a field the vendor documents as such — Smartcar'smeta.versionis "Webhook API version" in their docs.
Two are deliberately left in
latest/:huggingface's bareversion: 3has nodoc statement of what it versions, and
notion'sapi_versionappears on 4 of23 samples.
shopifyandscrapflyare no longer here at all. Both have captureharnesses in this repo, and a doc example standing where a real one can be had
makes the gap look filled. Shopify's 29 were also unplaceable: read from
shopify.dev/docs/api/webhooks/latest, which serves current stable, while thedirectory is
2024-10— inaccessible since 2025-10-16, and 15 of the 29 topicspostdate it. Scrapfly's 7 are also incomplete: Scrapfly documents 8 crawler
events and
crawler_url_visitedwas missing, which is its own argument forcapturing them.
The three existing files that change
Each was checked against the vendor's live documentation, not inferred from the
payloads on disk.
providers/monday/index.json—topic_identifiertype→event.typemonday's payload is
{"event": {"type": "create_pulse", ...}}; there is notop-level
type.developer.monday.com:
"Every webhook sent to your endpoint will have an
eventfield containing thepayload with the event's data." All four captured samples already in this repo
agree. The current value cannot resolve —
requestReceiver.tswould have writtenuntitled-<md5>for every one of them, which is presumably why they were enteredby hand.
providers/pipedrive/index.json—latest_version1→2, doc samples in a new2/The 12 captured samples are Webhooks v1: top-level
event, plusv,matches_filtersandretry. Pipedrive removed theeventfield in v2 andreplaced it with
meta.action+meta.entity(migration guide),
and v2 has been the default since 2025-03-17.
So these are a different format, not a different rendering of the same one, and
they get their own version rather than merging into
1/.topic_identifierstaysevent: it resolves for the v1 captures,meta.actionalone would yield
createrather thancreate.deal, and only one identifier perprovider is expressible.
— reverted, no longer changedproviders/scrapfly/README.mdKept for the record, since the finding stands even though the files are gone.
X-Scrapfly-Webhook-Resource-Typenames the product, not the event.Scrapfly's crawler webhook docs
call it "Resource type (always
crawlerfor crawler webhooks)" — so on thisfamily it is a constant, and keying on it would collapse all seven crawler events
into one topic called
crawler. The event name is inX-Scrapfly-Crawl-Event-Name.topic_identifieris left alone because theexisting value is what resolves for the Scrape and Extraction captures; the
crawler files carry their topic explicitly.
This is the one place where this PR's
topic_identifierand the registry'sdisagree, and it is a real disagreement rather than an oversight. The registry
now records
X-Scrapfly-Crawl-Event-Nameas scrapfly's discriminator withjoin_check: verified(examplecrawler_cancelled, resolved from these verysamples). It is right for classifying deliveries; the value here is right for
resolving a topic out of the two captures this repo already holds. One
topic_identifierper provider cannot express both, and changing it wouldretopic the existing
scrape.jsonandextraction.jsontocrawler.GitLab topics use the header vocabulary
GitLab's samples are named
Job Hook.json, notbuild.json— theX-Gitlab-Eventheader value, which is whattopic_identifierpoints at andwhat the seven captured samples here already use. The source corpus had named
them after the
object_kindbody field, which also meant the synthesized headercarried a string GitLab never sends. Renamed from
GitLab's own table,
which is also why it's a table and not a transform:
buildmaps toJob Hook.Contributing-guide conformance
one directory per provider under
providers/index.jsonwith a display-namelabel,configs.latest_version, andconfigs.topic_identifierwhere the provider has onelatest_versionis"latest"only for providers with no version scheme intheir docs, which the guide's "if the provider doesn't offer versioning then
input
latest" coverstopic_identifieromitted where the provider has none —rechargehas noevent-type field in the payload, and
adyenkeeps this repo's existing emptyvalue because its
eventCodeis nested insidenotificationItems[].NotificationRequestItemrather than at the top level(Adyen webhook structure)
one
<topic>.jsonper topic holdingheadersandbody, filename derived fromthe topic the same way
requestReceiver.tsdoes it (store/order/created→store.order.created.json), with the unmodified topic inside the fileheader-carried
topic_identifiers lowercased, matchingx-shopify-topic/x-github-eventand the header keys in the samples themselvesThis one is no longer just a convention match. The casing mismatch it came from
(Twitch documents
Twitch-Eventsub-Subscription-Typeand sendstwitch-eventsub-subscription-type) turned out to be the visible end of abroader defect, now fixed in
hookdeck/webhook-registry#4:
header names are matched case-insensitively per RFC 9110 §5.1, and the registry
publishes a case-folded
discriminator_path_lookupbeside the documented pathfor exactly this reason.
Every header-carried
topic_identifierin this PR was cross-checked against it.10 providers here have a header discriminator — bitbucket, commercelayer, github,
gitlab, scrapfly, shopify, solidgate, twitch, woocommerce, zerohash — and 9 match
exactly. The tenth is
scrapfly, deliberately, for the reason below.Headers are synthesized
Vendor docs publish bodies, not deliveries. Each file carries
content-typeplusthe topic header where the provider puts the event type in one. Signature headers
are absent — mock sends can't produce valid signatures anyway. A capture upgrades
the headers along with the body.
One thing a maintainer should decide
21 providers have a
topic_identifierthat a flat lookup won't resolve.requestReceiver.tsresolves the identifier asheaders[ti] || body[ti]— one level, no paths. Every entry in this repo todayis flat, but 21 of these providers genuinely put the event type deeper: 15 dotted
(
event.type,data.event_type,metadata.topic) and 6 inside an array(
events[].type,value[].changeType,entry[].changes[].field). A handful morecompose the topic from two fields — pipedrive's
meta.action+meta.entity,xero's
eventCategory+eventType.Nothing here breaks:
compile.tspassestopic_identifierthrough untouched andevery file carries an explicit
topic. But if Console's extraction is flat likethe receiver's, those identifiers are decorative. Tell me which it is and I'll
either leave them (accurate, and useful the day paths are supported) or drop them
to
null(honest about what today's readers can do). Flattening them to the lastsegment is the one option I'd avoid —
event.type→typeresolves to nothingfor monday and silently mis-resolves for others.