{"id":15392621,"url":"https://github.com/jackdbd/webhooks","last_synced_at":"2026-01-20T04:04:03.214Z","repository":{"id":168355196,"uuid":"617558671","full_name":"jackdbd/webhooks","owner":"jackdbd","description":"Application that I use to process webhook events fired by several 3rd party services","archived":false,"fork":false,"pushed_at":"2024-06-08T14:51:56.000Z","size":402,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-06T15:50:53.930Z","etag":null,"topics":["cloudflare","cloudflare-pages","webhook"],"latest_commit_sha":null,"homepage":"https://webhooks.giacomodebidda.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jackdbd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-22T16:28:58.000Z","updated_at":"2024-06-08T14:51:59.000Z","dependencies_parsed_at":"2024-10-19T00:15:50.859Z","dependency_job_id":"eec452e8-f6b1-43f1-ae6c-8e268e0b456e","html_url":"https://github.com/jackdbd/webhooks","commit_stats":{"total_commits":38,"total_committers":1,"mean_commits":38.0,"dds":0.0,"last_synced_commit":"4aeedbc099a0673924b6184a6ed1ad6de1afe69e"},"previous_names":["jackdbd/webhooks"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jackdbd/webhooks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackdbd%2Fwebhooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackdbd%2Fwebhooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackdbd%2Fwebhooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackdbd%2Fwebhooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jackdbd","download_url":"https://codeload.github.com/jackdbd/webhooks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jackdbd%2Fwebhooks/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28595352,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T02:08:49.799Z","status":"ssl_error","status_checked_at":"2026-01-20T02:08:44.148Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","cloudflare-pages","webhook"],"created_at":"2024-10-01T15:15:25.003Z","updated_at":"2026-01-20T04:04:03.191Z","avatar_url":"https://github.com/jackdbd.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# webhooks 🪝\n\nMy collection of webhook targets for several services: [Cal.com](https://cal.com/docs/core-features/webhooks), [Cloud Monitoring](https://cloud.google.com/monitoring/support/notification-options#webhooks), [npm.js](https://docs.npmjs.com/cli/v7/commands/npm-hook), [Stripe](https://stripe.com/docs/webhooks), etc.\n\nAll webhooks targets are hosted on the same Cloudflare Pages website. Some routes are handled by Cloudflare Pages [Functions routing](https://developers.cloudflare.com/pages/platform/functions/routing/). Some other routes are handled by [Hono](https://hono.dev/api/routing).\n\n| service | routing |\n| :--- | :--- |\n| `cal` | Hono |\n| `cloudinary` | Hono |\n| `monitoring` | Hono |\n| `npm` | Pages Functions |\n| `stripe` | Hono |\n| `webpagetest` | Pages Functions |\n\n## Installation\n\nThis project requires a recent version of Node.js, ngrok and wrangler.\n\nIf you use the [nix package manager](https://nixos.org/), you don't have to worry about installing them, since they are specified in the `flake.nix` file and will be installed automatically when you enter the project root directory. Otherwise you'll have to install them manually.\n\nYou then have to install the npm packages:\n\n```sh\nnpm install\n```\n\nThe project also requires a few environment variables and secrets to be set (see below).\n\n## Development\n\nWhen developing this Cloudflare Pages Function project, you will need to create a [.dev.vars](https://developers.cloudflare.com/workers/configuration/secrets/#secrets-in-development) file in the repository root. This file should **not** be tracked in version control since it contains environment variables and secrets that will be used when running `wrangler pages dev`.\n\nYou can generate the `.dev.vars` file using this script:\n\n```sh\nnode scripts/make-dev-vars.mjs\n```\n\nWhen developing handlers for [Stripe webhooks](https://stripe.com/docs/webhooks), you will need 2 terminals open to develop this application. In all other cases you will need 3 terminals open. I use [Tmux](https://github.com/tmux/tmux/wiki) for this.\n\n### Environment variables \u0026 secrets\n\nWhen developing an app for Cloudflare Workers or Cloudflare Pages with `wrangler dev`, you can set environment variables and secrets in a `.dev.vars` file. This file must be kept in the root directory of your project. Given that some secrets might be JSON strings, I like to keep them the [secrets](./secrets/README.md) directory. Then I generate the `.dev.vars` file using this script:\n\n```sh\nnode scripts/make-dev-vars.mjs\n```\n\n### Stripe webhooks\n\nFirst of all, create a Stripe webhook endpoint for you Stripe account in **test** mode, and your Stripe account in **live** mode. Double check that you have created and enabled such endpoints:\n\n```sh\nstripe webhook_endpoints list --api-key $STRIPE_API_KEY_TEST\nstripe webhook_endpoints list --api-key $STRIPE_API_KEY_LIVE\n```\n\nIn the **first terminal** run the following command, which watches all files using [wrangler](https://github.com/cloudflare/workers-sdk) and forwards all Stripe webhook events to `localhost:8788` using the [Stripe CLI](https://github.com/stripe/stripe-cli):\n\n```sh\nnpm run dev\n```\n\nThe main web page will be available at: http://localhost:8788/\n\nIn the **second terminal**, [trigger](https://stripe.com/docs/cli/trigger) some Stripe events:\n\n```sh\nstripe trigger customer.created\nstripe trigger payment_intent.succeeded\nstripe trigger price.created\nstripe trigger product.created\n\nAPI_KEY=$(cat secrets/stripe-webhook-endpoint-live.json | jq '.api_key') \u0026\u0026 \\\nSIGNING_SECRET=$(cat secrets/stripe-webhook-endpoint-live.json | jq '.signing_secret') \u0026\u0026\necho \"API key is ${API_KEY} and secret is ${SIGNING_SECRET}\"\n\nstripe trigger --api-key $STRIPE_API_KEY_RESTRICTED customer.created\n```\n\nOr make some POST requests manually:\n\nPOST to the test endpoint without required header and invalid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/stripe\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"foo\": \"bar\", \"baz\": 123}' | jq\n```\n\nPOST to the test endpoint with the required header but invalid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/stripe\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"stripe-signature: foobar\" \\\n  -d '{\"foo\": \"bar\", \"baz\": 123}' | jq\n```\n\nPOST to the test endpoint with the required header and valid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/stripe\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"stripe-signature: foobar\" \\\n  -d \"@./assets/webhook-events/stripe/customer-created.json\" | jq\n```\n\nPOST to the live endpoint with invalid data:\n\n```sh\nSTRIPE_WEBHOOKS_ENDPOINT=$(\n  cat secrets/stripe-webhook-endpoint-live.json | jq '.url' | tr -d '\"'\n) \u0026\u0026 \\\ncurl $STRIPE_WEBHOOKS_ENDPOINT \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"foo\": \"bar\",\n    \"baz\": 123\n  }' | jq\n```\n\nAlso, send a GET request to see list of all events that Stripe is allowed to send to this endpoint:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/stripe\" \\\n  -X GET \\\n  -H \"Content-Type: application/json\" | jq\n```\n\n### Instructions for all webhooks except the ones from Stripe\n\nIn the **first terminal**, run this command to [develop the Pages application locally](https://developers.cloudflare.com/pages/functions/local-development/#run-your-pages-project-locally):\n\n```sh\nnpm run dev:pages\n```\n\nThe app will be available at: http://localhost:8788/\n\nIn the **second terminal**, run this command to create a HTTPS =\u003e HTTP tunnel with [ngrok](https://ngrok.com/) on port `8788`:\n\n```sh\nnpm run tunnel\n```\n\nNow copy the public, **Forwarding URL** that ngrok gave you, and assign it to the `WEBHOOKS_TARGET` environment variable (for example, paste it in your `.envrc` file and reload it with `direnv allow`). Be sure to **remove any trailing slashes**.\n\n![Setting up a HTTP tunnel with ngrok](./assets/images/http-tunnel-with-ngrok.png)\n\n\u003e :information_source: **Note:**\n\u003e\n\u003e Now you can also:\n\u003e\n\u003e - visit http://localhost:4040/status to know the public URL ngrok assigned you.\n\u003e - visit http://localhost:4040/inspect/http to inspect/replay past requests that were tunneled by ngrok.\n\nIn the **third terminal**, make some POST requests simulating webhook events sent by a third-party service. See a few examples below.\n\n### cal.com webhooks\n\nSee the [documentation on cal.com](https://cal.com/docs/core-features/webhooks).\n\n![Cal.com webhooks configuration](./assets/images/cal-webhooks.png)\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cal\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"foo\": 123, \"bar\": 456}' | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cal\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cal-Signature-256: hex-string-sent-by-cal.com\" \\\n  -d '{\"foo\": 123, \"bar\": 456}' | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cal\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cal-Signature-256: hex-string-sent-by-cal.com\" \\\n  -d \"@./assets/webhook-events/cal/booking-created.json\" | jq\n```\n\nCreate a new booking:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cal\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cal-Signature-256: hex-string-sent-by-cal.com\" \\\n  -d \"@./assets/webhook-events/cal/booking-created.json\" | jq\n```\n\nReschedule a booking:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cal\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cal-Signature-256: hex-string-sent-by-cal.com\" \\\n  -d \"@./assets/webhook-events/cal/booking-rescheduled.json\" | jq\n```\n\nCancel a booking:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cal\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cal-Signature-256: hex-string-sent-by-cal.com\" \\\n  -d \"@./assets/webhook-events/cal/booking-cancelled.json\" | jq\n```\n\nEvent sent by cal.com when a meeting ends:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cal\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cal-Signature-256: hex-string-sent-by-cal.com\" \\\n  -d \"@./assets/webhook-events/cal/meeting-ended.json\" | jq\n```\n\n### Cloudinary webhooks\n\nSee the [documentation on Cloudinary](https://cloudinary.com/documentation/notifications).\n\nMissing headers, invalid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cloudinary\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"foo\": 123, \"bar\": 456}' | jq\n```\n\nRequired headers, invalid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cloudinary\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cld-Signature: signature-sent-by-cloudinary\" \\\n  -H \"X-Cld-Timestamp: 1685819601\" \\\n  -d '{\"foo\": 123, \"bar\": 456}' | jq\n```\n\nRequired headers, valid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cloudinary\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cld-Signature: signature-sent-by-cloudinary\" \\\n  -H \"X-Cld-Timestamp: 1685819601\" \\\n  -d \"@./assets/webhook-events/cloudinary/image-uploaded.json\" | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/cloudinary\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Cld-Signature: signature-sent-by-cloudinary\" \\\n  -H \"X-Cld-Timestamp: 1685819601\" \\\n  -d \"@./assets/webhook-events/cloudinary/image-uploaded.json\" | jq\n```\n\n### Cloud Monitoring webhooks\n\nSee the [documentation on Cloud Monitoring](https://cloud.google.com/monitoring/support/notification-options#webhooks).\n\nMissing headers, invalid data:\n\nA [Cloud Monitoring webhook notification channel](https://cloud.google.com/monitoring/support/notification-options#webhooks) supports basic access authentication.\n\nCloud Monitoring requires your server to return a 401 response with the proper [WWW-Authenticate header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate). So we use `curl --include` or `curl --verbose` to verify that the server returns the `WWW-Authenticate` response header.\n\n```sh\ncurl \"$WEBHOOKS_TARGET/monitoring\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"foo\": 123, \"bar\": 456}' --include\n```\n\nRequired headers, invalid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/monitoring\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Basic $BASE64_ENCODED_BASIC_AUTH\" \\\n  -d '{\"foo\": 123, \"bar\": 456}' | jq\n```\n\nRequired headers, valid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/monitoring\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Basic $BASE64_ENCODED_BASIC_AUTH\" \\\n  -d \"@./assets/webhook-events/cloud-monitoring/incident-created.json\" | jq\n```\n\nRequired headers, valid data:\n\n```sh\ncurl \"$WEBHOOKS_TARGET/monitoring\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Basic $BASE64_ENCODED_BASIC_AUTH\" \\\n  -d \"@./assets/webhook-events/cloud-monitoring/incident-created.json\" | jq\n```\n\n### HubSpot webhooks\n\nSee the [documentation on developer.hubspot.com](https://developers.hubspot.com/docs/api/webhooks).\n\n```sh\ncurl \"$WEBHOOKS_TARGET/hubspot\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d \"@./assets/webhook-events/hubspot/contact-created.json\" | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/hubspot\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d \"@./assets/webhook-events/hubspot/product-created.json\" | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/hubspot\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d \"@./assets/webhook-events/hubspot/deal-created.json\" | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/hubspot\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d \"@./assets/webhook-events/hubspot/deal-deleted.json\" | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/hubspot\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d \"@./assets/webhook-events/hubspot/product-deleted.json\" | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/hubspot\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d \"@./assets/webhook-events/hubspot/contact-deleted.json\" | jq\n```\n\n### npm.js webhooks\n\nSee the [documentation on npm.js](https://docs.npmjs.com/cli/v9/commands/npm-hook).\n\nOn NixOS, `~/.npmrc` is a symbolic link to a filepath in the Nix store, which is a read-only filesystem. To authenticate with the npm CLI we have to use a local `.npmrc`. Create an empty `.npmrc` in the project root, then obtain the authentication token from npm.js by running the following command:\n\n```sh\nnpm adduser --userconfig .npmrc\n```\n\nNow all npm commands that require authentication should work fine:\n\n```sh\nnpm whoami\nnpm hook ls\n```\n\nAdd a few npm hooks.\n\n```sh\n# npm scope\nnpm hook add '@thi.ng' \"$WEBHOOKS_TARGET/npm\" $NPM_WEBHOOK_SECRET --userconfig .npmrc\n# npm username\nnpm hook add '~jackdbd' \"$WEBHOOKS_TARGET/npm\" $NPM_WEBHOOK_SECRET\n# npm package\nnpm hook add @11ty/eleventy \"$WEBHOOKS_TARGET/npm\" $NPM_WEBHOOK_SECRET\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/npm\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"foo\": 123, \"bar\": 456}' | jq\n```\n\n```sh\ncurl \"$WEBHOOKS_TARGET/npm\" \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-npm-signature: $NPM_WEBHOOK_SECRET\" \\\n  -d \"@./assets/webhook-events/npm/package-changed.json\" | jq\n```\n\n### WebPageTest pingbacks\n\nSee the [documentation on WebPageTest](https://docs.webpagetest.org/integrations/).\n\n```sh\ncurl \"$WEBHOOKS_TARGET/webpagetest?id=some-webpagetest-test-id\" \\\n  -X GET \\\n  -H \"Content-Type: application/json\"\n```\n\n## Troubleshooting webhooks\n\nAccess your Cloudflare Pages Functions logs by using the Cloudflare dashboard or the Wrangler CLI:\n\n```sh\nnpm run logs\n```\n\n[See the docs](https://developers.cloudflare.com/pages/platform/functions/debugging-and-logging/) for details.\n\n## Deploy\n\nI enabled automatic deployments, so the application is automatically deployed to Cloudflare Pages on each `git push` (`main` is the production branch, all other branches are `preview` branches).\n\nYou can also deploy manually using this command:\n\n```sh\nnpm run deploy\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackdbd%2Fwebhooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjackdbd%2Fwebhooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackdbd%2Fwebhooks/lists"}