Type a Generic Webhook Payload in TypeScript

Use json to typescript to design a reusable webhook envelope with id, event name discriminator, timestamp, and a generic data payload parameter.

Real-World API Schemas

Detailed Explanation

A Reusable Webhook Envelope

Webhook providers (Stripe, GitHub, Linear, Shopify, internal services) all follow the same shape: an id, an event name from a closed set, a timestamp, and a payload that depends on the event. A single generic interface captures the pattern.

Example JSON Samples

{
  "id": "evt_01H8X9",
  "event": "user.created",
  "created_at": "2024-03-15T10:30:00Z",
  "data": { "user_id": "u_001", "email": "alice@example.com" }
}
{
  "id": "evt_01H8YA",
  "event": "order.paid",
  "created_at": "2024-03-15T11:05:00Z",
  "data": { "order_id": "o_42", "amount_cents": 2500 }
}

Generated TypeScript

interface WebhookEnvelope<TEvent extends string, TData> {
  id: string;
  event: TEvent;
  created_at: string;     // ISO 8601
  data: TData;
}

interface UserCreatedData {
  user_id: string;
  email: string;
}

interface OrderPaidData {
  order_id: string;
  amount_cents: number;
}

type Webhook =
  | WebhookEnvelope<"user.created", UserCreatedData>
  | WebhookEnvelope<"order.paid", OrderPaidData>;

Why a Generic Envelope

Without the generic, you would either repeat { id, event, created_at } across every event type or lose the link between event and data. The generic envelope keeps the discriminator and payload paired:

function handle(w: Webhook) {
  switch (w.event) {
    case "user.created":
      return sendWelcomeEmail(w.data.email);   // data is UserCreatedData
    case "order.paid":
      return chargeShipping(w.data.order_id);  // data is OrderPaidData
  }
}

Tips for Generation

  • Start by collecting one JSON sample per event variant — never guess fields.
  • Keep event strings exactly as the provider sends them, including dots ("user.created"); do not transform them in types.
  • Treat created_at as a string, then parse to Date only when needed for display.
  • Add an exhaustive never check in the default branch so future events show up as compile errors instead of silent passes.

Use Case

Designing an internal webhook receiver that consumes events from three internal services and must dispatch typed handlers without manually unioning every payload shape.

Try It — JSON to TypeScript

Open full tool