Designing API Webhook Payloads: Snapshots vs References and the Right Default for B2B SaaS

Webhook payloads come in two flavors: a complete snapshot of the resource at the moment the event fired, or a reference customers must follow with an API call to get the current state. The choice has consequences that scale with the volume and value of the integration.

Webhook payloads sit on a spectrum from minimal references (event_id and resource_id, customer fetches the rest) to complete snapshots (every field of the resource embedded in the body). The choice is one of the design decisions that shapes how customers integrate with your API, how much customer-side code they have to write, how reliably their integrations work, and how much bandwidth your platform burns through. Most teams pick one approach reflexively from how their first reference customer wanted to integrate, and then live with the consequences.

The right default for B2B SaaS is closer to the snapshot end than most teams instinctively pick, and the reasons matter.

What a reference-style payload looks like

The minimal version is something like:

{
  "event_id": "evt_abc123",
  "event_type": "invoice.paid",
  "created_at": "2026-05-28T14:30:00Z",
  "resource": {
    "type": "invoice",
    "id": "inv_xyz789",
    "url": "https://api.example.com/v1/invoices/inv_xyz789"
  }
}

The body is small (under 1KB), the schema is stable, the customer has minimal parsing to do, and the canonical state of the resource is whatever the API returns when the customer GETs the URL. The customer-side integration is two steps: receive webhook, fetch resource.

What a snapshot-style payload looks like

The full version includes the resource state at the moment the event fired:

{
  "event_id": "evt_abc123",
  "event_type": "invoice.paid",
  "created_at": "2026-05-28T14:30:00Z",
  "data": {
    "type": "invoice",
    "id": "inv_xyz789",
    "amount": 1500,
    "currency": "usd",
    "customer_id": "cus_def456",
    "status": "paid",
    "paid_at": "2026-05-28T14:29:55Z",
    "line_items": [...],
    "metadata": {...}
  }
}

The body is substantially larger (5-50KB depending on resource), the schema needs versioning because every field is an integration surface, and the customer can act on the event without making an additional API call. The customer-side integration is one step: receive webhook, process.

The arguments for references

The reference style minimizes bandwidth, keeps the payload schema stable, avoids stale-state problems because the customer always fetches current state, and makes payloads predictable in size. Reference payloads are easier to sign and harder to inadvertently include sensitive fields in, because the signature is computed over a smaller and more constrained body.

The reference style also avoids the question of when to send updates. If the customer always fetches current state when an event arrives, then sending an event is a notification rather than a data delivery, and the platform does not have to make decisions about which state version to embed.

The arguments for snapshots

The snapshot style works when the customer cannot fetch the resource. The most common case is webhook handlers running in environments without outbound HTTP access to your API, which is more common than platform teams expect: serverless functions with restrictive egress, scripts in private VPCs, integrations through cloud workflow engines. Reference-style payloads make these integrations harder than they need to be.

The snapshot style also captures historical state at the time of the event, which is what most customer-side reporting and audit needs. If the customer GETs the resource three days after the event arrives, the resource state may have changed in ways unrelated to the original event, and the audit log records the wrong state. The snapshot is the canonical answer to "what was true when this event fired."

The snapshot style avoids the rate-limit interaction where high-volume webhook delivery forces customers to make matching high-volume API calls, which then hit rate limits that did not apply to the webhook delivery itself. Stripe famously had to add separate higher rate limits for webhook-driven API access for exactly this reason.

The snapshot style also produces a better failure mode under partial-outage conditions. If your API is degraded but the webhook delivery infrastructure is healthy, customers with snapshot payloads continue to process events successfully while customers with reference payloads cannot. The decoupling is operational, not just a developer-experience preference.

The hybrid pattern

The pattern most B2B SaaS APIs end up at is a partial snapshot: include the fields customers most commonly need plus a reference URL for full resource state. The Stripe convention of including the entire resource snapshot in the data.object field is closer to full snapshot, and GitHub's convention of including a substantial subset plus links is closer to partial snapshot. Both work; the choice is mostly about how predictable the customer's needs are.

The implementation pattern is to define a webhook-specific projection of the resource (which may not be identical to the API resource representation) that includes the fields needed for common customer workflows. The projection is versioned with the same discipline as the API, but the schema can evolve somewhat independently to optimize for webhook ergonomics.

The deletion case

Deletions break the reference pattern, because the customer cannot GET a deleted resource. Webhook payloads for deletion events have to include enough resource state to be useful, which means deletion webhooks are partial snapshots even in otherwise reference-style designs. The asymmetry is one of the reasons most APIs end up with hybrid payloads.

The delete-event payload should include the resource ID, the type, any metadata needed to identify which customer-side records correspond to the deleted resource, and a timestamp. Including the full resource snapshot in the deletion event is good practice; it lets customer-side reconciliation queries work without race conditions.

The schema versioning question

Snapshot payloads have larger schema surfaces, which means more potential for breaking changes when the resource model evolves. The versioning discipline has to apply to webhook payloads with the same rigor as the API surface: deprecation notices, parallel versions during transition, sunset windows.

The right pattern is to version webhook event types using the same versioning scheme as the API, and to allow customers to subscribe to specific versions if their integration depends on a stable payload shape. Stripe does this with API version pinning at the subscription level, which gives customers control over when they migrate.

Reference payloads have smaller schema surfaces but push the versioning problem to the API. Customers fetching the resource via the URL get whatever schema version their API call resolves to, which may not be the version they expect if they have not pinned their API requests. The complexity moves rather than disappearing.

Three patterns that fail

First, the inconsistent mix where some event types are snapshots and others are references with no clear rule. Customers cannot build reliable integration code against an API where they have to remember which event types need a follow-up API call and which do not. The pattern usually arises from individual engineers picking the approach that was easiest for the specific event type they were building, without anyone consolidating across event types.

Second, the snapshot-with-missing-fields pattern where the snapshot payload includes most but not all fields of the resource. Customers cannot tell which fields are missing because they were not relevant to the event vs. which fields are missing because the platform forgot to include them. The right answer is either complete snapshots or explicitly documented partial snapshots with a clear principle for which fields are included.

Third, the reference-payload-with-stale-URL pattern where the resource URL in the payload returns 404 because the resource was deleted between event delivery and customer fetch. The pattern is most painful for high-volume integrations where the timing race is non-trivial. The fix is to send delete events with full resource snapshots so the reference pattern only applies to currently-existing resources.

Our use across DocuMint, CronPing, FlagBit, and WebhookVault

DocuMint outbound webhooks (when implemented) are snapshot-style because invoice events are inherently historical and customers need the invoice state at the moment of the event, not whatever the current state is when they check.

CronPing monitor-state webhooks are snapshot-style because the alert-handler workflows customers build need the state transition information without making a follow-up API call. The payload includes monitor metadata, current state, last successful ping timestamp, and missed-ping count.

FlagBit flag-update webhooks are snapshot-style because customers use them to invalidate caches of flag values, and the snapshot lets them update their local state without an additional API call. The full flag rule payload is included.

WebhookVault as a webhook capture product is not in the same position as the other three; the captured webhooks are themselves the payload, and WebhookVault simply forwards them with metadata about source, signature verification status, and replay history.

The pattern across our four products is that snapshot-style payloads have been the right default for the integration scenarios we have observed, with the cost being larger payloads (5-30KB typical) and a more elaborate schema versioning discipline.

The deeper observation

The choice between snapshot and reference is structurally similar to the choice between event sourcing and current-state storage in application architecture. The snapshot pattern is event-sourcing-shaped: the webhook is the canonical record of what happened, the customer can rebuild state from the stream, and the API resource is a derived view that may have moved on. The reference pattern is current-state-shaped: the webhook is a notification that something changed, the customer always queries the canonical state, and the historical record is whatever logs the customer maintains separately.

Most B2B SaaS use cases are closer to the event-sourcing shape than teams initially recognize. Customers want to know what happened, when, and what state was at that moment. The snapshot payload gives them that directly. The reference payload gives them a notification and shifts the work of capturing state to a separate query, which works fine when the customer is small and breaks down at scale where the rate-limit interactions matter and the timing races become reliability problems.

The webhook payload format is one of the few API decisions that genuinely benefits from copying what Stripe does: payload-style snapshots, event versioning, customer-controlled migration windows, and full snapshots on deletions. The reasons Stripe converged on these patterns are the same reasons most B2B SaaS APIs eventually do, and the migration cost from reference to snapshot is much higher than picking the right default at the beginning.


Our products: DocuMint (PDF invoice generation API), CronPing (cron job monitoring with status pages), FlagBit (feature flags API for modern teams), and WebhookVault (webhook capture and replay) put these patterns into production.

Read more