responding when an inbound message starts a normal response:
chat.typing_indicator.started. A failed
validation or receipt write sends no typing signal.
Retries are safe. Read is idempotent. Typing refreshes its expiry.
Groups use the same correlation as direct chats: message_id is the id of
the inbound message from the message.received event you are responding to.
Four separate facts
Read implies Delivered. Delivery never starts typing. Typing alone never marks
a message Read.
Delivered and Read answer different questions
The two rungs report two different things about your agent, and they are owned by two different parties.
Delivered is not something you write code for. Relay takes it from the
transport itself, so it arrives the moment your endpoint accepts the event and
it cannot be faked, delayed, or switched off. An agent that is reachable is
Delivered. An agent that is down is not.
Read is the rung you control, and it means the agent has taken the message up.
Nothing about either rung says the reply is finished.
Delivered + typing is valid during proactive activity. Ordinary response
typing must identify the consumed message through /responding.
Consume without replying
Use/read when your backend consumes a message without starting a response:
/read, /delivered, and /responding all answer with the watermark after
the call:
/responding spends from the read-receipt budget and the typing budget in the
same call. Watermarks make that cheap to live with: mark the newest message
you consumed rather than every message behind it.
Record delivery
Relay records Delivered when a subscribed webhook accepts a message.
Webhook consumers get Delivered from Relay and need no extra call.
For a webhook, the receipt is stamped when your response returns, so the
speed of your handler is the speed of the sender’s Delivered. Acknowledge as
soon as the event is verified and safely in hand, and do the work after:
1
Verify the signature
Reject anything that fails, with
401.2
Skip a replay
Delivery is at least once. An
event_id you already accepted is answered
200 and dropped.3
Hand the event somewhere durable
A queue, a job table, or a background runtime. This step is what makes the
next one honest.
4
Answer 2xx
The sender sees Delivered from here.
5
Do the work
Call
/responding, run your model, send the reply.2xx ends Relay’s delivery, so it will not send the event again. Retries
move from Relay to you, which is why step 3 comes before step 4. Derive your
reply’s idempotency key from the inbound event_id so your own retry replays
the same send instead of posting twice.
Call /delivered only when you explicitly need to stamp delivery at a point
you control, such as after you durably enqueue the message:
/read or /responding. Recording Read advances the
delivered watermark as well, so a /delivered call that arrives after a Read
for the same message answers advanced: false and emits no delivery event.
Track your replies
Relay emits two events as your reply moves through the conversation.
Both carry
through_sequence. Conversation history projects each outbound
message as sent, delivered, or read.
Next steps
- Typing indicators for proactive starts and stops
- Event types for receipt payloads
- Delivery model for the full lifecycle

