Why You Need Topics and Subscriptions When Sending Webhooks

Most webhook implementations start the same way. An event happens, you fire an HTTP request to every registered endpoint, and you move on. But, the moment your platform generates more than a handful of event types, "send everything everywhere" becomes a real problem.

Consumers who only care about payment.completed are suddenly drowning in inventory.adjusted and user.session.started events they'll never use. They're burning compute to receive, parse, and discard traffic that was never meant for them. Your infrastructure is burning resources to deliver it. And if a consumer's endpoint goes down under the load, you're now managing retries for events that endpoint was going to throw away anyway.

This is the problem that topics and topic subscriptions solve. They're a routing layer between your event producers and your consumers' destinations. They're a way to declare what kinds of events exist and let each consumer choose exactly which ones they want.

What topics actually are

A topic is a label that categorizes a webhook event by what happened. You'll also hear them called "event types," too (they're interchangeable). If your system publishes an event when an invoice is paid, the topic might be invoice.paid. When a customer updates their profile, it might be customer.updated.

The naming convention matters more than it might seem. Dot-notated strings following a resource.action pattern (like order.shipped, subscription.canceled, refund.created) give consumers a predictable, hierarchical vocabulary they can reason about. When a developer integrating with your platform sees invoice.payment_failed for the first time, they should be able to guess what it contains before reading a single line of documentation.

Topics serve a dual purpose. For you as the provider, they're a taxonomy of your platform's observable events — a way to communicate what your system can tell the outside world about. For your consumers, they're a filtering mechanism (a way to say "I care about this, not that.")

What topic subscriptions do for consumers

A topic subscription is the consumer's side of the equation. When a consumer registers a destination (a webhook endpoint, a message queue, a cloud event bus), they specify which topics that destination should receive. Everything else gets filtered out before delivery even happens.

This sounds simple, but the downstream effects are significant:

  • Noise reduction - A consumer building a billing integration doesn't need your user management events. Without topic subscriptions, they'd receive every event your system produces and implement their own filtering logic. This is all code they have to write, test, and maintain, that runs on their infrastructure, consuming their resources, for events they never wanted. Topic subscriptions move that filtering upstream, where it belongs.

  • Security - Every webhook delivery is data leaving your system. When you send an employee.salary.updated event to an endpoint that only processes invoice.paid, you're streaming sensitive data to a destination that has no business receiving it. Topic subscriptions enforce the principle of least privilege at the event delivery layer: each destination only sees the data it explicitly asked for.

  • Reliability - Fewer deliveries means fewer retries, fewer failure cascades, and less pressure on both your infrastructure and the consumer's. When a consumer's endpoint goes down, the blast radius is limited to the events that endpoint actually subscribed to (not your entire event catalog).

There's also a subtler operational benefit. Topic subscriptions give you, as the provider, a clear picture of what your consumers actually use. If you're publishing report.generated events but no destination has ever subscribed to them, that's signal. When you're planning deprecations, considering schema changes, or prioritizing which events to add next, subscription data tells you where the real demand is.

Implementing topics as a provider

Building a topic system involves three decisions: how you define the available topics, how consumers subscribe to them, and how you evaluate subscriptions at publish time.

Defining your topic catalog

Start by enumerating every event your system can produce. This becomes your topic catalog — the canonical list of event types consumers can subscribe to. Be deliberate about this. A topic catalog that grows ad-hoc, with inconsistent naming and overlapping semantics, creates confusion that compounds over time.

A few principles help keep the catalog clean. Use a consistent naming pattern: resource.action in past tense reads naturally and scales well (order.created, payment.refunded, user.deactivated). Group related topics by resource prefix so consumers can reason about the catalog hierarchically. And document each topic: what triggers it, what data it carries, and when consumers should expect to see it.

You can choose to enforce the catalog strictly (rejecting events published with unknown topics) or loosely (allowing any string as a topic). Strict enforcement catches typos and misconfigured publishers early. Loose enforcement gives you flexibility to add new topics without redeploying configuration. Most production systems land on strict enforcement because the cost of a stray ordr.created event silently going nowhere is higher than the cost of updating a configuration file.

Letting consumers subscribe

When a consumer registers a destination, they should be able to specify a list of topics. The API might look like this:

{
  "type": "webhook",
  "topics": ["invoice.paid", "invoice.payment_failed"],
  "config": {
    "url": "https://billing.example.com/webhooks"
  }
}

This destination will only receive events with a topic of invoice.paid or invoice.payment_failed. Everything else is filtered out.

You'll also want a wildcard option, or a way for consumers to say "send me everything." The convention is ["*"]. This is useful for consumers building general-purpose integrations, logging pipelines, or debugging setups where they need full visibility. But it should be an explicit choice, not the default.

A single tenant can (and often will) have multiple destinations with different topic subscriptions. A billing service subscribes to payment events. An analytics pipeline subscribes to everything. A Slack notification bot subscribes to deployment.failed. Each destination operates independently: its own subscriptions, its own delivery tracking, its own retry state.

Evaluating topics at publish time

When your application publishes an event, the delivery system needs to evaluate the event's topic against every destination's subscriptions to determine where the event should go. An event can match zero destinations, one destination, or many.

This fan-out evaluation is straightforward, but there's an optimization worth building early. In many applications, the majority of internal events won't match any destination subscriptions. If your system generates thousands of internal state changes per second but consumers have only subscribed to a dozen event types, you're publishing a lot of events that will immediately be discarded.

The fix is to expose the set of active topics (the union of all topics across all of a tenant's destinations) and let your application check it before publishing. If no destination has subscribed to cache.invalidated, your application can skip the publish call entirely. This avoids unnecessary network traffic, queue writes, and evaluation cycles.

The pattern looks like this: query the tenant's active topics (or cache them locally), check whether the event you're about to publish matches any of them, and only call the publish API if it does. For high-throughput systems, this check can eliminate the vast majority of unnecessary publishes.

How topics surface in the consumer experience

Topics aren't just a backend routing mechanism but a key part of the developer experience you expose to consumers.

When a consumer is setting up an integration in your portal or management UI, the topic catalog tells them what's available. A well-organized, clearly documented list of topics is the difference between a consumer who confidently configures their integration in minutes and one who opens a support ticket asking what events they should subscribe to.

Consider exposing your topic catalog through an API endpoint as well. This lets consumers programmatically discover available topics, which is particularly valuable for platforms that add new event types over time. A consumer's configuration UI can pull the latest topics dynamically rather than hardcoding a list that drifts out of date.

The portal experience should also make it easy to modify subscriptions after the initial setup. Consumers' needs change. A destination that started out subscribing to three topics might need ten. Making subscription management self-serve (rather than requiring an API call or a support request) reduces friction for everyone.

How Hookdeck Outpost handles topics

Outpost, Hookdeck's open-source outbound webhook solution, implements topic-based subscriptions as a core feature rather than an afterthought.

The available topics are configured through a TOPICS environment variable. You provide a comma-separated list of the event types your system supports, and Outpost enforces that list across the publish API, destination subscriptions, and the consumer-facing portal. If TOPICS isn't set, Outpost defaults to *, allowing any topic value.

Destinations subscribe to topics through the topics field when they're created via the API. A destination can list specific topics it wants, or use ["*"] to receive everything. When an event is published, Outpost evaluates its topic against every destination's subscriptions and only delivers to those that match.

Here's what creating a destination with specific topic subscriptions looks like:

curl --location 'localhost:3333/api/v1/tenants/<TENANT_ID>/destinations' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <API_KEY>' \
  --data '{
    "type": "webhook",
    "topics": ["order.created", "order.shipped"],
    "config": {
      "url": "https://example.com/webhooks"
    }
  }'

This destination will receive order.created and order.shipped events and nothing else. If another destination for the same tenant subscribes to ["*"], it will receive all events independently — each destination's delivery and retry state is tracked separately.

Outpost also exposes the tenant's active topics through the Tenant API. The tenant.topics array contains the union of all topics across all of the tenant's destinations. Your application can use this to evaluate whether an event is worth publishing before making the API call. If the topic isn't in the array, no destination will receive it, so you can skip the publish entirely. For high-volume systems, this optimization can significantly reduce unnecessary traffic.

The consumer-facing portal surfaces the topic catalog directly, letting your users see which topics are available and manage their destination subscriptions without needing to make raw API calls. This self-serve experience means fewer support tickets and faster integration setup.

Combined with Outpost's per-destination signing secrets, automatic retries, and support for non-webhook destinations (SQS, Pub/Sub, Kinesis, and others), topic subscriptions become part of a complete delivery pipeline — filtering events before they ever hit the wire, so consumers get exactly what they need and nothing more.

The cost of skipping this

It's tempting to defer topic subscriptions and just send everything to everyone. The implementation is simpler, the initial setup is faster, and when you only have three event types and a dozen consumers, the overhead is negligible.

But webhook systems have a way of growing faster than you expect. Three event types become thirty. A dozen consumers become hundreds. And suddenly you're delivering millions of events per day that nobody asked for, managing retry queues bloated with irrelevant traffic, and fielding complaints from consumers whose endpoints are buckling under the volume.

Adding topic subscriptions retroactively is painful. You need to define the catalog, migrate every existing destination to explicit subscriptions (because the current implicit behavior is "subscribe to everything"), coordinate with consumers who may have built logic assuming they receive all events, and do it all without disrupting active integrations. Every month you delay makes the migration harder.

Topics and subscriptions are the kind of infrastructure decision that costs almost nothing to make early and becomes exponentially more expensive to defer. If you're building a webhook system today, build them in from the start.