CodeWorlds
Back to collections
Guide18 min readCodeWorlds Team

Hookdeck, an Event Gateway for Webhooks

Hookdeck queues webhooks, retries delivery and forwards them to localhost. The CLI 2.5.0 is alive, the SDK sits at 0.4.0 from 2024. Pricing and limits.

Hookdeck, an Event Gateway for Webhooks

Hookdeck is a hosted event gateway: it accepts webhooks from providers, queues them, retries delivery according to rules and stores the payload of every request for inspection. The service and the command line tool are actively developed, but the official language libraries have been frozen for two years, so the recommended integration path today runs through plain HTTP calls.

What Hookdeck gives you beyond your own endpoint

Receiving webhooks looks like one controller with one POST. The problems start later, and they are all of one kind: the provider takes no interest in the state your application is in.

The first is duplicates. Stripe, Shopify or GitHub resend the same event if you miss their time window, and you can miss it because of an unrelated database migration. The second is ordering: order.updated events can arrive before order.created because they took different network paths. The third is traffic spikes. A campaign generates a thousand webhooks a minute, your server handles a hundred, and after a run of errors the provider disables the endpoint. The fourth is signature verification, written separately per provider, each with a different header and digest scheme.

Hookdeck sits between the provider and your API. It accepts the request and answers the provider immediately, so the provider never sees your outage. The event lands in a queue and is delivered to the destination at a rate limit you set yourself. If the destination misses 60 seconds, returns a code outside 2xx or is unreachable, the retry rule kicks in. The request payload is stored for the retention period, so you can inspect it, replay it or send it again once the bug is fixed.

On top come things your own endpoint usually lacks: filtering before delivery, deduplication on selected payload fields, JavaScript transformations, and forwarding production traffic to localhost while you work on the code. That last feature is sometimes the only reason teams open an account at all.

Project state: the service is alive, the libraries are not

The gap between the pace of the service and the state of the libraries is stark here, and you need to know it before wiring Hookdeck into a product.

The command line tool is being developed. The npm package hookdeck-cli is at version 2.5.0 from 13 August 2026, the previous 2.4.0 shipped on 7 August, and the beta tag points at 3.0.0-beta.1 published on 21 August 2026. The repository hookdeck/hookdeck-cli answers with code 200 and does not redirect elsewhere. The service itself is up as well: hookdeck.com answers with code 200 and the footer reads Hookdeck Technologies Inc. with the year 2026.

The TypeScript library is a different story. The package @hookdeck/sdk sits at version 0.4.0 published on 21 August 2024, exactly two years ago. That is below one, so the authors declare no interface stability, and yet no release has followed. The repository is called hookdeck/hookdeck-typescript-sdk and it exists, while the name variants hookdeck-sdk-typescript and hookdeck-python-sdk return 404. The SDK code was generated by Fern, and its dependencies are pinned down to the patch: node-fetch 2.7.0, qs 6.11.2, form-data 4.0.0, js-base64 3.7.2, url-join 4.0.1. A two year old pin means no security fix in them reaches you without a manual override.

The package @hookdeck/vercel, the integration for the Vercel platform described in the article on Next.js, is at version 0.3.1 from 29 October 2024 and depends on @hookdeck/sdk in the range ^0.4.0. There is no range conflict: the published SDK fits the range the integration asks for. Both packages went quiet in the same period.

There is no Python client. The names hookdeck, hookdeck-sdk, hookdeck-python and hookdeck-cli all return 404 on PyPI. The vendor gives no separate announcement, but the documentation answers it: the navigation has no libraries section, the Python signature examples use hmac, hashlib and base64 from the standard library, and the API reference mentions generating an SDK for your language of choice from the OpenAPI schemas. The schema is public at https://api.hookdeck.com/2025-07-01/openapi, weighs about 470 kilobytes and describes 89 paths.

For the record: the same company maintains a second product, Outpost, for sending webhooks, and there the libraries are alive. The package @hookdeck/outpost-sdk is at version 1.5.0 from 24 July 2026, and a Python counterpart exists on PyPI. A Terraform provider also exists in the repository hookdeck/terraform-provider-hookdeck. So it is not that the company cannot maintain libraries. Frozen is specifically the Event Gateway client for TypeScript.

The practical conclusion: today you integrate with Hookdeck over REST and through the CLI, not through the SDK. If you reach for @hookdeck/sdk anyway, pin the exact version, audit its dependencies yourself and expect new API fields to be missing from it.

Licensing and what is actually inside the published packages

I checked the licence from three sources per package, because the registry declaration and the repository file do not always agree.

For hookdeck-cli all three sources agree. The npm license field says Apache-2.0, the repository LICENSE holds the full Apache License 2.0 text, and the unpacked 2.5.0 tarball holds a LICENSE with the same text. No spelling variants such as LICENSE.md or COPYING exist there, only LICENSE. The package contains real code: the bin/hookdeck.js wrapper and a binaries directory with prebuilt executables for six platforms, including darwin-arm64, linux-amd64 and win32-amd64. It has no runtime dependencies at all.

For @hookdeck/sdk there is a mismatch, though a mild one. In the npm metadata for 0.4.0 the license field does not exist, so a tool collecting dependency licences shows a blank or an unknown licence here. The unpacked tarball does contain a LICENSE with the Apache License 2.0 text, the same as in the repository. The content matches what the repository declares, only the registry field is missing. The code is there, with the api, core, serialization and webhooks directories and a reference.md. If you keep a dependency licence list at your company, this package has to be filled in by hand.

The package @hookdeck/vercel declares Apache-2.0 in the registry. The package @hookdeck/outpost-sdk repeats the pattern with the lost field: there is no licence in the npm metadata, even though the code comes from the hookdeck/outpost repository.

The open licence covers client tooling only. The gateway itself is a closed, vendor hosted service with no self run version. What is open is the separate Outpost product, which solves the opposite problem, sending webhooks to your own customers. If self hosting is a hard requirement for you, the Event Gateway is out regardless of the rest of the arguments.

The command line tool and working on localhost

This is the part people learn fastest. The CLI creates a permanent URL for a source and forwards traffic to your local port, without reconfiguring the provider on every run.

Code
Bash
# install via npm, Homebrew or Scoop
npm install hookdeck-cli -g

# log in, opens a browser; in a terminal without one add --interactive
hookdeck login

# listen on one source, traffic goes to http://localhost:3000/webhooks/shopify
hookdeck listen 3000 shopify --path /webhooks/shopify

# listen on every source at once, plain logs instead of the full interface
hookdeck listen 3000 '*' --output compact

# only events matching a filter on the request body
hookdeck listen 3000 stripe --filter-body '{"type":"payment_intent.succeeded"}'

The second argument is the source alias, and it accepts a comma separated list or '*'. The third, optional one picks a specific connection by name, alias or path, and if none exists it creates a new one. The default interactive mode is a full screen interface with an event list, where the r key retries the selected event, d shows the full request and response, and o opens it in the browser dashboard. The compact and quiet modes suit a process running in the background or inside a continuous integration pipeline. The --max-connections flag caps concurrent connections to the local server at 50 by default.

Per invocation authentication is handled by the --cli-key and --api-key flags. The first takes a key bound to a user and can see all of their projects, the second a key scoped to a single project. Without logging in, the CLI creates a guest account, convenient but harder to attach to the right organisation later.

The CLI can also manage resources, not just listen. The hookdeck gateway connection group creates and updates connections, hookdeck gateway transformation handles transformations, and hookdeck gateway mcp starts an MCP server letting an agent in your editor inspect events without switching to the browser. A Docker image exists too, where you must use host.docker.internal instead of localhost.

Connection rules, filters and signature verification

A connection binds a source to a destination and carries an array of rules. Rules come in five types: retry, filter, transform, delay and deduplicate. The call below creates a connection with a retry rule and a deduplication rule.

Code
Bash
curl -X PUT "https://api.hookdeck.com/2025-07-01/connections" \
  -H "Authorization: Bearer $HOOKDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "shopify-orders",
    "source": { "name": "shopify" },
    "destination": {
      "name": "orders-api",
      "config": {
        "url": "https://api.example.com/webhooks/shopify",
        "rate_limit": 100,
        "rate_limit_period": "minute"
      }
    },
    "rules": [
      {
        "type": "retry",
        "strategy": "exponential",
        "interval": 300000,
        "count": 5,
        "response_status_codes": [">=500", "!501"]
      },
      {
        "type": "deduplicate",
        "window": 60000,
        "include_fields": ["body.id", "body.order.number"]
      }
    ]
  }'

The strategy field takes linear or exponential. With a linear strategy and an interval of 600000 milliseconds the retries land at the tenth, twentieth and thirtieth minute after the failure. With an exponential one and the same interval they land at the tenth, twentieth and fortieth. The response_status_codes field takes ranges such as 500-599, comparisons such as >=500 and exclusions such as !501, and in case of a contradiction the last match wins. The hard limit is 50 automatic retries per event, while manual ones you can trigger as often as you like, which the retries documentation covers together with Retry-After handling. The deduplication window sits between 1000 and 3600000 milliseconds, and include_fields supports dot notation and array indices, for example body.items[*].sku.

Filters are a separate rule with a separate syntax. They take JSON matched against headers, body, query or path, with the operators $gte, $gt, $lt, $lte, $eq, $neq, $in, $nin, $startsWith, $endsWith, $or, $and, $ref, $exist and $not.

Code
JSON
{
  "type": "filter",
  "body": {
    "$or": [
      { "type": { "$startsWith": "payment_intent." } },
      { "data": { "object": { "amount": { "$gte": 10000 } } } }
    ]
  }
}

Filters were designed for JSON payloads. For XML only simple text matches will work, and the correct path runs through a transformation that converts the payload to JSON before the filter. A request rejected by a filter generates no event, so it is not billed as an event but as a discarded request, which matters for the invoice.

One thing stays on your side: checking that the request came from Hookdeck and not from the open internet. The default HOOKDECK_SIGNATURE mode adds an x-hookdeck-signature header with an HMAC SHA-256 digest of the raw body, base64 encoded. When a secret is rotated with a delay, an additional x-hookdeck-signature-2 carries the old key.

Code
JavaScript
import crypto from 'node:crypto'

export function isFromHookdeck(rawBody, headers) {
  const digest = crypto
    .createHmac('sha256', process.env.HOOKDECK_WEBHOOK_SECRET)
    .update(rawBody)
    .digest('base64')

  const primary = headers['x-hookdeck-signature']
  const secondary = headers['x-hookdeck-signature-2']

  return digest === primary || (Boolean(secondary) && digest === secondary)
}

The key detail: rawBody has to be exactly the byte string that arrived. A framework that parses the JSON and serialises it back will reorder keys or change whitespace, and the signature will stop matching. For destination authentication you can also pick BASIC_AUTH, API_KEY, BEARER_TOKEN, CUSTOM_SIGNATURE, AWS_SIGNATURE or one of two OAuth2 variants, if your API already has a mechanism of its own.

Pricing and the bill for a hundred thousand events

The billing unit is an event, meaning a message delivered to a destination. An inbound request and an event are two different things: one request can generate zero, one or many events, depending on the number of connections and on the filters. Retries are included in the price of an event, stated explicitly in the questions section of the pricing page.

As of 22 August 2026 the plans look like this: Developer at 0 dollars, Team from 39 dollars a month, Growth from 499 dollars a month, and Enterprise with a negotiated price. In all three non negotiated plans the included event allowance is the same, 10,000 per month, the included discarded requests are 100,000, and extra discarded requests cost 2.50 dollars per million. The differences sit elsewhere: retention is 3 days, 7 days and 30 days, the user count is one and then unlimited, and the 99.999 percent uptime SLA together with the 99.99 percent latency SLA only appears on the Growth plan. The static IP add on costs 100 dollars on every plan.

The bill for a hundred thousand events a month on the Team plan calls for caution, because the vendor quotes three different rates in the same place. The calculator headline speaks of a price "as low as 0.33 dollars per 100k". The static calculator markup, with the slider at 10,000, shows "1.00 dollars per additional 100k". The questions section, meanwhile, gives an explicit example: billing is quantised in blocks of 10 thousand, 10,001 events cost 0.30 dollars and 20,001 events cost 0.60 dollars. That third rate works out to 3.00 dollars per 100 thousand, nine times the first one. The calculator is a slider rendered in the browser, so the raw page source only yields the default state and I cannot establish at what volume the 0.33 rate starts to apply.

Counting with the only rate stated explicitly in the example: 100,000 events minus the 10,000 included leaves 90,000 additional events, which is 9 blocks of ten thousand, which is 2.70 dollars. Together with the subscription that comes to 41.70 dollars a month. At the calculator rate it would be about 39.90 dollars, and at the headline rate about 39.30. The spread is therefore narrow and at this volume the cost is on the order of forty dollars, but the discrepancy has to be confirmed with the vendor before signing for larger traffic, because at ten million events the difference between 0.33 and 3.00 per hundred thousand is the difference between 33 and 300 dollars of usage alone.

Then there is throughput. The price includes 5 events per second per destination in a project, which at an even spread gives 432,000 events a day and at a hundred thousand a month is no constraint at all. It becomes a constraint during spikes: anything above the ceiling is not rejected but queued, so you pay in latency rather than in lost events. Inbound requests are always accepted.

The free plan has one clause that is easy to miss. Past 10,000 events the events keep being accepted, but the dashboard is locked until you change plan or a new billing period starts, and in case of excessive overage the vendor reserves the right to stop processing. Three day retention on the free plan also means that after a weekend you will not replay Friday's events.

Hookdeck versus Inngest, Trigger.dev, Svix and Pipedream

These names appear in one sentence more often than they should, because they solve different problems.

ToolRoleEvent directionWhere your code runs
Hookdeck Event Gatewaygateway for inbound webhooksfrom outside into your APIon your side only
Svixoutbound webhook infrastructurefrom your API to customerson your side only
Inngestjob queue and event driven flowsinside your own systemfunctions on your side, orchestration at the vendor
Trigger.devlong running background jobsinside your own systemjobs on the vendor infrastructure
Pipedreamautomation with steps and integrationsfrom outside to outsidesteps at the vendor

Inngest and Trigger.dev are job queues. Their centre of gravity lies in what happens after an event is received: steps, resuming, delays, concurrency. Hookdeck sits one step earlier and does not run your logic, it only brings the traffic to it. These tools do not exclude each other: Hookdeck can accept a webhook and pass it to an endpoint that enqueues a job in Inngest.

Pipedream combines event reception with automation, much like the separately described Zapier, only with code instead of clicking. If, after receiving a webhook, you have to append a spreadsheet row and send a message, Pipedream will do the whole thing. If you have to hand the event to your own API with delivery guarantees, Pipedream is the long way round.

Svix is a direct competitor, but on the other side of the arrow: it builds infrastructure for sending webhooks to your customers. The Hookdeck counterpart to Svix is not the Event Gateway but Outpost. If you are looking for a sending tool, compare Svix with Outpost rather than with the gateway described here.

Common mistakes

The first is reaching for @hookdeck/sdk by reflex, because the package exists. Version 0.4.0 from August 2024 knows nothing about fields added to the API over two years, and its pinned dependencies drag in an old node-fetch. A plain fetch with the token in the Authorization header is shorter and more current today.

The second is parsing the body before verifying the signature. In Express that means keeping the raw body in a rawBody field, in Next.js reaching for await request.text() instead of await request.json(). The signature is computed over bytes, not over an object.

The third is treating retries as a guarantee that an event will be processed exactly once. It will not be. Deduplication in Hookdeck works in a window of up to an hour and over selected fields, and a retry two days later will arrive again. Idempotency on the receiving side, even through a unique index on the event identifier in PostgreSQL, remains your job.

The fourth is setting an exponential retry rule with a large interval without checking retention. Retries spread over a full day against the three day retention of the free plan leave a narrow window for a manual fix.

The fifth is wiring in a gateway where none is needed. With a few hundred events a day and a single provider, a table in the database, a status column and a cron job retrying failed rows settle the matter for free and without an external dependency. Hookdeck starts paying for itself when there are several providers, traffic comes in spikes, and someone has to be able to inspect a payload from three days ago without digging through server logs.

The sixth is underestimating vendor lock in. The source URLs belong to Hookdeck and are written into the configuration of every event provider. Leaving means changing the address in Stripe, Shopify and every other dashboard, and transformations written in JavaScript and stored on the vendor side have to be rewritten. The configuration can be exported through the API and kept in the Terraform provider, and that is a sensible way to cap the cost of leaving.

FAQ

Does Hookdeck have an official Python library?

No. No package exists on PyPI under the names hookdeck, hookdeck-sdk or hookdeck-python. The documentation shows examples built on hmac, hashlib and base64 from the standard library and points to generating a client from the OpenAPI schema served at the API address.

Is @hookdeck/sdk worth using in a new project?

In most cases no. The package sits at version 0.4.0 from 21 August 2024, is below one and carries two year old pinned dependencies. REST calls with the token in an Authorization: Bearer header give you access to the full, current API. The exception is the verifyWebhookSignature function from the webhooks module, but it comes down to a few lines of node:crypto that you can just as well write yourself.

Can Hookdeck be run on your own server?

The Event Gateway cannot. It is a closed service hosted by the vendor, and the Apache 2.0 licence covers only the command line tool and the client libraries. What is open is the separate Outpost product in the hookdeck/outpost repository, but it solves the opposite problem, sending webhooks to your own customers.

How much does a hundred thousand events a month cost?

On the Team plan roughly 41.70 dollars, counting 39 dollars of subscription plus 2.70 dollars for 90 thousand events above the allowance at the 0.30 dollars per 10 thousand rate given in the questions section. The vendor quotes two other rates in the same place, 1.00 and 0.33 dollars per 100 thousand, so at larger volumes confirm the price with them.

Does Hookdeck guarantee event ordering?

Not in the sense of strict global ordering. Queueing with a per destination rate limit smooths the traffic, but a retry of a failed event will arrive after later events. If ordering matters, you need a version or timestamp marker in the payload and a rule that discards states older than the one already stored.

When is your own queue enough instead of a gateway?

When you have one provider, a few hundred events a day and predictable traffic. An events table in the database, a status column, a unique index on the identifier and a cron job retrying failed rows are one evening of work. The case for a gateway appears with several providers, spiky traffic and a need for people outside the team to inspect payloads.

Read next

We use cookies to enhance your experience on the site