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?
An Event Arrow Needs a Contract Too
Section titled “An Event Arrow Needs a Contract Too”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.
Who Writes the Contract
Section titled “Who Writes the Contract”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?
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.
The Contract and the Envelope
Section titled “The Contract and the Envelope”Two open standards cover the two halves, and they’re easy to blur, so here they are side by side.
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: OpenAPI’s Twin
Section titled “AsyncAPI: OpenAPI’s Twin”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.0info: 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 teamservers: 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, sogrpcis 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: sendsays this application, Salesforce, sends on that channel. A subscriber’s own AsyncAPI file would sayreceive.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’s Own Contract: The Schema
Section titled “Salesforce’s Own Contract: The Schema”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: The Label on the Box
Section titled “CloudEvents: The Label on the Box”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.
A Version Is a Promise, for Events Too
Section titled “A Version Is a Promise, for Events Too”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
typeif 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.
What an Architect Decides Here
Section titled “What an Architect Decides Here”- 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.
Explore
Section titled “Explore”- AsyncAPI Specification 3.1.0 (opens in a new tab) and AsyncAPI Spec 3.1.0 Release Notes (opens in a new tab): AsyncAPI Initiative (checked 2026-11-07)
- CloudEvents 1.0.2 (opens in a new tab): the specification (checked 2026-11-07); CloudEvents (opens in a new tab), CNCF project page (checked 2026-11-07)
- CloudEvents v1.0 schema with Azure Event Grid (opens in a new tab): Microsoft Learn (checked 2026-11-07)
- Event Data Serialization with Apache Avro (opens in a new tab) and GetSchema RPC Method (opens in a new tab): Pub/Sub API Developer Guide (checked 2026-11-07)
- Considerations for Defining and Publishing Platform Events (opens in a new tab): Platform Events Developer Guide (checked 2026-11-07)
