The CloudEvents wire format¶
Binary mode, as the CloudEvents specification defines it and
as the CloudEvents SDK's own bindings implement it. Attributes ride in headers; the payload is the
body, untouched. Everything below is written with the ce- prefix of the HTTP and NATS bindings,
which is what Binary(HTTP) and Binary(NATS) write; other bindings change only
the prefix.
Structured mode is at the end.
Required attributes¶
All four must be present and non-empty, and the absence of any of them — specversion included —
is ErrMissingAttribute.
ErrUnsupportedSpecVersion is for a specversion that is
present and is not 1.0. The distinction is load bearing: "this is not a CloudEvent" and "this
is a CloudEvent from a version we do not implement" are different problems, and an operator alerting
on version skew wants only the second.
| Header | Field | Notes |
|---|---|---|
ce-id |
Event.ID |
With source, uniquely identifies the occurrence. Do not reuse the pair |
ce-source |
Event.Source |
A URI-reference, so it may be relative — /phpbotscout/ingest is valid |
ce-specversion |
Event.SpecVersion |
Always 1.0. Encode fills it in when empty |
ce-type |
Event.Type |
What consumers filter on. Reverse-DNS by convention: uk.phpboyscout.orders.created |
Optional attributes¶
Omitted entirely when empty: an absent optional attribute is an absent key, never an empty one.
| Header | Field | Notes |
|---|---|---|
| content type | Event.DataContentType |
Not prefixed, and not a header on write: it is Wire.ContentType. ce-datacontenttype is also accepted on read, see below |
ce-dataschema |
Event.DataSchema |
An absolute URI. Estate schema identifiers are URNs, see the URN package |
ce-subject |
Event.Subject |
Names the thing the event is about, within source |
ce-time |
Event.Time |
RFC 3339. Written in UTC, with fractional seconds when they are non-zero |
Percent-encoding¶
Both bindings make this a MUST, and binary mode does it in both directions.
A header value has space (U+0020), double-quote (U+0022), percent (U+0025) and everything outside
printable ASCII U+0021–U+007E written as UTF-8 bytes in %XY form. A receiver performs exactly one
round of decoding — one, so a value that legitimately contains %25 is not decoded twice into %.
Upper-case hex is written and either case is read, as the specification asks. A value that decodes
to invalid UTF-8, or carries a truncated or non-hexadecimal escape, is
ErrInvalidEncoding.
The content type is exempt. It is a media type the transport interprets, it is never encoded on
write, and a media type containing a literal % is not valid anyway.
Control characters¶
Refused, not encoded. The specification's String type excludes U+0000–U+001F and U+007F–U+009F,
and while percent-encoding would carry them, transportable and permitted are different questions.
net/http rewrites a newline in a header rather than refusing it, so without the check a receiver
would get a different attribute value than the sender set, with no error anywhere.
Extensions¶
Any other header under the binding's prefix. The prefix is stripped on read and added on write, so
Extensions["scope"] is ce-scope on the wire.
Names must be one or more lower-case ASCII letters or digits, must not be data, and must not be
one of the eight specification attribute names. Without that last rule Extensions["id"] would
overwrite ce-id on the way out, producing an event with a hijacked identity, no error, and a round
trip that agrees with itself. See
Add an extension attribute.
A complete event¶
ce-id: 01JQ8Z3F7K2M4N6P8R0T2V4W6X
ce-source: /phpbotscout/ingest
ce-specversion: 1.0
ce-type: uk.phpboyscout.orders.created
ce-subject: order-4172
ce-time: 2026-08-28T11:22:33Z
ce-dataschema: urn:phpboyscout:schema:orders.created:3
ce-scope: urn:phpboyscout:scope:continuity:01JQ8Z3F7K2M4N6P8R0T2V4W6X
content-type: application/json
{"order":"4172"}
That is testdata/full.txt in
go/envelope-cloudevents,
and it is read by the test suite rather than described by it. As a Wire, content-type is
ContentType and the rest are Headers.
Payload ownership¶
Binary mode's Encode returns a Wire whose Body aliases Event.Data, and its Decode
returns an Event whose Data aliases the Wire.Body it was given. Neither copies.
That is deliberate — a codec on a message hot path should not double every payload — but it makes
the ownership yours. A producer reusing an encode buffer will mutate a message it has already handed
to a transport, and an Event retained past an HTTP handler that read into a pooled buffer will
change underneath you. Copy at the boundary if you need to hold either.
The two spellings of datacontenttype¶
The bindings genuinely disagree. The HTTP binding maps datacontenttype onto Content-Type and
says a ce-datacontenttype header MUST NOT also be present; the NATS binding prefixes it like every
other attribute.
Binary mode writes the HTTP form, which is what the SDK's NATS binding emits in practice, and accepts either on read, because silently losing a conformant NATS peer's content type is worse than tolerating a spelling. Where both arrive, the unprefixed one wins.
Header case¶
Binary(HTTP), Binary(NATS) and Binary(Kafka) write lower-case keys; Binary(AMQP) writes
cloudEvents_ as the binding spells it. Decode matches every key without regard to case, so an
event that crossed http.Header, which canonicalises to Ce-Id, is read back identically.
http.Header.Get will not find them
Get canonicalises the key it is given, so it cannot see a map keyed ce-id. Read Encode's
output by iteration or by exact key.
Two spellings of one key carrying different values is
ErrAmbiguousHeader, not a choice. So are two values under one key.
Headers the format does not own are not compared: X-Forwarded-For with two hops is the
ordinary shape behind a proxy, and refusing the event over it would be refusing over something that
is none of the format's business. They come back in Foreign.Headers.
Other bindings¶
Binary(binding) takes the binding's prefix in place of ce-. Keys are matched without regard to
case, on every binding.
| Binding | Writes | Also reads |
|---|---|---|
HTTP, NATS |
ce-id |
|
Kafka |
ce_id |
|
AMQP (1.0) |
cloudEvents_id |
cloudEvents:id, which the binding also permits |
RabbitMQ speaks AMQP 0-9-1, which no binding covers; use HTTP, whose headers it most resembles.
A Format puts the content type in Wire.ContentType, not in a header, because AMQP and RabbitMQ
carry it as a native property. On a transport without one, write it as the content-type header.
Binary(b).Decode reads it from either place, and refuses the two with ErrAmbiguousHeader if they
differ; Structured() uses it only to recognise the message, preferring Wire.ContentType.
Every header without the binding's prefix, other than content-type, comes back in
Foreign.Headers. It is never promoted into the event: a bare traceparent from a broker does not
replace the event's own. Binary mode never reports Foreign.Conflicts, because it has no envelope a
header can shadow.
A binary datacontenttype must not start application/cloudevents. Every binding reserves that
prefix for structured mode, so a reader asked to accept both modes could not tell which this was.
Binary(b).Encode refuses it with
ErrReadsAsStructured.
Structured mode¶
Structured() is the JSON event format:
the whole event as one JSON object in the body, with content type application/cloudevents+json.
Attributes and extensions are members by their own names; there are no attribute headers, so a
transport's header limit no longer applies.
{"datacontenttype":"application/json","id":"01JQ8Z3F7K2M4N6P8R0T2V4W6X","source":"/phpbotscout/ingest",
"specversion":"1.0","subject":"order-4172","time":"2026-08-28T11:22:33Z",
"type":"uk.phpboyscout.orders.created","data":{"order":"4172"}}
| Payload | Travels as |
|---|---|
JSON (application/json, text/json, any +json, or no content type), valid, no surrounding whitespace |
data, verbatim, so it reads back byte for byte |
| Anything else: protobuf, text, JSON with a margin | data_base64 |
On read:
- a member that appears twice is
ErrAmbiguousHeader, as is a body with bothdataanddata_base64; nullis absent;- an extension that is a JSON number or boolean becomes its JSON text,
3ortrue, because extensions are strings here; an object or array isErrInvalidJSON; - a string
datawith a content type that is not JSON is the payload's text; - every header but
content-typeis inForeign.Headers, because in this mode the event is the body; - a refusal still returns every member read before it, keeping the first value of a repeated one.
Batch mode, application/cloudevents-batch+json, is not implemented and is not recognised.
Reading more than one format¶
envelope.Read(w, formats...) asks every format whether it recognises the message, and decodes with the one
that does. It does not stop at the first, because no order is safe: binary mode recognises a message
by its ce-specversion header and structured mode by its content type, and a message can carry
both.
| Formats that claim it | Result |
|---|---|
| none | ErrUnrecognised |
| one | that format's Decode, and the format, so a reply can be written in it |
| two or more | ErrAmbiguous, naming them; nothing is decoded |