AsyncAPI 3.0 for Analysts: Reading and Reviewing a Kafka Event Contract
Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.
Key takeaways
- AsyncAPI is to events what OpenAPI is to HTTP APIs: a machine-readable contract that names the channels (Kafka topics), the messages on them, their schemas, and the applications that send or receive them.
- AsyncAPI 3.0 split operations out of channels: a channel describes where messages flow, and a separate operation with action send or receive describes what this application does, which ended the publish and subscribe confusion of 2.x.
- The Kafka bindings carry the details analysts most often find missing: the message key, partitions, replicas, retention, the consumer groupId, and the schema registry URL and vendor.
- No AsyncAPI field states the schema registry compatibility mode, so the review must ask for it explicitly; Confluent Schema Registry defaults to BACKWARD, which allows removing fields and adding optional ones and requires consumers to upgrade before producers.
- Use an OpenAPI 3.1 webhooks entry when an external consumer receives events over HTTP, and an AsyncAPI document when consumers read from the broker; many payment platforms need both for the same business event.
AsyncAPI is the contract format for event-driven APIs: it names the broker, the channels (Kafka topics), the messages on each channel, their schemas, and which applications send or receive them. AsyncAPI 3.0 separates operations from channels, with an explicit send or receive action, and Kafka bindings carry the key, partitions, consumer group, and schema registry. Reviewing one as an analyst means checking the things events break on: the key and ordering, idempotency, versioning, and schema registry compatibility mode.
Most event-driven programmes I have joined had a beautifully maintained OpenAPI file and a Confluence page for the Kafka topics, last edited by someone who had left. The events were where the business outcome actually arrived (settled, failed, refunded), and they were the least specified interface in the estate. AsyncAPI fixes the format problem; the analyst still has to fix the content. This article is part of the advanced track of APIs for Analysts and builds on event-driven requirements.
APIs for Analysts, advanced track. Builds on event-driven requirements and how to test Kafka. Full learning path: APIs for Analysts.
What is AsyncAPI, and which version should you read?
AsyncAPI is an open, protocol-agnostic specification for message-driven APIs, written in YAML or JSON. The specification text is at asyncapi.com. Version 3.0.0 was released in December 2023. Version 3.1.0 followed on 31 January 2026, and its release notes list one feature: ROS 2 bindings. Everything in this article about Kafka applies to both 3.0 and 3.1. The example declares asyncapi: 3.0.0; changing that line to 3.1.0 changes nothing for Kafka, and the AsyncAPI CLI’s convert command already defaults to 3.1.0 as its target.
The mental model, if you already read OpenAPI:
| OpenAPI | AsyncAPI 3.0 | Kafka meaning |
|---|---|---|
servers | servers (host, protocol) | The broker cluster |
paths | channels (with address) | Topics |
Operation (get, post) | operations with action: send or receive | What this application produces or consumes |
| Request and response bodies | messages with payload and headers | The record value and headers |
components.schemas | components.schemas and components.messages | Shared schemas |
| No equivalent | bindings.kafka | Key, partitions, groupId, schema registry |
What changed from AsyncAPI 2.x to 3.0?
If you inherit a 2.x document, this table is the translation key. It comes from the official migration guide.
| 2.x | 3.0 | Why it matters in review |
|---|---|---|
publish and subscribe inside a channel, meaning what others may do | Top-level operations with action: send or receive, meaning what this application does | The single most misread thing in 2.x documents |
| Channel key is the topic name | Channel key is an id; the topic is in address | One channel definition can be reused across documents |
message with optional oneOf | messages map on the channel | Every message on a channel must match exactly one entry |
messageId | Removed; the map key is the id | One fewer field to keep in sync |
schemaFormat on the message | schemaFormat inside a Multi Format Schema Object on payload | Avro and JSON Schema payloads sit side by side |
Server url | host, pathname, and protocol | Clearer per environment |
| Request-reply not modelled | reply on an operation | Command and response topics described as a pair |
| Traits override the object | The object wins over its traits | Shared defaults no longer clobber specifics |
The AsyncAPI CLI converts older documents (asyncapi convert), but read the result. A converter cannot know whether a 2.x subscribe was written from the producer’s or the consumer’s point of view, and I have seen that inverted on the first attempt more than once.
What does a payment.settled event look like in AsyncAPI 3.0?
The example is the settlement service of a card and SEPA payment platform. It sends one payment.settled event per payment to the settlement.payment.settled topic, keyed by payment_id. The same topic appears in the Northline Pay event catalogue that the free Validate an Event Flow mission asks you to trace.
asyncapi: 3.0.0
info:
title: Settlement events
version: 1.3.0
description: Events produced by the settlement service. Owner, payments platform team.
servers:
production:
host: broker-1.payments.internal:9093
protocol: kafka
description: Production cluster, TLS and SASL.
bindings:
kafka:
schemaRegistryUrl: https://schema-registry.payments.internal
schemaRegistryVendor: confluent
bindingVersion: '0.5.0'
channels:
paymentSettled:
address: settlement.payment.settled
description: One event per payment when funds are settled. Keyed by payment_id.
messages:
paymentSettled:
$ref: '#/components/messages/PaymentSettled'
bindings:
kafka:
partitions: 12
replicas: 3
topicConfiguration:
cleanup.policy: [delete]
retention.ms: 604800000
bindingVersion: '0.5.0'
operations:
sendPaymentSettled:
action: send
channel:
$ref: '#/channels/paymentSettled'
summary: Settlement sends payment.settled after the ledger posts the settlement entry.
messages:
- $ref: '#/channels/paymentSettled/messages/paymentSettled'
components:
messages:
PaymentSettled:
name: PaymentSettled
title: Payment settled
contentType: application/json
correlationId:
description: Propagated from the original payment request.
location: $message.header#/correlation_id
headers:
type: object
required: [correlation_id, event_type, schema_version]
properties:
correlation_id: { type: string }
event_type: { type: string, const: payment.settled }
schema_version: { type: integer, const: 1 }
payload:
$ref: '#/components/schemas/PaymentSettledPayload'
bindings:
kafka:
key:
type: string
description: payment_id. Every event of one payment goes to one partition.
bindingVersion: '0.5.0'
examples:
- name: sepaSettled
headers:
correlation_id: 6f1c2a9e-4b7d-4e1a-9c3f-2d8e5b7a1f04
event_type: payment.settled
schema_version: 1
payload:
event_id: evt_9Hq2LmT4vB8cXk1R
occurred_at: '2026-10-09T08:14:22Z'
payment_id: pay_8f3k2LmQ9xT4vB1c
settlement_id: set_4KdP9sW2qL7m
amount: 125000
currency: EUR
end_to_end_id: E2E-20261009-000431
schemas:
PaymentSettledPayload:
type: object
required: [event_id, occurred_at, payment_id, settlement_id, amount, currency]
properties:
event_id:
type: string
description: Unique per event. Consumers deduplicate on this field.
occurred_at: { type: string, format: date-time }
payment_id: { type: string }
settlement_id: { type: string }
amount: { type: integer, minimum: 1, description: "Minor units, 125000 = 1,250.00" }
currency: { type: string, enum: [EUR, GBP] }
end_to_end_id:
type: [string, 'null']
maxLength: 35
description: ISO 20022 EndToEndId for SEPA payments, null for card.
The consumer side is a separate document owned by the consumer, with the operation reversed and its consumer group declared:
operations:
receivePaymentSettled:
action: receive
channel:
$ref: '#/channels/paymentSettled'
bindings:
kafka:
groupId:
type: string
enum: [webhook-dispatcher]
bindingVersion: '0.5.0'
What if the payload is Avro?
Avro payloads use the Multi Format Schema Object: set schemaFormat and inline the Avro schema as YAML.
payload:
schemaFormat: application/vnd.apache.avro+yaml;version=1.9.0
schema:
type: record
name: PaymentSettled
namespace: com.example.settlement
fields:
- { name: event_id, type: string }
- { name: payment_id, type: string }
- { name: amount, type: long, doc: "Minor units" }
- { name: currency, type: string }
- { name: end_to_end_id, type: ['null', string], default: null }
Note the union for end_to_end_id: in Avro, the default of a union must match its first type, so a nullable field with a null default lists 'null' first. Get this wrong and the schema registry rejects the schema, or worse, accepts a field that old consumers cannot read.
What do the Kafka bindings tell an analyst?
The bindings are where the contract stops being generic and starts being Kafka. Each field below comes from the Kafka bindings (current binding version 0.5.0).
| Binding | Field | Review question it answers |
|---|---|---|
| Server | schemaRegistryUrl, schemaRegistryVendor (apicurio, confluent, ibm, karapace) | Where is the schema enforced, and by which serializer? |
| Channel | topic, partitions, replicas | How much parallelism, how much redundancy? |
| Channel | topicConfiguration: cleanup.policy, retention.ms, retention.bytes, max.message.bytes | How long can a consumer be down before it loses events? |
Channel (topicConfiguration) | confluent.key.subject.name.strategy, confluent.value.subject.name.strategy | Which registry subject holds this schema? |
| Operation | groupId, clientId | Which consumer group, so offsets and lag can be checked |
| Message | key | What is the key, and therefore what is ordered |
| Message | schemaIdLocation, schemaIdPayloadEncoding, schemaLookupStrategy | How a consumer finds the schema for each record |
retention.ms: 604800000 is seven days. That number is a business rule in disguise: a consumer down for eight days over a long weekend and a release freeze loses events permanently, and the recovery is a replay from the producer or a reconciliation. Ask who agreed to seven days.
How do you review an event contract?
This is the checklist I run on every AsyncAPI document before it goes to build. Each line should be answered in the contract itself or in a page it links to.
Keys and partitioning
- Is the message
keydeclared, and does its description say what it is (payment_id)? - Is the ordering guarantee stated in business terms (“all events of one payment are delivered in order”), given that Kafka orders only within a partition?
- Is the partition count stated, and is it written down who may change it? Adding partitions changes which partition a key maps to for new records, which can break per-key ordering across the change.
- Are hot keys considered? A key such as
merchant_idputs a large merchant’s whole volume on one partition.
Ordering and idempotency
- Does each message carry a unique
event_id, and does the contract tell consumers to deduplicate on it? Delivery is at least once in practice, so duplicates are normal. - Is there a field consumers can use to reject stale events (a version or
occurred_at) when events for one key arrive out of order after a retry? - Is the
correlationIddeclared, and does it match the identifier used in logs and in the HTTP API (a UETR or EndToEndId for ISO 20022 payments)?
Versioning and the schema registry
- Is the registry compatibility mode stated? AsyncAPI has no field for it, so record it in the channel description or a team extension such as
x-schema-compatibility. Confluent Schema Registry defaults toBACKWARD, which allows deleting fields and adding optional fields, and means all consumers must upgrade before producers send the new schema. - Is it stated what counts as a breaking change, and does a breaking change go to a new topic or a new
schema_version? - Is
info.versionbumped on every change, and does a changelog exist?
Failure and recovery
- Is the consumer’s dead letter topic named, with the headers it adds and the replay process? See dead letter queues.
- Is retention long enough for the longest realistic consumer outage?
Evidence
- Does every message have at least one complete example that validates against its schema?
- Is there a way to produce each message type on demand in a test environment?
These map onto Concurrency and ordering (C8), Versioning (C10), Traceability (C11), and Evidence (C12) in the Review Scorecard, which works for event contracts as well as for HTTP APIs.
Turning that checklist into automated checks against a real topic (consume, filter by correlation id, assert schema and key) is what Automate Kafka Validation with Postman walks through step by step, and the free walkthrough is Kafka validation with Postman.
How do you validate and diff an AsyncAPI document?
The AsyncAPI CLI covers validation, conversion, and diffs. Spectral’s built-in spectral:asyncapi ruleset has rules for both v2 and v3 documents.
npm install -g @asyncapi/cli
# Validate structure and references
asyncapi validate settlement-events.yaml
# Show only breaking changes between two versions of the contract
asyncapi diff settlement-events-1.2.0.yaml settlement-events-1.3.0.yaml -t breaking
# Lint with Spectral
echo 'extends: ["spectral:asyncapi"]' > .spectral.yaml
npx @stoplight/spectral-cli lint settlement-events.yaml
A green asyncapi diff is not a green light. The registry compatibility check is the one that actually blocks a producer at runtime, so the release checklist needs both: the contract diff in the pull request, and the registry’s compatibility check against the subject before deployment. Then test the behaviour no schema shows, as described in how to test Kafka: duplicates, out-of-order pairs, and a consumer killed mid-batch.
Should this event be a webhook or a Kafka topic?
Often both, described by two contracts. The question is who the consumer is.
| Question | Webhook, in OpenAPI 3.1 webhooks | Kafka topic, in AsyncAPI |
|---|---|---|
| Who consumes? | External parties: merchants, partners | Internal services on the platform |
| How do they receive it? | An HTTP POST to their own URL | They read from the broker |
| Who controls the pace? | The provider pushes, with retries | The consumer reads at its own pace, from its offset |
| Replay after an outage | Provider redelivery or an events API | Rewind the consumer offset within retention |
| Ordering | Not guaranteed; consumers must handle it | Guaranteed per partition, so per key |
| Security | Signature header, HTTPS | Broker authentication and ACLs |
A common payment platform shape: settlement sends payment.settled to Kafka for internal consumers, and a webhook dispatcher consumes it and delivers a payment.settled webhook to the merchant. The internal contract is AsyncAPI; the external one is a webhooks entry in the OpenAPI file, which is covered in webhooks for analysts and in OpenAPI 3.1 and 3.2 for analysts. AsyncAPI can describe HTTP too, but external consumers’ tooling expects OpenAPI for HTTP.
The two contracts must agree on field names and meaning, or merchants and internal teams will reconcile against different definitions of “settled”. Put the shared payload schema in one file and reference it from both. If the end-to-end flow spans an HTTP call and an event, Arazzo 1.1 can now describe a workflow whose steps send and receive AsyncAPI messages alongside OpenAPI operations. For the broader choice between patterns, see synchronous vs asynchronous.
The takeaway
AsyncAPI gives events the same contract discipline OpenAPI gives HTTP: servers, channels, operations, messages, and schemas in one reviewable file. In 3.0, operations sit outside channels with an explicit send or receive, and the Kafka bindings hold the details that decide correctness: the key, partitions, retention, consumer group, and schema registry. Review every event contract for the key and the ordering it buys, a deduplication id, a correlation id, the registry compatibility mode (which no field captures, so ask), retention against realistic outages, the dead letter process, and a valid example per message. Then publish the external version of the same event as an OpenAPI webhook, with the same payload definition.
Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.
Tags: AsyncAPI, Kafka, Event-Driven Architecture, Event Contracts, Schema Registry, Systems Analysis
About the author
Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.
Related articles
- Event-Driven Requirements: Specifying Systems That Talk in Events How to write requirements for event-driven systems: event schemas, ordering, idempotency, retries, and consistency. A practitioner guide with a Kafka example.
- How to Test Kafka: Validating Events You Cannot See A guide to testing Kafka: consuming events in a test, asserting schema and key, verifying ordering and duplicates, and checking consumer side effects.
- Dead-Letter Queues: Where Failed Messages Go What a dead-letter queue is, why event-driven systems need one, and how an analyst specifies DLQ behavior: retries, routing, monitoring, and recovery.
- Webhooks Explained for Analysts: How to Specify, Test, and Debug Them How webhooks work and what analysts must specify: events, signatures, retries, duplicates, and ordering, plus testing with webhook.site and the Stripe CLI.
Go deeper on this
Not ready to buy? The free downloads are a no-cost place to start, and every article here stays free.
Free account
Practice on the Labs, keep your progress
A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.
Your email is used to sign you in. Nothing else, unless you ask. Privacy.