Envelope vs Payload: What Carrier Webhooks Actually Send

Envelope vs payload, defined and worked through a real carrier tracking webhook, so you can design schemas that route and version cleanly.

Envelope vs Payload: What Carrier Webhooks Actually Send

Envelope vs Payload: The One-Sentence Definition

In a carrier integration message, the envelope is the routing and transport metadata wrapped around an event (event ID, type, timestamp, tenant reference, signature), and the payload is the domain data it carries (the tracker, the shipment, the label). The envelope tells your infrastructure what to do with a message before anything has to look inside it; the payload tells your business logic what actually happened.

This sounds trivial until you build multi-tenant carrier middleware and watch a router try to deserialise fifteen different carrier-specific JSON shapes just to work out which tenant a webhook belongs to. Get the envelope vs payload split wrong, and every downstream service ends up coupled to every carrier's schema.

Where This Split Comes From

The envelope/payload pattern isn't a carrier-industry invention. It's the standard shape for any pub/sub or event-driven system, formalised most clearly by the CNCF's CloudEvents specification. CloudEvents defines a fixed set of context attributes that every CloudEvent conforming to this specification MUST include, and MAY include one or more OPTIONAL context attributes and MAY include one or more extension attributes. Crucially, these attributes are designed so they can be serialized independent of the event data, allowing them to be inspected at the destination without having to deserialize the event data.

That's the whole point of an envelope: a router, a dead-letter queue, or a rate limiter can act on it without trusting or even parsing the payload's shape. CloudEvents requires four attributes: id, source, type, and specversion, plus optional ones like time and subject. The spec is explicit that producers MUST ensure that source + id is unique for each distinct event, and consumers MAY assume that events with identical source and id are duplicates. That rule alone is why idempotency keys belong in the envelope, not buried in a carrier's tracking payload.

If you want an analogy that sticks for a logistics audience: the envelope is the shipping label, the payload is what's inside the box. A sorting hub reads the label to route the parcel. It never opens the box to decide which conveyor belt it goes on.

What Envelope vs Payload Is Not

Three things people conflate with this split, worth separating cleanly:

  • It isn't HTTP headers vs body. Headers are transport-level and often stripped or altered when a message is replayed through a queue, a webhook relay, or a dead-letter store. An application-level envelope travels inside the JSON body itself, so it survives re-transport intact. CloudEvents supports both a "binary mode" (attributes as HTTP headers) and a "structured mode" (attributes and data together in one JSON body) precisely because you need the envelope to survive both paths.
  • It isn't the fat-vs-thin payload debate. That's a separate, orthogonal decision about how much domain data to put inside the payload. Hookdeck frames it as three fundamental approaches: fat payloads that include everything, thin events that include almost nothing, and update payloads that include what changed. Stripe's newer webhook design leans thin: thin events provide a lightweight, version-stable alternative to snapshot events, where instead of receiving full resource objects in webhook payloads, you receive compact notifications and fetch the details you need. Whether you go fat or thin, the envelope fields stay the same.
  • It isn't your versioning scheme. The envelope carries a pointer to the version (a field like api_version or a topic suffix like .v2), but the envelope pattern itself doesn't tell you how to version. Those are two different design decisions that happen to share a field.

A Worked Example: EasyPost's Tracking Webhook

EasyPost's tracking webhook is a clean, real-world illustration. Every tracking update arrives as an Event object, and tracking updates always use object: "Event", description: "tracker.updated", result: a nested Tracker object containing the latest status and details. Here, object, id, description, mode, and created_at form the envelope. The nested result object is the payload, an actual EasyPost Tracker resource with carrier, tracking code, and scan history.

{
  "id": "evt_qatAiJDM",
  "object": "Event",
  "created_at": "2014-11-19T10:51:54Z",
  "description": "tracker.updated",
  "mode": "test",
  "previous_attributes": { "status": "unknown" },
  "result": {
    "id": "trk_Txyy1vaM",
    "object": "Tracker",
    "status": "delivered",
    "tracking_code": "EZ4000000004",
    "carrier": "UPS"
  }
}

EasyPost even guarantees at-least-once delivery, which is exactly why the envelope's id field matters: every time an Event related to one of your objects is created, EasyPost guarantees at least one POST request will be sent to each of the webhook URLs set up for your account, and for this reason, they strongly encourage your webhook handler to be idempotent. Deduplicate on evt_qatAiJDM, the envelope's event ID, never on some combination of payload fields like tracking code and status, which can legitimately repeat.

Now compare that with a CloudEvents-shaped version of the same "package delivered" event, and with Stripe's convention of pinning a version directly in the envelope:

// CloudEvents shape
{
  "id": "evt_qatAiJDM",
  "source": "/carriers/ups/trackers",
  "type": "com.easypost.tracker.updated",
  "specversion": "1.0",
  "time": "2014-11-19T10:51:54Z",
  "data": { "status": "delivered", "tracking_code": "EZ4000000004" }
}

// Stripe-style shape
{
  "id": "evt_1abc2def",
  "api_version": "2026-01-28",
  "type": "invoice.paid",
  "data": { "object": { "id": "inv_4k9x7m", "amount_paid": 4999 } }
}

Three different vendors, three different field names, the same structural idea: the consumer subscribes at a specific API version, and all webhooks they receive use that version's payload schema until they explicitly upgrade, with Stripe as the canonical example where each account is pinned to an API version identified by a date.

Why Multi-Tenant Carrier Middleware Needs This Split

In a platform routing webhooks for 1,000+ shippers across UPS, DHL, PostNL and FedEx, the tenant ID, correlation ID, and idempotency key have to live in the envelope. That's what lets a router, a dead-letter queue, or a rate limiter act on a message without deserialising or trusting the carrier-specific payload shape underneath. A DLQ that has to parse ten different carrier payload formats just to decide whether to retry is a DLQ that will eventually retry the wrong thing.

Multi-carrier platforms like Cargoson, alongside nShift, ShipEngine and EasyPost, all face the same underlying problem: dozens of carrier-specific payload shapes have to be normalised into one consistent envelope before anything downstream, billing, notifications, exception handling, gets to touch the data. The carrier's payload stays messy and carrier-specific. The envelope is where you buy back consistency.

Common Mistakes

  • Tenant or auth data buried in the payload. If tenant identity only exists inside a carrier-specific nested object, your router has to understand every carrier's schema just to know whose queue a message belongs in.
  • Deduplicating on payload business fields instead of the envelope's event ID. Tracking code plus status is not a safe dedup key, it repeats legitimately. The envelope's id, unique per producers MUST ensure that source + id is unique for each distinct event, is the only field designed for this.
  • No version field in the envelope at all. Without one, consumers are forced to sniff the payload shape to guess what schema they're looking at. Hookdeck's guidance is blunt on this: if you don't have a versioning strategy from day one, you'll break consumer integrations the first time you ship a schema change, and those consumers won't be gentle about it.

FAQ

Is envelope vs payload the same as header vs body?
No. Headers are transport-level and can be dropped or rewritten when a message is replayed through a queue or relay. An envelope is application-level JSON that survives replay, retries, and re-transport intact.

Do I need CloudEvents to do this properly?
No. CloudEvents is a convention, not a requirement. Its own spec only mandates context attributes designated as REQUIRED, four fields, and leaves everything else optional. You can build a perfectly sound envelope with your own field names, as EasyPost and Stripe both do.

Where does the idempotency key live, envelope or payload?
Envelope, alongside the event ID. That's the field guaranteed unique per event, and the one EasyPost strongly encourage your webhook handler to be idempotent against.

How does this interact with schema versioning?
The envelope carries the version pointer, it doesn't define the versioning strategy itself. Stripe's approach puts "api_version": "2026-01-28" directly in the envelope alongside id and type, so a consumer can branch on schema version before it even touches the nested payload.

If you're designing a new carrier webhook consumer, start by writing down your envelope fields on paper before you write a line of payload-parsing code. If you can't route, dedupe, and version a message using only the envelope, the split isn't done yet.

Read more

Real-Time SLO Monitoring for Carrier Integration: Predictive Error Budget Alerting That Detects API Failures 30 Minutes Before SLA Breaches

Real-Time SLO Monitoring for Carrier Integration: Predictive Error Budget Alerting That Detects API Failures 30 Minutes Before SLA Breaches

You can build comprehensive SLO monitoring that catches carrier API failures 30 minutes before your customers notice anything wrong. But the approach most teams take—watching uptime percentages and setting static error rate thresholds—catches problems too late. Between Q1 2024 and Q1 2025, average API uptime fell from 99.

By Koen M. Vermeulen
Microservice Decomposition for Carrier Integration Platforms: Bounded Context Patterns That Prevent Multi-Tenant Coupling Disasters

Microservice Decomposition for Carrier Integration Platforms: Bounded Context Patterns That Prevent Multi-Tenant Coupling Disasters

Decomposing a monolithic carrier integration platform into microservices sounds straightforward until you face the reality: breaking down a giant, tightly coupled monolith into more minor, independent microservices requires careful planning and a deep understanding of the existing system's functionality. The challenge becomes exponentially harder when you're

By Koen M. Vermeulen
Concurrent Carrier Migration Architecture: Coordinating USPS, FedEx, and UPS API Transitions Without Breaking Multi-Tenant Shipment Processing

Concurrent Carrier Migration Architecture: Coordinating USPS, FedEx, and UPS API Transitions Without Breaking Multi-Tenant Shipment Processing

Every integration team thinks they've mastered carrier APIs. USPS Web Tools shut down on January 25, 2026, and FedEx SOAP endpoints retire on June 1, 2026. For the first time in shipping software history, enterprise platforms face three simultaneous carrier migrations with compressed timelines and fundamentally different architectural

By Koen M. Vermeulen