http clients and apis

Event And Message Standards: CloudEvents

Event-driven systems need messages that can be routed, logged, traced, validated, and understood by more than one service. Standards such as CloudEvents provide a consistent envelope around event data so every team does not invent a different shape.

CloudEvents is often seen around webhooks, queues, event buses, serverless platforms, and integration systems. A PHP app may create CloudEvents, receive them over HTTP, or consume them from a queue.

The envelope and the data

A CloudEvent separates metadata from the business payload. The metadata explains what the event is. The data field contains the domain-specific information.

PHP example
<?php

declare(strict_types=1);

$event = [
    'specversion' => '1.0',
    'id' => 'evt_123',
    'source' => '/billing',
    'type' => 'com.example.invoice.paid',
    'time' => '2026-05-28T12:00:00Z',
    'datacontenttype' => 'application/json',
    'data' => [
        'invoice_id' => 'inv_456',
        'amount' => 1999,
        'currency' => 'GBP',
    ],
];

echo json_encode($event, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// "specversion": "1.0"
// "type": "com.example.invoice.paid"
// "invoice_id": "inv_456"

The envelope helps infrastructure and generic event consumers handle the event before they understand the specific business data.

Required fields

For a structured JSON CloudEvent, the important fields to recognise are:

  • specversion: the CloudEvents specification version, commonly 1.0.
  • id: a unique event ID from the producer.
  • source: where the event came from.
  • type: what happened.
  • data: the event payload.

Other common fields include time, subject, datacontenttype, and extension attributes.

PHP example
<?php

declare(strict_types=1);

function validateCloudEvent(array $event): array
{
    foreach (['specversion', 'id', 'source', 'type'] as $field) {
        if (!isset($event[$field]) || trim((string) $event[$field]) === '') {
            return ['valid' => false, 'message' => $field . ' is required'];
        }
    }

    if ($event['specversion'] !== '1.0') {
        return ['valid' => false, 'message' => 'Unsupported CloudEvents specversion'];
    }

    return ['valid' => true, 'message' => 'CloudEvent envelope is valid'];
}

print_r(validateCloudEvent(['specversion' => '1.0', 'id' => 'evt_1', 'source' => '/billing', 'type' => 'invoice.paid']));
print_r(validateCloudEvent(['id' => 'evt_1']));

// Prints:
// [valid] => 1
// [message] => specversion is required

Envelope validation is not the same as business validation. An event can be a valid CloudEvent while still having invalid data for your application.

Type names

Event type names should be stable and specific. Names such as updated or event are too vague. Names such as com.example.invoice.paid or uk.co.example.user.email_changed are easier to route and search.

Good event types describe facts that already happened. Prefer invoice.paid over pay_invoice, because consumers are reacting to an event, not being commanded to perform an action.

HTTP binding

CloudEvents can be sent as structured JSON, where the whole event is the JSON body, or in binary mode, where metadata appears in HTTP headers and the body contains only the data.

Structured mode is easy to inspect:

Content-Type: application/cloudevents+json

Binary mode commonly uses headers such as:

ce-specversion: 1.0
ce-id: evt_123
ce-source: /billing
ce-type: com.example.invoice.paid
Content-Type: application/json

As a junior developer, the important part is to recognise that metadata may be in the body or in headers depending on the binding.

Idempotency and ordering

CloudEvents gives an event an id, but it does not magically make processing idempotent. Your consumer still needs to store processed IDs or otherwise make repeated events safe.

CloudEvents also does not guarantee ordering by itself. If event order matters, the surrounding transport, partitioning, database design, or consumer logic must handle it.

What to check

Before moving on, make sure you can:

  • Separate event metadata from business data.
  • Recognise the core CloudEvents fields.
  • Validate an event envelope before processing data.
  • Choose clear event type names.
  • Explain the difference between structured and binary HTTP bindings.
  • Avoid assuming that event IDs solve ordering or idempotency alone.

Practice

Practice: Validate A CloudEvent

Write a PHP function that validates a structured JSON CloudEvent envelope.

Requirements

  • Decode JSON with exceptions enabled.
  • Require specversion, id, source, and type.
  • Support only specversion value 1.0.
  • Require data for this exercise.
  • Return a clear result for valid event, invalid JSON, missing field, and unsupported version.
Show solution

This solution validates the CloudEvents envelope before any business-specific processing happens.

PHP example
<?php

declare(strict_types=1);

function validateStructuredCloudEvent(string $rawJson): array
{
    try {
        $event = json_decode($rawJson, true, flags: JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        return ['valid' => false, 'message' => 'Event body must be valid JSON.'];
    }

    if (!is_array($event)) {
        return ['valid' => false, 'message' => 'Event body must decode to an object.'];
    }

    foreach (['specversion', 'id', 'source', 'type', 'data'] as $field) {
        if (!array_key_exists($field, $event) || $event[$field] === '') {
            return ['valid' => false, 'message' => $field . ' is required.'];
        }
    }

    if ($event['specversion'] !== '1.0') {
        return ['valid' => false, 'message' => 'Only CloudEvents specversion 1.0 is supported.'];
    }

    return ['valid' => true, 'message' => 'CloudEvent envelope is valid.'];
}

$examples = [
    '{"specversion":"1.0","id":"evt_1","source":"/billing","type":"com.example.invoice.paid","data":{"invoice_id":"inv_1"}}',
    '{bad json',
    '{"specversion":"1.0","id":"evt_1","type":"com.example.invoice.paid","data":{}}',
    '{"specversion":"0.3","id":"evt_1","source":"/billing","type":"com.example.invoice.paid","data":{}}',
];

foreach ($examples as $example) {
    $result = validateStructuredCloudEvent($example);
    echo ($result['valid'] ? 'valid' : 'invalid') . ': ' . $result['message'] . PHP_EOL;
}

// Prints:
// valid: CloudEvent envelope is valid.
// invalid: Event body must be valid JSON.
// invalid: source is required.
// invalid: Only CloudEvents specversion 1.0 is supported.

After this check passes, the application still needs to validate the data payload for the specific event type.