<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE rfc [
  <!ENTITY nbsp    "&#160;">
  <!ENTITY zwsp   "&#8203;">
  <!ENTITY nbhy   "&#8209;">
  <!ENTITY wj     "&#8288;">
]>
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl" -->
<rfc xmlns:xi="http://www.w3.org/2001/XInclude" version="3" ipr="trust200902" docName="draft-hardt-aauth-events-00" submissionType="IETF" category="std" xml:lang="en" indexInclude="true">

<front>
<title abbrev="AAuth-Events">AAuth Events</title><seriesInfo value="draft-hardt-aauth-events-00" stream="IETF" status="standard" name="Internet-Draft"/>
<author initials="D." surname="Hardt" fullname="Dick Hardt"><organization>Hellō</organization><address><postal><street/>
</postal><email>dick.hardt@gmail.com</email>
</address></author><date/>
<area>Security</area>
<workgroup>TBD</workgroup>
<keyword>agent</keyword>
<keyword>events</keyword>
<keyword>webhooks</keyword>
<keyword>async</keyword>
<keyword>subscribe</keyword>
<keyword>http</keyword>
<keyword>identity</keyword>

<abstract>
<t>This document defines AAuth Events — an event subscription and delivery mechanism for agents operating under the AAuth Protocol (<xref target="I-D.hardt-oauth-aauth-protocol"/>). It specifies the subscribe token that agents use to register callbacks with resources, the event token that resources deliver when events fire, and the delivery path through the Agent Provider (AP). AAuth Events enables agents to receive asynchronous notifications without requiring a public endpoint, using the cryptographic identity established by the AAuth Protocol.</t>
</abstract>

<note><name>Discussion Venues</name>
<t><em>Note: This section is to be removed before publishing as an RFC.</em></t>
<t>This document is part of the AAuth specification family. Source for this draft and an issue tracker can be found at <eref target="https://github.com/dickhardt/AAuth">https://github.com/dickhardt/AAuth</eref>.</t>
</note>

</front>

<middle>

<section anchor="introduction"><name>Introduction</name>

<section anchor="agents-cannot-receive-webhooks"><name>Agents Cannot Receive Webhooks</name>
<t>Agents are often not servers with a routable endpoint. Whether running as a workload, a mobile app, or a single-page application, agents typically cannot receive inbound HTTP connections. Existing event delivery mechanisms — webhooks, WebSub, callback URLs — all assume the receiver is always-on and reachable. This assumption fails for agents that run intermittently, execute behind NAT, or live inside a platform that does not expose inbound HTTP.</t>
<t>At the same time, many interactions agents initiate are inherently asynchronous. An agent books a medical appointment and needs to know if an earlier slot opens. An agent monitors inventory and needs to know when a product becomes available. An agent submits an order and needs confirmation when it ships. In each case, the agent initiates a synchronous request, the resource accepts it, and then the resource needs to reach back to the agent when something changes — potentially hours or days later.</t>
<t>Existing approaches each fall short:</t>

<ul spacing="compact">
<li><strong>Webhooks</strong> require the agent to have a public URL. Agents do not.</li>
<li><strong>Polling</strong> is wasteful and imprecise. For time-sensitive events like waitlist slots, polling is too slow and too expensive.</li>
<li><strong>Server-Sent Events / WebSocket</strong> require a persistent outbound connection, which conflicts with intermittent agent workloads.</li>
<li><strong>Message queues</strong> (SQS, Kafka, RabbitMQ) require shared infrastructure, are not web-standard, and have no standardized subscription protocol across trust domains.</li>
</ul>
</section>

<section anchor="the-agent-provider-as-inbox"><name>The Agent Provider as Inbox</name>
<t>The AAuth Protocol establishes that every agent has an Agent Provider (AP) — a stable, always-on server that issues the agent its agent token. The AP is already a first-class principal in the AAuth ecosystem: it has its own cryptographic identity, publishes metadata at a well-known URL, and is trusted by all parties that interact with the agent.</t>
<t>AAuth Events uses the AP as the agent's permanent event inbox. A resource that needs to notify an agent does not need to reach the agent directly — it posts the event to the AP's event endpoint. The AP delivers the event to the agent through whatever mechanism the AP and agent have established. The agent does not need a public URL. The AP is the public URL.</t>
</section>

<section anchor="an-open-event-network"><name>An Open Event Network</name>
<t>The AP inbox and AAuth identity together form a decentralized event network: no central broker, no pre-registration between parties. A resource that has never seen an AP before can deliver to it by resolving the AP's event endpoint from its well-known metadata and signing the delivery with its own discoverable key. This is the federated reachability of email without its deliverability failure. Every delivery is cryptographically attributable to an identified resource, the AP accepts events only for subscriptions its agents established, and protected channels can require the person's identity before a subscription is created. There is no barrier to entry: an agent operator can run their own AP or use a hosted one with no difference in protocol, and any resource with a domain and a JWKS can deliver events.</t>
</section>

<section anchor="what-aauth-events-provides"><name>What AAuth Events Provides</name>

<ul spacing="compact">
<li><strong>No public endpoint required</strong>: The AP receives events on the agent's behalf. AP-to-agent delivery is platform-dependent and out of scope for this specification.</li>
<li><strong>Cryptographic authorization</strong>: The subscribe token is AP-signed and restricts event delivery to a specific resource. No shared secrets.</li>
<li><strong>Deliverability through identity</strong>: Every delivery is signed by an identified resource and accepted only against an active subscription. No spoofed events, no shared webhook secrets, no spam.</li>
<li><strong>Decentralized</strong>: No central broker and no out-of-band registration. Resources discover AP event endpoints via well-known metadata, and APs can be self-hosted or outsourced interchangeably.</li>
<li><strong>Agent identity at subscription time</strong>: The resource knows cryptographically which agent subscribed, via the <tt>sub</tt> claim in the subscribe token.</li>
<li><strong>Protected and public subscriptions</strong>: Public event channels require only a subscribe token. Protected channels use a pre-authorized subscription URL issued by the resource during a prior authenticated interaction.</li>
<li><strong>Event discovery via AsyncAPI</strong>: Resources describe their event channels using AsyncAPI (<xref target="AsyncAPI"/>) as an AAuth R3 vocabulary (<xref target="I-D.hardt-aauth-r3"/>).</li>
</ul>
</section>

<section anchor="relationship-to-existing-standards"><name>Relationship to Existing Standards</name>
<t>AAuth Events builds on the AAuth Protocol (<xref target="I-D.hardt-oauth-aauth-protocol"/>) and HTTP Signature Keys (<xref target="I-D.hardt-httpbis-signature-key"/>). It provides the transport and subscription mechanisms that AsyncAPI (<xref target="AsyncAPI"/>) describes: resources use AsyncAPI to document their event channels and payload schemas, while AAuth Events defines how agents subscribe and how events are delivered.</t>
</section>
</section>

<section anchor="conventions-and-definitions"><name>Conventions and Definitions</name>
<t>{::boilerplate bcp14-tagged}</t>
</section>

<section anchor="terminology"><name>Terminology</name>
<t>Terms defined in <xref target="I-D.hardt-oauth-aauth-protocol"/> are used here with the same meaning. In particular: Agent, Agent Provider (AP), Agent Token, Person Token, Resource, Resource Token, Auth Token, Person Server (PS), Access Server (AS), and HTTP Sig.</t>
<t>This document additionally uses:</t>

<ul spacing="compact">
<li><strong>Subscribe Token</strong>: A JWT issued by the AP to the agent, authorizing a specific resource to deliver events to the AP on the agent's behalf. Contains the Event ID and the agent's current signing key.</li>
<li><strong>Event ID (eid)</strong>: An opaque, AP-generated identifier that uniquely identifies a subscription at the AP. The agent maps the <tt>eid</tt> to its own context. The <tt>eid</tt> is the correlation key between the subscribe token, the AP's subscription record, and the event token.</li>
<li><strong>Event Token</strong>: A JWT issued and signed by the resource when an event fires, addressed to the agent (<tt>aud</tt> = agent identifier), identified by its <tt>jti</tt>, and delivered to the AP's event endpoint using the <tt>self-jwt</tt> Signature-Key scheme (<xref target="I-D.hardt-httpbis-signature-key"/>).</li>
<li><strong>Event Endpoint</strong>: An endpoint published by the AP in its metadata at which resources deliver event tokens.</li>
<li><strong>Subscription Ticket</strong>: An opaque, short-lived value returned by a resource in response to an authenticated interaction, pre-authorizing a subsequent subscription registration call. Used when subscription to a protected channel requires prior authenticated context.</li>
</ul>
</section>

<section anchor="protocol-overview"><name>Protocol Overview</name>
<t>AAuth Events involves four phases: setup, subscription registration, event delivery from resource to AP, and event delivery from AP to agent.</t>
<figure anchor="fig-overview"><name>AAuth Events Protocol Overview </name>
<sourcecode type="ascii-art"><![CDATA[Agent          AP                   Resource
  |             |                        |
  | (1) request |                        |
  |  subscribe  |                        |
  |  token      |                        |
  |------------>|                        |
  |             |                        |
  | subscribe   |                        |
  | token       |                        |
  |<------------|                        |
  |             |                        |
  | (2) signed request                   |
  |  w/ subscribe token                  |
  |------------------------------------->|
  |             |                        |
  |           200 OK                     |
  |<-------------------------------------|
  |             |                        |
  |             | ... time passes ...    |
  |             |                        |
  |             |  (3) POST event token. |
  |             |  (+ optional payload)  |
  |             |<-----------------------|
  |             |                        |
  |             |   202 Accepted         |
  |             |----------------------->|
  |             |                        |
  | (4) event   |                        |
  |  token +    |                        |
  |  payload    |                        |
  |<------------|                        |
]]>
</sourcecode>
</figure>

<ol>
<li><t><strong>Subscribe token acquisition (non-normative)</strong>: The agent requests a subscribe token from its AP. The AP generates an <tt>eid</tt>, creates a subscription record, and issues a subscribe token. This interaction is AP-internal and out of scope for this specification. See <xref target="non-normative-ap-agent"/> for examples.</t>
</li>
<li><t><strong>Subscription registration</strong>: The agent presents the subscribe token to the resource as the <tt>Signature-Key</tt> JWT on a signed HTTP request to the resource's subscription endpoint. The resource validates the subscribe token, stores the <tt>eid</tt> and the AP's issuer, from which it resolves the AP's <tt>event_endpoint</tt> at delivery time, and registers the subscription.</t>
</li>
<li><t><strong>Event delivery — resource to AP</strong>: When an event fires, the resource issues an event token (a JWT signed by the resource) and POSTs it to the AP's <tt>event_endpoint</tt>, presenting the event token as the <tt>Signature-Key</tt> JWT using the <tt>self-jwt</tt> scheme (<xref target="I-D.hardt-httpbis-signature-key"/>). The optional request body carries the AsyncAPI-defined payload for the event type, and the event token carries its digest.</t>
</li>
<li><t><strong>Event delivery — AP to agent (non-normative)</strong>: The AP validates the event, looks up the subscription by <tt>eid</tt>, and delivers the event token and any payload to the agent. This step is platform-dependent and out of scope for this specification. See <xref target="non-normative-ap-agent"/> for examples.</t>
</li>
</ol>
</section>

<section anchor="ap-metadata"><name>AP Metadata</name>
<t>The AP MUST publish an <tt>event_endpoint</tt> field in its metadata at <tt>/.well-known/aauth-agent.json</tt> if it supports AAuth Events. The <tt>event_endpoint</tt> is an HTTPS URL at which the AP receives event tokens from resources.</t>

<sourcecode type="json"><![CDATA[{
  "issuer": "https://ap.example",
  "jwks_uri": "https://ap.example/.well-known/jwks.json",
  "event_endpoint": "https://ap.example/events"
}
]]>
</sourcecode>
<t>The AP MAY update the <tt>event_endpoint</tt> URL at any time. Resources resolve the AP's <tt>event_endpoint</tt> from the AP's metadata (using the <tt>iss</tt> claim in the subscribe token to locate the AP's well-known document) rather than caching it from the subscribe token. A resource verifies the document per (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Metadata Documents) before using its <tt>event_endpoint</tt>. Before fetching the AP's metadata or posting to its <tt>event_endpoint</tt>, a resource applies egress admission (<xref target="I-D.hardt-httpbis-signature-key"/>, Scheme-Specific Risks).</t>
</section>

<section anchor="subscribe-token"><name>Subscribe Token</name>

<section anchor="structure"><name>Structure</name>
<t>A subscribe token is a JWT with <tt>typ: aa-subscribe+jwt</tt>, issued and signed by the AP, with the following claims:</t>
<t>Header:</t>

<ul spacing="compact">
<li><tt>alg</tt>: per (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Signature Algorithms)</li>
<li><tt>typ</tt>: <tt>aa-subscribe+jwt</tt></li>
<li><tt>kid</tt>: Key identifier (AP's signing key)</li>
</ul>
<t>Required payload claims:</t>

<ul spacing="compact">
<li><tt>iss</tt>: Agent Provider URL. Used by the resource to locate the AP's metadata and <tt>event_endpoint</tt>.</li>
<li><tt>dwk</tt>: <tt>aauth-agent.json</tt> — the well-known metadata document name for key discovery (<xref target="I-D.hardt-httpbis-signature-key"/>).</li>
<li><tt>sub</tt>: Agent identifier. The AAuth agent identifier (<tt>aauth:local@domain</tt>) of the subscribing agent.</li>
<li><tt>aud</tt>: Resource URL. The resource that is authorized to deliver events for this subscription. The resource MUST verify that its own URL matches this claim.</li>
<li><tt>cnf</tt>: Confirmation claim (<xref target="RFC7800"/>) with <tt>jwk</tt> containing the agent's current public signing key. The resource uses this key to verify the HTTP signature on the subscription registration request.</li>
<li><tt>eid</tt>: Event ID. An opaque string generated by the AP, unique to the AP. The agent maps the <tt>eid</tt> to its own context (see <xref target="agent-context-mapping"/>). The resource includes the <tt>eid</tt> in every event token it issues for this subscription.</li>
<li><tt>jti</tt>: Unique token identifier (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Common JWT Claims).</li>
<li><tt>iat</tt>: Issued-at timestamp.</li>
<li><tt>exp</tt>: Expiration timestamp. The resource MUST reject subscribe tokens with <tt>exp</tt> in the past.</li>
</ul>
<t>Optional payload claims:</t>

<ul spacing="compact">
<li><tt>max_uses</tt>: A positive integer. If present, the AP MUST NOT accept more than this many event tokens for this <tt>eid</tt>. If absent, the subscription is unlimited. Enforcement is the AP's responsibility; the AP informs the resource of remaining uses in its <tt>202 Accepted</tt> response (see <xref target="event-delivery"/>). The resource SHOULD track <tt>remaining_uses</tt> to manage subscription state — for example, prompting the agent to re-subscribe when the subscription is exhausted.</li>
</ul>
<t>Example subscribe token payload:</t>

<sourcecode type="json"><![CDATA[{
  "iss": "https://ap.example",
  "dwk": "aauth-agent.json",
  "sub": "aauth:k7q3p9n2@ap.example",
  "aud": "https://resource.example",
  "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519",
                    "x": "...", "alg": "Ed25519" } },
  "eid": "evt_8f3k2n9p",
  "jti": "st-7c1e22",
  "iat": 1750000000,
  "exp": 1750086400,
  "max_uses": 1
}
]]>
</sourcecode>
</section>

<section anchor="presentation"><name>Presentation</name>
<t>The agent presents the subscribe token as the <tt>Signature-Key</tt> JWT on the subscription registration request, under the <tt>jwt</tt> scheme:</t>

<sourcecode type="http"><![CDATA[POST /appointments/waitlist HTTP/1.1
Host: resource.example
Content-Type: application/json
Signature-Input: sig=("@method" "@authority"
    "@path" "signature-key" "content-type");created=1750000000
Signature: sig=:...signature bytes...:
Signature-Key: sig=jwt;
    jwt="eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiYWEtc3Vic2NyaWJlK2p3dCIsImtpZCI6Ii4uLiJ9..."

{
  "event_types": ["slot.available"]
}
]]>
</sourcecode>
<t>On a subscription registration request the subscribe token is the <tt>Signature-Key</tt> JWT, in place of the agent token, person token, or auth token the agent presents on its other requests to the resource (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Keying Material). The <tt>cnf.jwk</tt> in the subscribe token provides the key the resource uses to verify the HTTP signature. The subscribe token is structurally analogous to the agent token — both are AP-signed JWTs carrying <tt>cnf.jwk</tt> — distinguished by <tt>typ</tt>.</t>
</section>

<section anchor="verification"><name>Verification</name>
<t>The resource MUST verify the subscribe token as follows:</t>

<ol spacing="compact">
<li>Verify the token per (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Common JWT Verification), with <tt>typ</tt> <tt>aa-subscribe+jwt</tt> and <tt>dwk</tt> <tt>aauth-agent.json</tt>. <tt>exp</tt> is judged by the verifier's own clock; <tt>iat</tt> is not a validity check.</li>
<li>Verify <tt>aud</tt> matches the resource's own URL.</li>
<li>Verify <tt>cnf.jwk</tt> matches the key used to sign the HTTP request.</li>
<li>Verify <tt>eid</tt> is present and non-empty.</li>
</ol>
<t>The subscribe token is carried in the <tt>Signature-Key</tt> header, so a subscribe token that fails any of these steps is a signature failure, answered with <tt>401</tt> and <tt>Signature-Error</tt> (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Verification (Server)).</t>
<t>After verification, the resource stores the subscription record with sufficient information to deliver events — at minimum <tt>{eid, iss}</tt> (the Event ID and the AP's issuer URL). When an event fires, the resource resolves the AP's <tt>event_endpoint</tt> from <tt>{iss}/.well-known/aauth-agent.json</tt> at delivery time, using standard HTTP caching for the well-known document.</t>
</section>

<section anchor="agent-context-mapping"><name>Agent Context Mapping</name>
<t>The <tt>eid</tt> is the agent's correlation key. The agent maintains a local mapping of <tt>eid</tt> values to internal context — for example, "eid <tt>evt_8f3k2n9p</tt> corresponds to the appointment waitlist for Dr. Smith, opened under the agent's current mission". This mapping is the agent's own concern and is not defined by this specification.</t>
</section>
</section>

<section anchor="subscription-registration"><name>Subscription Registration</name>

<section anchor="public-subscriptions"><name>Public Subscriptions</name>
<t>For event channels that do not require prior authorization, the agent presents the subscribe token (as the <tt>Signature-Key</tt> JWT) on a signed POST to the resource's subscription endpoint. No additional credential is required. The resource validates the subscribe token per <xref target="subscribe-token"/> and registers the subscription.</t>
</section>

<section anchor="protected-subscriptions"><name>Protected Subscriptions</name>
<t>Some event channels require the agent to be authorized before it can register a subscription — for example, subscribing to events for a specific patient's appointments, or events associated with a particular account. In these cases, the resource does not accept subscription registrations from arbitrary agents; only agents that have already been authorized in an earlier interaction may register.</t>
<t>This specification defines a <strong>pre-authorized subscription URL</strong> pattern for protected subscriptions:</t>

<ol spacing="compact">
<li>The agent makes an authenticated request to the resource, presenting a person token or auth token, since a protected subscription is bound to a person.</li>
<li>The resource, if subscription to events is available for the context established by this interaction, returns a <strong>subscription ticket URL</strong> — an HTTPS URL that encodes a short-lived, single-use authorization to register a subscription. The ticket URL is opaque and is valid only for the specific context (key, operation, and resource state) established in step 1.</li>
<li>The agent obtains a subscribe token from its AP.</li>
<li>The agent presents the subscribe token (as the <tt>Signature-Key</tt> JWT) on a signed POST to the subscription ticket URL. No person token or auth token is required at this step; the authorization is embedded in the URL. The request body MAY include additional parameters as defined by the resource's AsyncAPI channel schema.</li>
<li>The resource validates the subscribe token, verifies the ticket in the URL is valid for the calling key (the JWK Thumbprint of the subscribe token's <tt>cnf.jwk</tt> equals the thumbprint recorded when the ticket was issued, see <xref target="pre-authorized-subscription-url-security"/>) and has not been used before, and registers the subscription.</li>
</ol>
<t>The subscription ticket URL is resource-controlled: the resource issues it, defines its scope and expiry, and enforces its single-use constraint. The ticket is not defined by this specification beyond the pattern above.</t>
<t>Example response from step 2:</t>

<sourcecode type="json"><![CDATA[{
  "status": "unavailable",
  "next_available": "2026-08-24",
  "waitlist": {
    "subscribe_url": "https://resource.example/waitlist/st_9k2m_abc123",
    "event_types": ["slot.available"],
    "offer_window_seconds": 300
  }
}
]]>
</sourcecode>
<t>Example subscription registration from step 4:</t>

<sourcecode type="http"><![CDATA[POST /waitlist/st_9k2m_abc123 HTTP/1.1
Host: resource.example
Content-Type: application/json
Signature-Input: sig=("@method" "@authority"
    "@path" "signature-key" "content-type");created=1750000000
Signature: sig=:...signature bytes...:
Signature-Key: sig=jwt;
    jwt="eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiYWEtc3Vic2NyaWJlK2p3dCIsImtpZCI6Ii4uLiJ9..."

{
  "event_types": ["slot.available"]
}
]]>
</sourcecode>
<t>The HTTP signature covers the request path (including the ticket), cryptographically binding the subscribe token's identity to this specific ticket URL.</t>
<t>The resource SHOULD include the subscription ticket URL in an AsyncAPI channel parameter (<xref target="AsyncAPI"/>) so that agents that discover the resource's event capabilities through its AsyncAPI document know to obtain the URL from a prior API response.</t>
</section>
</section>

<section anchor="event-token"><name>Event Token</name>

<section anchor="structure-1"><name>Structure</name>
<t>When an event fires, the resource issues an event token: a JWT signed by the resource with the following claims:</t>
<t>Header:</t>

<ul spacing="compact">
<li><tt>alg</tt>: per (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Signature Algorithms)</li>
<li><tt>typ</tt>: <tt>aa-event+jwt</tt></li>
<li><tt>kid</tt>: Key identifier (resource's signing key)</li>
</ul>
<t>Required payload claims:</t>

<ul spacing="compact">
<li><tt>iss</tt>: Resource URL.</li>
<li><tt>dwk</tt>: <tt>aauth-resource.json</tt> — the well-known metadata document name for key discovery (<xref target="I-D.hardt-httpbis-signature-key"/>). A resource that delivers events makes signed calls, so its resource metadata carries <tt>jwks_uri</tt> (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Resource Metadata).</li>
<li><tt>aud</tt>: Agent identifier (<tt>aauth:local@domain</tt>). The agent MUST verify this matches its own identifier. Agent identifiers are compared exactly and case-sensitively (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Agent Identifiers).</li>
<li><tt>eid</tt>: Event ID. MUST match the <tt>eid</tt> from the subscribe token for this subscription. The AP uses the <tt>eid</tt> to look up the subscription record and route to the agent. The agent uses the <tt>eid</tt> to look up its local context mapping.</li>
<li><tt>jti</tt>: Unique identifier for this event token. <tt>(iss, jti)</tt> identifies one event; it is the deduplication key at the AP and the agent. The <tt>eid</tt> cannot serve, since every event on a subscription carries the same <tt>eid</tt>.</li>
<li><tt>iat</tt>: Issued-at timestamp.</li>
<li><tt>exp</tt>: Expiration timestamp. The agent MUST NOT act on an event token with <tt>exp</tt> in the past. The meaning of <tt>exp</tt> is event-specific — for time-sensitive events, it encodes the deadline by which the agent must act.</li>
</ul>
<t>Optional payload claims:</t>

<ul spacing="compact">
<li><tt>body_s256</tt>: The base64url-encoded SHA-256 hash, without padding, of the request body bytes the resource sends with this event token. REQUIRED when the delivery carries a body, and absent when it carries none. The agent receives the body from the AP and cannot verify the resource's HTTP signature, so this claim is what lets it verify the payload end to end.</li>
</ul>
<t>The event token MUST NOT contain a <tt>cnf</tt> claim. The event token is a self-issued JWT per the <tt>self-jwt</tt> Signature-Key scheme (<xref target="I-D.hardt-httpbis-signature-key"/>): the resource is both the JWT issuer and the HTTP request signer, and the key identified by <tt>kid</tt> in the resource's JWKS verifies both the JWT and the HTTP Message Signature.</t>
<t>The event token is the transport and security layer. It carries no event-specific data. Event-specific content is delivered as the POST body alongside the event token (see <xref target="event-delivery"/>), and <tt>body_s256</tt> binds the token to that body.</t>
<t>Example event token payload:</t>

<sourcecode type="json"><![CDATA[{
  "iss": "https://resource.example",
  "dwk": "aauth-resource.json",
  "aud": "aauth:k7q3p9n2@ap.example",
  "eid": "evt_8f3k2n9p",
  "jti": "ev-4d2a91",
  "body_s256": "30HnqDZZZKuRXvaahsaXMeL7bBJBNUT9TrTpqvOIkH8",
  "iat": 1750200000,
  "exp": 1750200300
}
]]>
</sourcecode>
</section>
</section>

<section anchor="event-delivery"><name>Event Delivery: Resource to AP</name>

<section anchor="request"><name>Request</name>
<t>When an event fires for an active subscription, the resource posts to the AP's <tt>event_endpoint</tt>, presenting the event token as the <tt>Signature-Key</tt> JWT using the <tt>self-jwt</tt> scheme (<xref target="I-D.hardt-httpbis-signature-key"/>). The POST body is the AsyncAPI-defined payload for the event type (OPTIONAL — omitted if the event carries no payload):</t>

<sourcecode type="http"><![CDATA[POST /events HTTP/1.1
Host: ap.example
Content-Type: application/json
Content-Digest:
    sha-256=:30HnqDZZZKuRXvaahsaXMeL7bBJBNUT9TrTpqvOIkH8=:
Signature-Input: sig=("@method" "@authority"
    "@path" "signature-key" "content-type" "content-digest");created=1750200000
Signature: sig=:...resource signing key signature bytes...:
Signature-Key: sig=self-jwt;
    jwt="eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiYWEtZXZlbnQrand0Iiwia2lkIjoiLi4uIn0..."

{
  "event_type": "slot.available",
  "slot_time": "2026-07-15T10:00:00Z"
}
]]>
</sourcecode>
<t>The event token in <tt>Signature-Key</tt> provides the resource's identity (<tt>iss</tt>), routing and authorization claims (<tt>eid</tt>, <tt>aud</tt>, <tt>exp</tt>), the event's identity (<tt>jti</tt>), and the digest of the body (<tt>body_s256</tt>). Unlike agent tokens and subscribe tokens (which use the <tt>jwt</tt> scheme with <tt>cnf.jwk</tt>), the event token uses the <tt>self-jwt</tt> scheme (<xref target="I-D.hardt-httpbis-signature-key"/>): the resource is both the JWT issuer and the HTTP request signer, so no <tt>cnf.jwk</tt> is needed. The resource has a stable JWKS discoverable from <tt>{iss}/.well-known/{dwk}</tt>, and the AP uses the same key (identified by <tt>kid</tt> in the JWT header) to verify both the JWT signature and the HTTP signature. The request body structure is defined by the resource's AsyncAPI message schema for the event type (see <xref target="event-discovery"/>). The AP forwards both the event token and the payload body to the agent.</t>
<t>The signature covers the base components of (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Covered Components). When the request carries a body, it MUST also cover <tt>content-digest</tt> and <tt>content-type</tt>, as a request to a revocation endpoint does: the event endpoint is defined by this document rather than by the AP's own API, and the body is what the AP forwards to the agent.</t>
<t>The resource resolves the AP's <tt>event_endpoint</tt> from <tt>{iss}/.well-known/aauth-agent.json</tt>, where <tt>iss</tt> is the subscribe token's, at delivery time, using standard HTTP caching for the AP's well-known document.</t>
</section>

<section anchor="ap-validation"><name>AP Validation</name>
<t>The AP MUST validate the event delivery request as follows:</t>

<ol spacing="compact">
<li>Extract the event token JWT from the <tt>Signature-Key</tt> header (the <tt>self-jwt</tt> scheme). Verify it per (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Common JWT Verification), with <tt>typ</tt> <tt>aa-event+jwt</tt> and <tt>dwk</tt> <tt>aauth-resource.json</tt>. Verify <tt>cnf</tt> is absent, per the <tt>self-jwt</tt> scheme (<xref target="I-D.hardt-httpbis-signature-key"/>).</li>
<li>The AP MAY deduplicate on <tt>(iss, jti)</tt> and drop a delivery it has already recorded.</li>
<li>Verify the HTTP signature using the same key (matched by <tt>kid</tt>), per the <tt>self-jwt</tt> scheme: the JWT signing key and the HTTP signing key are the same key, discoverable from the resource's well-known document.</li>
<li>If the request carries a body, verify <tt>body_s256</tt> is present and equals the SHA-256 hash of the body bytes. If it carries none, verify <tt>body_s256</tt> is absent. A mismatch is a token verification failure, answered <tt>401</tt> with <tt>Signature-Error: error=invalid_jwt</tt>.</li>
<li>Look up the subscription record by <tt>eid</tt>. If no active subscription exists for this <tt>eid</tt>, return <tt>404</tt>.</li>
<li>Verify <tt>iss</tt> matches the resource recorded at subscription time (the <tt>aud</tt> of the subscribe token for this <tt>eid</tt>).</li>
<li>If <tt>max_uses</tt> is set in the subscribe token, verify the use count has not been exceeded. Increment the use count atomically. If the use limit is reached, the AP MAY mark the subscription as complete after delivery.</li>
<li>Verify the event token <tt>aud</tt> matches the agent identifier in the subscription record. A mismatch is a token verification failure, answered <tt>401</tt> with <tt>Signature-Error: error=invalid_jwt</tt>.</li>
</ol>
<t>If all checks pass, the AP returns <tt>202 Accepted</tt> and proceeds with delivery to the agent. The AP MUST NOT return <tt>202</tt> before the event has been durably recorded for delivery. If <tt>max_uses</tt> was set in the subscribe token, the AP MUST include a JSON response body with a <tt>remaining_uses</tt> field indicating how many more event tokens the AP will accept for this <tt>eid</tt>:</t>

<sourcecode type="http"><![CDATA[HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "remaining_uses": 0
}
]]>
</sourcecode>
<t>When <tt>remaining_uses</tt> is <tt>0</tt>, the subscription is exhausted. The resource SHOULD clean up its subscription record and MAY prompt the agent to re-subscribe on the next interaction. When <tt>max_uses</tt> was not set, the AP returns <tt>202 Accepted</tt> with no body (or an empty JSON object).</t>
<t>The AP returns <tt>400</tt> for malformed requests, <tt>401</tt> with <tt>Signature-Error</tt> if the event token or the HTTP signature fails step 1, 3, 4, or 8, an expired event token included (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Verification (Server)), <tt>403</tt> if the resource does not match the subscription's authorized resource, and <tt>404</tt> if the <tt>eid</tt> is unknown, the subscription has expired, or <tt>max_uses</tt> has been exceeded.</t>
<t>Those last three are one condition from the resource's side: the AP has no subscription that will accept this event, and the answer is to clean up and re-subscribe. <tt>410 Gone</tt> would say more — that there was a subscription here and it is finished — but an AP can answer that only while it still holds a record it has no other use for, so the distinction would report its retention policy rather than any protocol state. Nothing is lost by collapsing them, because exhaustion is reported in-band: <tt>remaining_uses: 0</tt> in the <tt>202</tt> of the delivery that spent the subscription reaches the resource at the moment it can act on it. An event posted after that is a race or a resource that ignored the signal, and the <tt>404</tt> tells it what the earlier <tt>202</tt> already did.</t>
<t><tt>429 Too Many Requests</tt> is not used. It invites a retry, and none of these conditions is retryable; the base protocol's two uses of it — <tt>slow_down</tt> on a pending URL (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Polling Error Codes) and <tt>rate_limited</tt> at a revocation endpoint (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Revocation Response) — both mean come back later.</t>
</section>
</section>

<section anchor="ap-to-agent"><name>Event Delivery: AP to Agent</name>
<t>How the AP delivers the event token to the agent is platform-dependent and outside the scope of this specification. The AP is the agent's inbox; the internal mechanism is an implementation choice for the AP and agent. Whatever the mechanism, the AP MUST deliver the body bytes to the agent unchanged, with the event token, so that the agent can verify <tt>body_s256</tt>.</t>
<t>See <xref target="non-normative-ap-agent"/> for non-normative examples of AP-to-agent delivery for different platforms.</t>

<section anchor="agent-verification"><name>Agent Verification</name>
<t>Upon receiving an event token (and optional payload) from the AP, the agent MUST:</t>

<ol spacing="compact">
<li>Verify the token per (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Common JWT Verification), with <tt>typ</tt> <tt>aa-event+jwt</tt> and <tt>dwk</tt> <tt>aauth-resource.json</tt>. An event token whose <tt>exp</tt> has passed fails this step: the response window has closed, and the agent MUST NOT act on it.</li>
<li>Verify <tt>aud</tt> matches the agent's own identifier.</li>
<li>If <tt>body_s256</tt> is present, verify the AP delivered a body and that its SHA-256 hash equals <tt>body_s256</tt>. If <tt>body_s256</tt> is absent, verify no body was delivered. The agent MUST NOT act on an event whose body does not verify.</li>
<li>Look up <tt>eid</tt> in the agent's local context mapping to recover the context associated with this subscription.</li>
<li>Deduplicate: if the agent has already processed an event with this <tt>jti</tt> from this <tt>iss</tt>, it SHOULD ignore the duplicate. <tt>(iss, jti)</tt> is the idempotency key.</li>
</ol>
<t>Once the body verifies, the agent MAY use the payload directly. The payload structure is defined by the resource's AsyncAPI message schema for the event type.</t>
</section>
</section>

<section anchor="event-discovery"><name>Event Discovery</name>
<t>Resources describe their event capabilities using AsyncAPI (<xref target="AsyncAPI"/>) as an AAuth R3 vocabulary (<xref target="I-D.hardt-aauth-r3"/>).</t>

<section anchor="r3-vocabulary-identifier"><name>R3 Vocabulary Identifier</name>
<t>The vocabulary identifier for AAuth Events is <tt>urn:aauth:vocabulary:asyncapi</tt>, registered in the AAuth R3 Vocabulary Registry by R3 (<xref target="I-D.hardt-aauth-r3"/>), which also defines the operation entry format. Resources that support AAuth Events SHOULD declare this vocabulary in their AAuth resource metadata:</t>

<sourcecode type="json"><![CDATA[{
  "issuer": "https://resource.example",
  "r3_vocabularies": {
    "urn:aauth:vocabulary:openapi": "/openapi.json",
    "urn:aauth:vocabulary:asyncapi": "/asyncapi.json"
  }
}
]]>
</sourcecode>
</section>

<section anchor="asyncapi-document"><name>AsyncAPI Document</name>
<t>The resource's AsyncAPI document describes:</t>

<ul spacing="compact">
<li><strong>Channels</strong>: Event streams the agent may subscribe to. Channels MAY use parameterized addresses (e.g., <tt>/waitlist/{subscriptionTicket}</tt>) when the subscription endpoint URL is dynamic (see <xref target="protected-subscriptions"/>).</li>
<li><strong>Operations</strong>: <tt>receive</tt> operations on channels, with the security requirement and message schema.</li>
<li><strong>Messages</strong>: The payload schema for each event type. The AsyncAPI payload schema describes the event delivery POST body (see <xref target="event-delivery"/>). The AAuth event token envelope (<tt>iss</tt>, <tt>aud</tt>, <tt>eid</tt>, <tt>jti</tt>, <tt>exp</tt>, <tt>body_s256</tt>) is implicit and not part of the AsyncAPI schema.</li>
<li><strong>Security schemes</strong>: The AAuth subscribe token security scheme.</li>
</ul>
</section>

<section anchor="security-scheme"><name>Security Scheme</name>
<t>Resources MUST declare the AAuth subscribe token security scheme as follows:</t>

<sourcecode type="yaml"><![CDATA[securitySchemes:
  aauth_subscribe:
    type: http
    scheme: aauth-subscribe
    description: >
      AAuth Subscribe Token (typ: aa-subscribe+jwt), issued by the agent's Agent
      Provider, presented as the Signature-Key JWT with HTTP Message Signatures.
      See draft-hardt-aauth-events.
]]>
</sourcecode>
<t>Operations that require only a subscribe token declare:</t>

<sourcecode type="yaml"><![CDATA[security:
  - aauth_subscribe: []
]]>
</sourcecode>
<t>Operations that require a pre-authorized subscription URL (see <xref target="protected-subscriptions"/>) have no security scheme on the subscription endpoint itself — the subscription ticket in the URL carries the authorization. The resource SHOULD annotate such channels with a description noting that the subscription URL is obtained from a prior authenticated API call.</t>
</section>

<section anchor="example-asyncapi-document"><name>Example AsyncAPI Document</name>

<sourcecode type="yaml"><![CDATA[asyncapi: 3.0.0
info:
  title: Appointments Events
  version: 1.0.0

channels:
  waitlistPublic:
    address: /appointments/waitlist/public
    messages:
      slotAvailable:
        $ref: '#/components/messages/SlotAvailable'

  waitlistProtected:
    address: /appointments/waitlist/{subscriptionTicket}
    description: >
      Subscription URL returned by POST /appointments when no slot is
      immediately available and the calling agent is authorized for
      waitlist access. The subscriptionTicket is embedded in the URL
      and carries the authorization context.
    parameters:
      subscriptionTicket:
        description: Single-use ticket from the POST /appointments response.
    messages:
      slotAvailable:
        $ref: '#/components/messages/SlotAvailable'

operations:
  subscribePublicWaitlist:
    action: receive
    channel:
      $ref: '#/channels/waitlistPublic'
    security:
      - aauth_subscribe: []

  subscribeProtectedWaitlist:
    action: receive
    channel:
      $ref: '#/channels/waitlistProtected'

components:
  messages:
    SlotAvailable:
      contentType: application/json
      payload:
        type: object
        properties:
          event_type:
            type: string
            const: slot.available
          slot_time:
            type: string
            format: date-time
          doctor_id:
            type: string
        required:
          - event_type
          - slot_time

  securitySchemes:
    aauth_subscribe:
      type: http
      scheme: aauth-subscribe
      description: AAuth Subscribe Token as Signature-Key JWT
]]>
</sourcecode>
</section>
</section>

<section anchor="security-considerations"><name>Security Considerations</name>

<section anchor="subscribe-token-scope"><name>Subscribe Token Scope</name>
<t>The <tt>aud</tt> claim in the subscribe token restricts which resource may deliver events to the AP for this <tt>eid</tt>. If a resource attempts to deliver events for an <tt>eid</tt> issued to a different resource, the AP MUST reject the request <xref target="event-delivery"/>. This prevents a compromised resource from hijacking another resource's subscription channel.</t>
</section>

<section anchor="event-token-forgery"><name>Event Token Forgery</name>
<t>Event tokens are signed by the resource using the resource's own signing key. The agent verifies the event token against the resource's JWKS (<xref target="I-D.hardt-httpbis-signature-key"/>). A party without the resource's private key cannot forge a valid event token. There are no shared secrets in AAuth Events.</t>
</section>

<section anchor="replay-prevention"><name>Replay Prevention</name>
<t>The AP enforces <tt>max_uses</tt> per <tt>eid</tt>, rejects event tokens with <tt>exp</tt> in the past, and MAY deduplicate on <tt>(iss, jti)</tt>. The agent deduplicates on <tt>(iss, jti)</tt> <xref target="agent-verification"/>. These layers prevent replay: a captured event token cannot be re-delivered once the AP has tracked its delivery and the agent has processed it.</t>
</section>

<section anchor="subscribe-token-replay-at-registration"><name>Subscribe Token Replay at Registration</name>
<t>A subscribe token with a valid <tt>exp</tt> could in principle be presented to the resource's subscription endpoint more than once. An <tt>eid</tt> is unique only within the AP that issued it, so <tt>(iss, eid)</tt> is the deduplication key: the resource SHOULD reject subscription registration requests for an <tt>(iss, eid)</tt> it has already registered. Single-use enforcement of the subscription ticket URL (in protected subscriptions) provides an additional constraint.</t>
</section>

<section anchor="pre-authorized-subscription-url-security"><name>Pre-Authorized Subscription URL Security</name>
<t>The subscription ticket URL (see <xref target="protected-subscriptions"/>) encodes authorization from a prior authenticated context. Resources MUST ensure that subscription tickets are:</t>

<ul spacing="compact">
<li>Short-lived (expiry appropriate to the expected delay between issuing and using the ticket).</li>
<li>Single-use (the resource invalidates the ticket on first successful subscription registration).</li>
<li>Bound to the signing key that established the ticket. No token the resource verified when it issued the ticket carries an agent identifier (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Why No Agent Identifier Reaches a Resource); what it verified is the key in <tt>cnf</tt>. The resource MUST record the JWK Thumbprint of that key with the ticket, and MUST verify that the thumbprint of the subscribe token's <tt>cnf.jwk</tt> equals it when the ticket is used. The subscribe token's <tt>sub</tt> names the agent to its AP for delivery; it is not what binds the ticket.</li>
</ul>
</section>

<section anchor="ap-as-delivery-intermediary"><name>AP as Delivery Intermediary</name>
<t>The AP sees every event token delivered to an agent. The AP validates the event token's <tt>iss</tt>, <tt>aud</tt>, and <tt>eid</tt> claims but does not need to inspect resource-specific payload claims. APs SHOULD document their data retention policies for event tokens.</t>
<t><tt>body_s256</tt> lets the agent detect a payload the AP altered or substituted. It does not stop the AP withholding or delaying an event.</t>
</section>

<section anchor="event-endpoint-egress"><name>Event Endpoint Egress</name>
<t>The AP chooses its <tt>event_endpoint</tt>, and anyone can operate an AP. A resource that posted to whatever URL an AP's metadata named could be induced to send signed requests to its own internal addresses. For this reason <xref target="ap-metadata"/> requires egress admission (<xref target="I-D.hardt-httpbis-signature-key"/>, Scheme-Specific Risks) before a resource fetches the AP's metadata or posts to its <tt>event_endpoint</tt>.</t>
</section>

<section anchor="resource-enumeration"><name>Resource Enumeration</name>
<t>A resource that exposes its AsyncAPI document publicly reveals what event types it emits. This may be intentional (public API). Resources that wish to restrict event type discovery MAY gate their AsyncAPI document with AAuth authentication.</t>
</section>
</section>

<section anchor="privacy-considerations"><name>Privacy Considerations</name>

<section anchor="agent-identifier-stability"><name>Agent Identifier Stability</name>
<t>The <tt>sub</tt> claim in the subscribe token carries the agent's stable identifier, and <tt>iss</tt> names its AP. An agent that subscribes at a resource discloses both to that resource, by its own choice. This departs from the AAuth Protocol, in which no token a resource reads carries an agent identifier outside agent identity access and resource-managed access (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Agent Identity Access and Why No Agent Identifier Reaches a Resource). A resource that receives subscribe tokens can correlate an agent's subscription activity over time, and for a protected subscription can link the agent to the person the ticket was issued for. This is the intended property — the resource needs to know which agent subscribed and which AP to deliver to. Agents and APs should be aware that subscription registrations leave a record at the resource.</t>
</section>

<section anchor="ap-visibility"><name>AP Visibility</name>
<t>Delivery through the AP necessarily shows the AP which resources its agents subscribe at: each subscribe token it issues names the resource in <tt>aud</tt>, and each event token it receives names the resource in <tt>iss</tt>. This departs from the AAuth Protocol, which keeps from the AP which resources a person uses through its agents (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Why a PS Reports Nothing to the Agent Provider). It is inherent in the AP acting as the agent's inbox.</t>
</section>

<section anchor="event-content"><name>Event Content</name>
<t>The event token carries no event-specific data — it is the security and routing envelope only. Event-specific content travels as the POST body (see <xref target="event-delivery"/>), which is also visible to the AP during routing. Resources SHOULD NOT include sensitive personal data in the payload beyond what is necessary for the agent to evaluate relevance. Sensitive details SHOULD be fetched by the agent from the resource's data API, on a signed request presenting the token the resource requires for that data (<xref target="I-D.hardt-oauth-aauth-protocol"/>, Resource Access).</t>
</section>
</section>

<section anchor="iana-considerations"><name>IANA Considerations</name>

<section anchor="media-type-registrations"><name>Media Type Registrations</name>
<t>This specification registers the following media types. Their subtypes, <tt>aa-subscribe+jwt</tt> and <tt>aa-event+jwt</tt>, are the <tt>typ</tt> values of the subscribe token and the event token (<xref target="RFC7515"/>, Section 4.1.9).</t>

<section anchor="application-aa-subscribe-jwt"><name>application/aa-subscribe+jwt</name>

<ul spacing="compact">
<li>Type name: application</li>
<li>Subtype name: aa-subscribe+jwt</li>
<li>Required parameters: N/A</li>
<li>Optional parameters: N/A</li>
<li>Encoding considerations: binary; a JWT is a sequence of Base64url-encoded parts separated by period characters</li>
<li>Security considerations: See <xref target="security-considerations"/></li>
<li>Interoperability considerations: N/A</li>
<li>Published specification: This document, <xref target="subscribe-token"/></li>
<li>Applications that use this media type: AAuth agent providers, agents, and resources</li>
<li>Fragment identifier considerations: N/A</li>
</ul>
</section>

<section anchor="application-aa-event-jwt"><name>application/aa-event+jwt</name>

<ul spacing="compact">
<li>Type name: application</li>
<li>Subtype name: aa-event+jwt</li>
<li>Required parameters: N/A</li>
<li>Optional parameters: N/A</li>
<li>Encoding considerations: binary; a JWT is a sequence of Base64url-encoded parts separated by period characters</li>
<li>Security considerations: See <xref target="security-considerations"/></li>
<li>Interoperability considerations: N/A</li>
<li>Published specification: This document, <xref target="event-token"/></li>
<li>Applications that use this media type: AAuth resources, agent providers, and agents</li>
<li>Fragment identifier considerations: N/A</li>
</ul>
</section>
</section>

<section anchor="jwt-claims-registrations"><name>JWT Claims Registrations</name>
<t>This specification registers the following claims in the IANA "JSON Web Token Claims" registry established by <xref target="RFC7519"/>:</t>
<table>
<thead>
<tr>
<th>Claim Name</th>
<th>Claim Description</th>
<th>Change Controller</th>
<th>Reference</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>eid</tt></td>
<td>Event ID: the AP-generated identifier of a subscription, in subscribe tokens and event tokens</td>
<td>IETF</td>
<td>This document, <xref target="subscribe-token"/></td>
</tr>

<tr>
<td><tt>max_uses</tt></td>
<td>The number of event tokens the AP accepts for a subscription, in a subscribe token</td>
<td>IETF</td>
<td>This document, <xref target="subscribe-token"/></td>
</tr>

<tr>
<td><tt>body_s256</tt></td>
<td>SHA-256 hash of the request body delivered with an event token</td>
<td>IETF</td>
<td>This document, <xref target="event-token"/></td>
</tr>
</tbody>
</table></section>
</section>

<section anchor="implementation-status"><name>Implementation Status</name>
<t><em>Note: This section is to be removed before publishing as an RFC.</em></t>
<t>TBD</t>
</section>

<section anchor="document-history"><name>Document History</name>
<t><em>Note: This section is to be removed before publishing as an RFC.</em></t>

<ul spacing="compact">
<li><t>draft-hardt-aauth-events-00</t>

<ul spacing="compact">
<li>The event token carries <tt>body_s256</tt>, the SHA-256 hash of the delivery body, REQUIRED when there is a body. The AP delivers the body unchanged, and the agent verifies it before acting, since it cannot verify the resource's HTTP signature. The subscribe token carries a REQUIRED <tt>jti</tt>. An event token whose <tt>aud</tt> does not name the subscribing agent is answered <tt>401</tt> with <tt>Signature-Error: error=invalid_jwt</tt>. A resource applies egress admission before fetching the AP's metadata or posting to its <tt>event_endpoint</tt>. Privacy Considerations state that subscribing discloses the agent identifier and AP to the resource, which departs from the protocol, and that delivery shows the AP which resources its agents subscribe at. Protected subscriptions require a person token or auth token because they bind to a person. Dropped the JWT type table: IANA has no such registry, and the media type registrations carry the <tt>typ</tt> values.</li>
<li>Aligned with the published AAuth Protocol -11. The subscribe token takes the place of whichever token the agent otherwise presents to the resource. A subscribe token or event token that fails verification is answered <tt>401</tt> with <tt>Signature-Error</tt>, and an agent MUST NOT act on an event token whose <tt>exp</tt> has passed. Event delivery with a body covers <tt>content-digest</tt> and <tt>content-type</tt>. A resource verifies the AP's metadata document and deduplicates registrations on <tt>(iss, eid)</tt>. Agent identifiers are compared case-sensitively. Registered the media types and the <tt>eid</tt>, <tt>max_uses</tt>, and <tt>body_s256</tt> claims. Protocol citations name their sections.</li>
<li>An exhausted subscription is answered <tt>404</tt>, with an unknown <tt>eid</tt> and an expired subscription, rather than <tt>429 Too Many Requests</tt>. None of the three is retryable, while <tt>429</tt> invites a retry — the meaning it carries in both of the base protocol's uses of it. They are answered alike because an AP can distinguish them only while it holds a record it has no other use for, which would make the status code a report of its retention policy; exhaustion reaches the resource in-band anyway, as <tt>remaining_uses: 0</tt> in the <tt>202</tt> of the delivery that spent the subscription.</li>
<li>Consistency pass against AAuth Protocol -11. Event tokens carry <tt>jti</tt>, and the AP and agent deduplicate on <tt>(iss, jti)</tt>; deduplicating on <tt>eid</tt> would have dropped every event after the first on an unlimited subscription. The protected-subscription flow binds the ticket to the JWK Thumbprint of the subscribe token's <tt>cnf.jwk</tt>, matching Security Considerations. <tt>iat</tt> is not a validity check. The event payload is the POST body, not a <tt>payload</tt> member, and the AsyncAPI message <tt>contentType</tt> is <tt>application/json</tt>. Token <tt>alg</tt> rules and the common verification steps point at the protocol rather than restating it. Example JWTs use <tt>Ed25519</tt> and the registered <tt>typ</tt> values. The AsyncAPI vocabulary identifier is registered by R3, not defined here.</li>
<li>Referenced the AAuth Protocol, AAuth Bootstrap, and R3 by their datatracker document URLs, which track the latest revision.</li>
<li>Algorithm identifiers: <tt>Ed25519</tt> rather than the deprecated polymorphic <tt>EdDSA</tt>; the <tt>cnf.jwk</tt> example carries the <tt>alg</tt> member now required of every conveyed key.</li>
<li>Initial draft.</li>
</ul></li>
</ul>
</section>

<section anchor="acknowledgments"><name>Acknowledgments</name>
<t>The author would like to thank reviewers for their feedback on concepts and earlier drafts: Rohit Khare.</t>
</section>

</middle>

<back>
<references><name>References</name>
<references><name>Normative References</name>
<reference anchor="I-D.hardt-aauth-r3" target="https://datatracker.ietf.org/doc/draft-hardt-aauth-r3">
  <front>
    <title>AAuth Rich Resource Requests (R3)</title>
    <author fullname="Dick Hardt" initials="D." surname="Hardt">
      <organization>Hellō</organization>
    </author>
    <date year="2026"/>
  </front>
</reference>
<reference anchor="I-D.hardt-httpbis-signature-key" target="https://datatracker.ietf.org/doc/draft-hardt-httpbis-signature-key">
  <front>
    <title>HTTP Signature Keys</title>
    <author fullname="Dick Hardt" initials="D." surname="Hardt">
      <organization>Hellō</organization>
    </author>
    <author fullname="Thibault Meunier" initials="T." surname="Meunier">
      <organization>Cloudflare</organization>
    </author>
    <date year="2026"/>
  </front>
</reference>
<reference anchor="I-D.hardt-oauth-aauth-protocol" target="https://datatracker.ietf.org/doc/draft-hardt-oauth-aauth-protocol">
  <front>
    <title>AAuth Protocol</title>
    <author fullname="Dick Hardt" initials="D." surname="Hardt">
      <organization>Hellō</organization>
    </author>
    <date year="2026"/>
  </front>
</reference>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.7515.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.7519.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.7800.xml"/>
</references>
<references><name>Informative References</name>
<reference anchor="AsyncAPI" target="https://www.asyncapi.com/docs/reference/specification/v3.0.0">
  <front>
    <title>AsyncAPI Specification 3.0.0</title>
    <author>
      <organization>AsyncAPI Initiative</organization>
    </author>
    <date year="2023"/>
  </front>
</reference>
<reference anchor="I-D.hardt-aauth-bootstrap" target="https://datatracker.ietf.org/doc/draft-hardt-aauth-bootstrap">
  <front>
    <title>AAuth Bootstrap Guidance</title>
    <author fullname="Dick Hardt" initials="D." surname="Hardt">
      <organization>Hellō</organization>
    </author>
    <date year="2026"/>
  </front>
</reference>
</references>
</references>

<section anchor="design-rationale"><name>Design Rationale</name>
<t>This appendix explains the key design decisions in AAuth Events and the alternatives considered.</t>

<section anchor="why-the-ap-is-the-delivery-intermediary"><name>Why the AP Is the Delivery Intermediary</name>
<t>Agents are workloads, not servers. They spin up, execute, and terminate. They run behind NAT, inside containers, or on mobile devices. They have no stable public endpoint.</t>
<t>Every existing push delivery mechanism (webhooks, WebSub, CIBA ping/push mode, W3C Web Push) assumes the subscriber has a stable HTTP endpoint. W3C Web Push is the closest analog to what AAuth Events does — it uses a browser push service (Google/Apple/Mozilla) as the subscriber's stable address. AAuth Events uses the AP in this role, with two improvements: the AP already has a trust relationship with the agent (it issued the agent's agent token), and the subscriber's identity is cryptographic (not just an opaque push service subscription).</t>
<t>The AP-as-inbox pattern mirrors how email works: you do not need to be online when someone sends you mail. The mail server is the stable address. AAuth Events gives agents the same property for event delivery.</t>
</section>

<section anchor="why-the-subscribe-token-is-the-signature-key-jwt"><name>Why the Subscribe Token Is the Signature-Key JWT</name>
<t>The subscribe token simultaneously serves two functions: it proves the agent's identity (via <tt>cnf.jwk</tt> + HTTP signature) and registers the subscription (via <tt>eid</tt>, <tt>aud</tt>, <tt>exp</tt>). Presenting it as the <tt>Signature-Key</tt> JWT means a single signed HTTP request to the subscription endpoint accomplishes both without a separate credential or header.</t>
<t>This is structurally analogous to the agent token — both are AP-signed JWTs with <tt>cnf.jwk</tt>, distinguished by <tt>typ</tt>. The resource's verification path is the same whether it is processing a request under agent identity access with an agent token or a subscription registration with a subscribe token.</t>
<t>The alternative — a separate header or body parameter carrying the subscribe token alongside the agent token, person token, or auth token the agent otherwise presents — was rejected because it requires two credentials where the subscribe token alone is sufficient.</t>
</section>

<section anchor="why-aud-in-the-subscribe-token-is-the-resource"><name>Why <tt>aud</tt> in the Subscribe Token Is the Resource</name>
<t>The <tt>aud</tt> claim restricts event delivery authorization to a specific resource. Only the resource named in <tt>aud</tt> may deliver events for this <tt>eid</tt> to the AP. This prevents:</t>

<ul spacing="compact">
<li>A compromised resource from injecting events into another agent's subscription channels.</li>
<li>The AP from accepting events from unexpected callers.</li>
</ul>
<t>The AP enforces this by matching the calling resource (identified by its HTTP signature) against the <tt>aud</tt> in the subscribe token stored in the subscription record.</t>
</section>

<section anchor="why-exp-is-the-jwt-validity-period-not-the-subscription-lifetime"><name>Why <tt>exp</tt> Is the JWT Validity Period, Not the Subscription Lifetime</name>
<t>Subscription lifetime is a negotiation between the agent and the resource at registration time. The resource has its own policy on maximum subscription duration. These durations can be days or months and are resource-specific.</t>
<t>The subscribe token's <tt>exp</tt> is the standard JWT validity window — how long the resource may accept this token for registration. Conflating JWT validity with subscription lifetime would either force a very long-lived token (security concern: replay window) or a very short subscription (UX concern: subscriptions expire before they're useful).</t>
<t>The resource stores the subscription record with whatever lifetime the agent and resource negotiate at registration. The subscribe token is a registration credential, not a subscription policy document.</t>
</section>

<section anchor="why-max-uses-is-in-the-subscribe-token"><name>Why <tt>max_uses</tt> Is in the Subscribe Token</name>
<t><tt>max_uses</tt> is the AP's throttle on how many event tokens it will accept for a given <tt>eid</tt>. It is AP-enforced, not resource-enforced. Placing it in the subscribe token — which the AP issued and controls — makes it AP-policy without requiring a separate AP configuration step.</t>
<t>For single-shot events (confirm this reservation), <tt>max_uses: 1</tt> ensures the AP accepts exactly one event. For ongoing subscriptions, <tt>max_uses</tt> is omitted (unlimited). When <tt>max_uses</tt> is absent, there is no sentinel value — absence means unlimited, avoiding any need for a special value such as -1.</t>
<t>The AP informs the resource of remaining uses in the <tt>202 Accepted</tt> response body after each delivery. The resource SHOULD use <tt>remaining_uses: 0</tt> as the signal to clean up its subscription record and prompt the agent to re-subscribe. This keeps the AP as the enforcement point while giving the resource the state it needs to manage the subscription lifecycle.</t>
</section>

<section anchor="why-the-event-token-is-the-transport-layer-not-the-data-layer"><name>Why the Event Token Is the Transport Layer, Not the Data Layer</name>
<t>The event token carries only what is needed for security, routing, and correlation: <tt>iss</tt>, <tt>aud</tt>, <tt>eid</tt>, <tt>jti</tt>, <tt>exp</tt>, and <tt>body_s256</tt>. It is the cryptographic layer — the AP uses it to authenticate the resource, look up the subscription, and verify the delivery is authorized. The agent uses it to verify authenticity, verify the body, and look up its context via <tt>eid</tt>.</t>
<t>Event-specific data travels as the POST body. The AsyncAPI message schema for the event type defines the payload structure. This separation keeps the JWT minimal and avoids embedding event data in a signed-but-not-encrypted envelope. The token carries a digest of the body instead: the agent receives the body from the AP, not from the resource, and cannot verify the resource's HTTP signature, so <tt>body_s256</tt> is what carries the resource's signature over the payload to the agent. For events where the agent needs full details beyond the payload, it fetches them from the resource's data API, presenting the token the resource requires for that data.</t>
</section>

<section anchor="why-the-event-token-uses-the-self-jwt-scheme"><name>Why the Event Token Uses the self-jwt Scheme</name>
<t>Agent tokens and subscribe tokens use the <tt>jwt</tt> Signature-Key scheme because they are delegation credentials: the AP issues the JWT, and the JWT delegates HTTP signing authority to the agent's key via <tt>cnf.jwk</tt>. The event token has no delegation — the resource issues the JWT and signs the HTTP request itself, with the same key. The <tt>self-jwt</tt> scheme (<xref target="I-D.hardt-httpbis-signature-key"/>) models exactly this: the JWT issuer and the HTTP signer are the same party, <tt>cnf</tt> is absent, and the key discovered from <tt>{iss}/.well-known/{dwk}</tt> (matched by <tt>kid</tt>) verifies both the JWT and the HTTP Message Signature. Using <tt>self-jwt</tt> lets the event token carry application claims (<tt>eid</tt>, <tt>aud</tt>, <tt>exp</tt>, <tt>body_s256</tt>) in the Signature-Key JWT without inventing an AAuth-specific extension to the <tt>jwt</tt> scheme.</t>
</section>

<section anchor="why-exp-in-the-event-token-is-the-response-window"><name>Why <tt>exp</tt> in the Event Token Is the Response Window</name>
<t>The <tt>exp</tt> claim in the event token defines how long the agent has to respond to the event. Its meaning is event-specific: for a waitlist slot, it is the deadline by which the agent must claim the slot; for a shipping confirmation, it may be a much longer acknowledgment window.</t>
<t>The AP delivers events in near real-time. If the AP cannot deliver an event before its <tt>exp</tt>, the agent should not act on it (the response window has closed). The agent verifies <tt>exp</tt> before acting.</t>
</section>

<section anchor="why-protected-subscriptions-use-a-pre-authorized-url"><name>Why Protected Subscriptions Use a Pre-Authorized URL</name>
<t>For protected event channels, the resource needs to verify that the subscribing agent has been authorized in a prior interaction before accepting the subscription. The naive approach — requiring both a person token or auth token and a subscribe token on the subscription call — is awkward because the two tokens serve different purposes and conveying a person token or auth token alongside a <tt>Signature-Key</tt> subscribe token has no established AAuth pattern.</t>
<t>The pre-authorized subscription URL pattern solves this cleanly: authorization is captured in the prior authenticated interaction, and the resource returns a ticket URL that encodes this context. The agent then presents only the subscribe token at the ticket URL. The HTTP signature covers the URL path (including the ticket), binding the subscribe token's identity to this specific authorization context.</t>
<t>This mirrors established patterns (OAuth authorization codes, S3 presigned URLs) while preserving the AAuth Events invariant: all subscription endpoints accept only the subscribe token as the <tt>Signature-Key</tt> JWT.</t>
</section>

<section anchor="why-asyncapi-is-the-discovery-vocabulary"><name>Why AsyncAPI Is the Discovery Vocabulary</name>
<t>AsyncAPI is the de facto standard for describing event-driven APIs. It describes channels, message schemas, security requirements, and (via parameterized addresses) dynamic subscription endpoints. The AAuth R3 vocabulary framework already accommodates multiple vocabularies per resource — AsyncAPI sits naturally alongside OpenAPI for synchronous operations.</t>
<t>The target reader of an AAuth resource's AsyncAPI document is an AAuth-capable agent, not generic AsyncAPI tooling. This means the <tt>aauth_subscribe</tt> security scheme (type: <tt>http</tt>, scheme: <tt>aauth-subscribe</tt>) does not need to be understood by Swagger UI or code generators — it is a declaration for the agent's benefit, interpreted per this specification.</t>
</section>

<section anchor="comparison-to-existing-patterns"><name>Comparison to Existing Patterns</name>
<table>
<thead>
<tr>
<th/>
<th>AAuth Events</th>
<th>Webhooks</th>
<th>WebSub</th>
<th>CIBA (ping)</th>
<th>Web Push</th>
</tr>
</thead>

<tbody>
<tr>
<td>Receiver needs public URL</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>No</td>
</tr>

<tr>
<td>Caller identity</td>
<td>Cryptographic (resource key)</td>
<td>HMAC shared secret</td>
<td>None</td>
<td>Client creds</td>
<td>Push service</td>
</tr>

<tr>
<td>Subscriber identity at resource</td>
<td>Agent identifier (<tt>sub</tt>)</td>
<td>None</td>
<td>None</td>
<td>Client ID</td>
<td>Subscription ID</td>
</tr>

<tr>
<td>Per-operation subscription</td>
<td>Yes</td>
<td>No (account-level)</td>
<td>No</td>
<td>Yes</td>
<td>No</td>
</tr>

<tr>
<td>General event types</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>No (auth only)</td>
<td>Yes</td>
</tr>

<tr>
<td>Standard description format</td>
<td>AsyncAPI (R3)</td>
<td>Proprietary</td>
<td>Atom/RSS</td>
<td>N/A</td>
<td>None</td>
</tr>
</tbody>
</table></section>
</section>

<section anchor="non-normative-ap-agent"><name>Non-Normative AP-to-Agent Delivery Examples</name>
<t>How the AP delivers an event token to an agent is platform-dependent and not specified by this document. The following examples illustrate common patterns, paralleling the approach taken in <xref target="I-D.hardt-aauth-bootstrap"/> for agent token acquisition.</t>

<section anchor="workload-agents"><name>Workload Agents</name>
<t>A workload agent (running in a cloud function, container, or batch job) may poll the AP for pending event tokens on startup, using an AP-internal endpoint. The AP acts as a durable inbox — storing event tokens until the workload polls. The workload validates and processes pending events before beginning its primary task.</t>
</section>

<section anchor="mobile-agents"><name>Mobile Agents</name>
<t>A mobile agent may receive events via the platform's native push notification infrastructure (APNs on iOS, FCM on Android). The AP holds a push token registered by the agent at enrollment time and delivers event tokens to the agent via push notification. The agent wakes on receipt, fetches the full event token from the AP if needed, and processes it.</t>
</section>

<section anchor="web-agents"><name>Web Agents</name>
<t>A web agent with a persistent session may receive events via a server-sent event (SSE) or WebSocket connection that the agent maintains to the AP. The AP streams event tokens over this connection as they arrive.</t>
</section>

<section anchor="self-hosted-agents"><name>Self-Hosted Agents</name>
<t>A self-hosted agent may receive events in two ways depending on whether it manages its own AP or delegates to an external one.</t>
<t>If the agent acts as its own AP (<xref target="I-D.hardt-aauth-bootstrap"/>), it may expose an internal event endpoint. Events are delivered directly to this endpoint by the resource — the AP and agent are collocated.</t>
<t>If the agent uses an external AP service, it maintains an outbound persistent connection (SSE, WebSocket, or a similar mechanism) to the AP's inbox service. The AP delivers event tokens and payloads over this connection as they arrive. The self-hosted agent does not need a public inbound endpoint — the outbound connection to the AP is sufficient.</t>
</section>
</section>

</back>

</rfc>
