Designing Webhook Event Names: How to Pick Identifiers Customers Can Build On

Webhook event names are public API. Customers build switch statements and routing tables on them. The naming convention you pick on day one is the one you live with for years. There are a small number of patterns that age well and several that age badly.

Event names look like the most trivial part of a webhook API to design. They are short strings, they show up in a single field of the payload, and they seem like the kind of decision that can be made informally during the first sprint and adjusted later. They are also one of the most permanent parts of the API. Once customers have written code that branches on invoice.created versus invoice.updated, the names cannot change without breaking every integration. Choosing them well at the start is much cheaper than fixing them later.

What event names have to do

An event name has three jobs. It tells the receiver what kind of thing happened. It distinguishes between events that share a resource but differ in lifecycle (created vs updated vs deleted). And it provides enough structure that receivers can subscribe to or filter on subsets of events without enumerating every type by hand.

The first two jobs are usually obvious. The third is the one most teams get wrong, because it requires thinking about the customer's switch statement and routing infrastructure, not just the API's internal model.

The dotted-namespace convention

The dominant convention across most modern webhook APIs is resource.verb: invoice.created, customer.updated, subscription.canceled. Stripe uses it. GitHub uses a variant (issues.opened, where the resource is plural). Shopify, Twilio, Linear, Vercel, and most others converge on the same shape.

The convention has three properties worth keeping. The resource comes first because customers usually route by resource (their handler for customer events is separate from their handler for invoice events). The verb comes second because once they have routed to the resource, they branch on the lifecycle action. The dot is a stable delimiter that allows prefix matching (customer.*) and exact matching (customer.created) in the same scheme.

The convention has trade-offs. Deeper nesting (customer.subscription.payment_method.updated) is allowed but accumulates noise; most APIs cap at two levels. Plural-vs-singular is a stylistic choice that should be consistent within an API; mixing invoice.created and customers.created is the kind of inconsistency customers will complain about for years.

Verb tense and naming

The verbs in event names should be in past tense. customer.created describes a fact that has happened; customer.create sounds like an imperative. The past-tense form makes the semantics of webhooks (events that happened, not commands you are issuing) explicit in the name.

The verb should describe the state transition from the customer's perspective, not the implementation. customer.created not customer.inserted. subscription.canceled not subscription.row_updated_with_canceled_flag. The customer does not care that your implementation uses soft deletes; they care that the lifecycle event was a cancellation.

Synonyms should be consolidated. Pick deleted or removed or destroyed and use it consistently across resources. Pick updated or modified or changed and use it consistently. Customers reading your event reference should be able to predict the verb for a new resource based on the verbs for existing resources.

Versioning the names

Event names are part of the API surface and need a versioning story. The two common approaches are embedding the version in the event name (customer.created.v2) and embedding the version in the subscription or in the payload (customer.created with a version field). The second approach scales better; the first means that every version bump renames every event and forces customer code changes.

The Stripe approach (and the one we have copied at WebhookVault) is that event names are stable across the API version. The schema of the payload varies by API version, and the customer pins to a version when they create the webhook endpoint. The event name invoice.created means the same thing in 2024 and 2027, but the payload shape may have evolved. This is the version-the-schema-not-the-name pattern; it keeps the customer's switch statements stable across years.

The case where event names do legitimately change is when the semantic content of the event changes. If the old event fired on row insert and the new event fires on a state machine transition that does not correspond 1:1 to row insert, the new event needs a new name. The old name continues to fire on its old semantics during the deprecation window, and customers migrate when they are ready.

The completeness question

The harder design question is which events to expose at all. Every internal state transition is a candidate event. Most should not be exposed; the API surface gets large fast, and the value of exposing internal mechanism is low.

The rule of thumb is: expose events that correspond to state changes the customer cares about, not events that correspond to internal implementation details. invoice.paid is a customer-facing state; invoice.payment_method_charge_attempted is implementation detail. subscription.renewed is a customer-facing state; subscription.renewal_attempt_started is implementation. The narrower set ages better because it is anchored to the customer's domain model, not the API's internal architecture.

The completeness question also includes whether to fire events for partial state changes. If a customer record has many fields and one of them changes, is that one customer.updated event or several field-specific events? The dominant answer is one customer.updated event with the changed fields listed in the payload. Field-specific events (customer.email_changed) get expensive in payload schema and customer switch statements, and they are usually not needed.

The customer subscription surface

The event names interact with the customer-facing subscription dashboard. Customers want to opt into specific event types, not the whole firehose. The dashboard typically shows a checkbox list grouped by resource: a Customer Events section with checkboxes for created, updated, deleted, and a Subscription Events section with checkboxes for created, renewed, canceled. The dotted-namespace convention makes this grouping mechanical.

Wildcards are the customer-side feature that most teams ask for and that most webhook providers do not offer. customer.* subscriptions look convenient, but they make the addition of new event types a breaking-by-default change rather than an opt-in. The pattern most providers settle on is: explicit subscriptions to event types, plus an optional opt-in to receive new event types as they are added. The default-off behavior keeps customer code from breaking when a new event type is added.

What this looks like across our four products

Our four products use the dotted-namespace convention. CronPing emits monitor.created, monitor.ping_received, monitor.missed, monitor.recovered. FlagBit emits flag.created, flag.updated, flag.rule_added, flag.rollout_changed. DocuMint emits invoice.generated, usage.threshold_reached, subscription.updated. WebhookVault emits endpoint.created, endpoint.received_request, endpoint.expired.

The verbs are consistent across products (every product uses created, updated, and the resource-specific lifecycle verbs). The plural-vs-singular choice (singular resource) is consistent. The version-the-schema-not-the-name policy applies across all four. The new-event-types-are-opt-in policy is consistent across the dashboards.

Three observations

First: the dotted-namespace convention has won decisively across the webhook ecosystem. New webhook APIs that adopt a different convention pay a cost in customer cognitive overhead because customers have to learn the local convention; new APIs that adopt the dominant convention inherit the customer's existing mental model for free. Picking the convention everyone else uses is the right default unless there is a specific reason to deviate.

Second: the choice of whether to expose an event is more important than the choice of what to name it. A well-named event for an internal implementation detail is still noise on the customer's bus. A poorly-named event for a customer-meaningful state transition is still useful, even if customers grumble about the name. The completeness question deserves more thought than most teams give it.

Third: event names are one of the cleanest examples of the asymmetry between provider cost and customer cost. The provider can rename an event in five minutes; the customer has to find every receiver in their codebase, update each one, redeploy, and verify. The customer cost is 100x the provider cost, which means the provider's choice of stability standard matters disproportionately. Treating event names as public-API-frozen-by-default is the rule that respects this asymmetry.

The deeper observation is that webhook event names are one of the longest-lived parts of an API. Customer dashboards, switch statements, routing tables, log filters, alerting rules, and analytics queries are all built on top of the event-name vocabulary. The vocabulary persists across customer code rewrites, framework changes, and team turnover. The names you pick at API launch will outlive your product roadmap, your tech stack, and possibly your company. The investment of an extra week of thinking before launch about whether the names express the right model, in the right grammar, with the right verbs, is one of the highest-leverage investments in customer-facing API design. The cost of getting it wrong compounds across every customer integration for the life of the product.

Read more