>_ Analyst Engineering

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.

Cover for AsyncAPI 3.0 for analysts, showing a payment.settled event contract with a channel, a send operation, a Kafka key, and schema registry settings.

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:

OpenAPIAsyncAPI 3.0Kafka meaning
serversservers (host, protocol)The broker cluster
pathschannels (with address)Topics
Operation (get, post)operations with action: send or receiveWhat this application produces or consumes
Request and response bodiesmessages with payload and headersThe record value and headers
components.schemascomponents.schemas and components.messagesShared schemas
No equivalentbindings.kafkaKey, 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.x3.0Why it matters in review
publish and subscribe inside a channel, meaning what others may doTop-level operations with action: send or receive, meaning what this application doesThe single most misread thing in 2.x documents
Channel key is the topic nameChannel key is an id; the topic is in addressOne channel definition can be reused across documents
message with optional oneOfmessages map on the channelEvery message on a channel must match exactly one entry
messageIdRemoved; the map key is the idOne fewer field to keep in sync
schemaFormat on the messageschemaFormat inside a Multi Format Schema Object on payloadAvro and JSON Schema payloads sit side by side
Server urlhost, pathname, and protocolClearer per environment
Request-reply not modelledreply on an operationCommand and response topics described as a pair
Traits override the objectThe object wins over its traitsShared 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).

BindingFieldReview question it answers
ServerschemaRegistryUrl, schemaRegistryVendor (apicurio, confluent, ibm, karapace)Where is the schema enforced, and by which serializer?
Channeltopic, partitions, replicasHow much parallelism, how much redundancy?
ChanneltopicConfiguration: cleanup.policy, retention.ms, retention.bytes, max.message.bytesHow long can a consumer be down before it loses events?
Channel (topicConfiguration)confluent.key.subject.name.strategy, confluent.value.subject.name.strategyWhich registry subject holds this schema?
OperationgroupId, clientIdWhich consumer group, so offsets and lag can be checked
MessagekeyWhat is the key, and therefore what is ordered
MessageschemaIdLocation, schemaIdPayloadEncoding, schemaLookupStrategyHow 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 key declared, 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_id puts 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 correlationId declared, 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 to BACKWARD, 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.version bumped 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.

QuestionWebhook, in OpenAPI 3.1 webhooksKafka topic, in AsyncAPI
Who consumes?External parties: merchants, partnersInternal services on the platform
How do they receive it?An HTTP POST to their own URLThey read from the broker
Who controls the pace?The provider pushes, with retriesThe consumer reads at its own pace, from its offset
Replay after an outageProvider redelivery or an events APIRewind the consumer offset within retention
OrderingNot guaranteed; consumers must handle itGuaranteed per partition, so per key
SecuritySignature header, HTTPSBroker 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.

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.