Skip to content

The Contract and the Envelope for Events

Module 8 of 8 in the chapter Events, chapter 11 of the Integration Architect course. Each module stands on its own. Previous: Beyond the Bus · Back to the chapter: Events

The question this module answers: How do you write down what an event promises, so every subscriber can build to it?

Every important arrow gets a contract. The Data Architect course built one line by line: who starts it, which way, how often, the keys, the fields, what happens on failure (Every Important Arrow Gets a Contract). Chapter 3 read the technical half of a REST contract, the OpenAPI file (The Contract).

An event arrow needs the same. Every subscriber to Delivery Update has to know: where do I connect, what channel, what’s in the event, which fields are always there, what the status values mean. If it isn’t written down, every subscriber team finds out by reading someone’s code, and they each find out something slightly different. And this needs to be decided in the first week or two of the project, while you’re in design, not after three teams have built to three guesses.

When I teach REST, I say the server defines the contract. It decides the protocol and what the payload will be, and every client dances to it. That’s the client-server tango.

Events are a little different, and module 1 said why: the arrow is reversed. The subscriber dials in, and the publisher sends. So who writes the contract?

Who writes the contract Who writes the contract: two rows. Top row: a client calls the server, as the contract says; the server, highlighted, writes the contract, an OpenAPI file. Bottom row: a subscriber subscribes to the publisher and listens, as the contract says; the publisher, highlighted, writes the contract, an AsyncAPI file. The legend: highlighted writes the contract, the side everyone else connects to. Underneath: chapter 3 read the server's contract; with events, the publisher's contract says what it sends, and the subscribers build to it. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.58.0", "built": "2026-11-03", "id": "DGM-INT-EV-15", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.5.0", "theme": "stevetecharc 1.4.0"} A client calls, as the contract says The server writes the contract: an OpenAPI file A subscriber listens, as the contract says The publisher writes the contract: an AsyncAPI file calls subscribes Chapter 3 read the server's contract. With events, the publisher's contract says what it sends, and the subscribers build to it. HOW TO READ IT Writes the contract: the side everyone else connects to © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

The side everyone else connects to writes the contract: the server, or the publisher.

The publisher. It’s the side everyone else connects to, just as the server is with REST. One event, many subscribers: the publisher says what it sends, and the subscribers build to it. At Steve Trucking, Salesforce publishes Delivery Update, so the Salesforce team owns its contract, and the integration layer, Snowflake’s feed and anyone who comes later build to it.

Two open standards cover the two halves, and they’re easy to blur, so here they are side by side.

The contract and the envelope The contract and the envelope: two equal panels. AsyncAPI, the contract, written once, before anything is sent: where to connect, the servers; which channels, and what each message holds; who sends and who receives; OpenAPI's twin, for events. CloudEvents, the envelope, the label on every event: an id, and the source it came from; its type, and the spec version; optional, the time, the subject, the content type; the same label on any bus or cloud. Underneath: the contract says what will be sent; the envelope says what this one is. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.58.0", "built": "2026-11-03", "id": "DGM-INT-EV-16", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "comparison/side-by-side 0.5.0", "theme": "stevetecharc 1.4.0"} AsyncAPI: the contract written once, before anything is sent Where to connect: the servers. Which channels, and what each message holds. Who sends and who receives. OpenAPI's twin, for events. CloudEvents: the envelope the label on every event An id, and the source it came from. Its type, and the spec version. Optional: the time, the subject, the content type. The same label on any bus or cloud. The contract says what will be sent. The envelope says what this one is. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

The contract says what will be sent. The envelope says what this one is.

Think of the event as a box, the way chapter 4 did. AsyncAPI is the agreement about the boxes: where they’re shipped from, what’s inside each kind, who sends and who receives. You write it once. CloudEvents is the label stuck on each box: what it is, where it came from, its own ID. Every box gets one.

AsyncAPI is “a project used to describe message-driven APIs in a machine-readable format. It’s protocol-agnostic,” and its own specification credits “the great work done by the folks at the OpenAPI Initiative” (AsyncAPI Specification 3.1.0 (opens in a new tab), checked 2026-11-07). If you can read an OpenAPI file, you can read this one. The current version, 3.1.0, came out on January 31, 2026, with no breaking changes from 3.0 (AsyncAPI Spec 3.1.0 Release Notes (opens in a new tab), checked 2026-11-07).

Here’s Steve Trucking’s Delivery Update, written as an AsyncAPI file. It’s our own example, built from the event chapter 4 defined:

asyncapi: 3.1.0
info:
title: Steve Trucking Delivery Updates
version: 1.0.0
description: >
Salesforce publishes one Delivery Update each time a delivery's status
changes, after the change is saved (Publish After Commit).
contact:
name: Steve Trucking Salesforce team
servers:
salesforce:
host: api.pubsub.salesforce.com:7443
protocol: grpc
description: >
Salesforce's Pub/Sub API. Subscribers send the accesstoken,
instanceurl and tenantid headers, and decode each event with Avro.
channels:
deliveryUpdates:
address: /event/Delivery_Update__e
description: Kept on the event bus for 72 hours.
messages:
deliveryUpdate:
$ref: '#/components/messages/DeliveryUpdate'
operations:
sendDeliveryUpdate:
action: send
channel:
$ref: '#/channels/deliveryUpdates'
messages:
- $ref: '#/channels/deliveryUpdates/messages/deliveryUpdate'
components:
messages:
DeliveryUpdate:
title: Delivery Update
summary: A delivery's status changed.
payload:
type: object
required: [EventUuid, Delivery_Number__c, Status__c]
properties:
EventUuid:
type: string
description: The event's own ID. A repeat has the same one.
CreatedDate:
type: string
format: date-time
Delivery_Number__c:
type: string
description: The delivery's number, as the partners know it.
Status__c:
type: string
description: The new status.
New_ETA__c:
type: string
format: date-time
description: Empty unless the ETA changed.

Read it top to bottom, the way chapter 3 read the OpenAPI file:

  • info: what this is, its version, and who owns it. The owner line matters more than it looks: it’s who a subscriber team calls.
  • servers: where to connect. Here, the Pub/Sub API’s endpoint from module 2. AsyncAPI lets you name any protocol, so grpc is fine.
  • channels: where the events are. A channel’s address is “typically the ‘topic name’”: for us, /event/Delivery_Update__e.
  • operations: who does what. action: send says this application, Salesforce, sends on that channel. A subscriber’s own AsyncAPI file would say receive.
  • components: what’s in the box. The payload lists every field, which ones are always there, and what each one means.

Notice what it says in plain words that the code never would: Publish After Commit, kept for 72 hours, a repeat has the same ID. Those are the promises from modules 3 and 4, written where every subscriber will find them.

Salesforce already keeps the technical half for you. Through the Pub/Sub API, every event’s payload is encoded with Apache Avro, and the schema comes from Salesforce: “Pub/Sub API publishes and delivers binary-encoded events with payloads encoded in the Apache Avro schema” (Event Data Serialization with Apache Avro (opens in a new tab), Pub/Sub API Developer Guide, checked 2026-11-07). A subscriber asks for it with GetSchema.

And the schema has an ID that changes when the event changes: “If the event schema changes, for example, when an administrator adds a field to the event definition, the schema ID changes” (GetSchema RPC Method (opens in a new tab), checked 2026-11-07). Each event carries the ID of the schema it was written with, so a well-built subscriber notices and fetches the new one.

So why write an AsyncAPI file too? Because the Avro schema says the field types, and nothing else. It doesn’t say what Status means, who owns the event, when it’s published, or how long it’s kept. The schema is the shape of the box; the contract is the agreement. AsyncAPI can even carry the Avro schema itself, with a schema format such as application/vnd.apache.avro;version=1.9.0, if your team wants one document.

CloudEvents is “a vendor-neutral specification for defining the format of event data” (CloudEvents 1.0.2 (opens in a new tab), checked 2026-11-07). It doesn’t care what’s in the box. It standardizes the label: four required attributes, id, source, specversion and type, and optional ones such as time, subject and datacontenttype. It’s a graduated project of the Cloud Native Computing Foundation, since January 25, 2024 (CloudEvents (opens in a new tab), CNCF, checked 2026-11-07), and the clouds use it: “Azure Event Grid natively supports events in the JSON implementation of CloudEvents v1.0” (CloudEvents v1.0 schema with Azure Event Grid (opens in a new tab), Microsoft Learn, updated February 12, 2026).

Salesforce’s platform events have their own envelope on the bus. Where CloudEvents earns its place is on the far side of a bridge (module 7). Say Steve Trucking’s integration layer passes each Delivery Update on to partners. Instead of a different shape for each partner, it could send every one the same label:

{
"specversion": "1.0",
"id": "5f0c2a9e-1d3b-4c7e-9a61-2b8f0d4e7c15",
"source": "https://stevetrucking.example/salesforce",
"type": "delivery.updated.v1",
"time": "2026-11-03T16:05:00Z",
"subject": "D-1042",
"datacontenttype": "application/json",
"data": {
"deliveryNumber": "D-1042",
"status": "Delivered",
"newEta": null
}
}

Look at the id. It’s the event’s EventUuid, carried across. CloudEvents says “Consumers MAY assume that Events with identical source and id are duplicates.” That’s module 4’s repeat handling, now written into the label, so a partner can skip a repeat without knowing anything about Salesforce.

Chapter 3 said it about APIs: a version is a promise (A Version Is a Promise). It’s even more true for events, because the publisher doesn’t know who’s listening. A REST server at least sees who calls. A publisher just publishes.

So, for an event:

  • Adding a field is usually safe. The schema ID changes, and a subscriber that follows it carries on. Write the new field into the contract, and say whether it can be empty.
  • Renaming or removing breaks someone. Salesforce’s guide says that if an event is renamed after clients subscribe, “the subscribed clients must resubscribe” (Considerations for Defining and Publishing Platform Events (opens in a new tab), checked 2026-11-07). And a deleted event definition is gone for good. Deprecate, don’t mutate: add the new field or the new event, tell the subscribers, and retire the old one on a date (Be Careful About Changing the Bones).
  • Changing what a value means is the worst of all, because nothing fails. If “Delivered” starts meaning “left at the door” instead of “signed for,” every subscriber is now wrong, and no error says so. That’s a new version of the contract, with a new type if you use CloudEvents.

That’s why the contract has a version and an owner, and why it lives in source control next to the event definition.

Test knowledge. The publisher owns an event’s contract. AsyncAPI describes event-driven APIs the way OpenAPI describes REST (servers, channels, operations with send or receive, message payloads). CloudEvents standardizes the envelope on each event (required: id, source, specversion, type). Pub/Sub API payloads are Avro; the schema comes from GetSchema, and its ID changes when the event’s fields change. Adding a field is usually safe; renaming or removing breaks subscribers, so deprecate and version.

  • Who owns each event’s contract, named in the contract: the publishing team.
  • What the contract says beyond the schema: what each field means, when the event is published, how long it’s kept, how a repeat is recognized.
  • Whether to write it as AsyncAPI, and where it lives: in source control, beside the event definition.
  • Whether events leaving through a bridge carry a common envelope, such as CloudEvents, and how the event’s own ID is carried in it.
  • How the contract changes: what counts as safe, what needs a new version, and how subscribers are told before anything is retired.
  • When it’s decided: in the first weeks of design, before the first subscriber builds.