# Relay agent integration brief
Use raw HTTPS and JSON at https://api.relayapp.im. Read the Agent Token from
RELAY_AGENT_TOKEN and send it as `Authorization: Bearer <token>`. Verify it
with `GET /v1/agents/me`.
## Receive
Agent events are webhook-only. Register a subscription with:
POST /v1/webhooks
{ "url": "https://...", "events": ["message.received", ...] }
Store the returned `signing_secret`. Relay sends only the Standard Webhooks
headers `webhook-id`, `webhook-timestamp`, and `webhook-signature`. Verify
HMAC-SHA256 over
`<webhook-id>.<webhook-timestamp>.<raw-request-body>` using the base64-decoded
bytes after `whsec_`. Reject timestamps outside five minutes and compare in
constant time.
Persist and deduplicate on `event_id`, then return `2xx` before model or tool
work. Delivery is at least once.
The body uses Linq's current v3 envelope exactly:
```json
{
"api_version": "v3",
"webhook_version": "2026-02-03",
"event_type": "message.received",
"event_id": "evt_...",
"created_at": "2026-08-27T18:06:02.000Z",
"trace_id": "8af9171a45022df2eb74ba4e4c83be0f",
"partner_id": "agt_...",
"data": {
"chat": {
"id": "cnv_...",
"is_group": false,
"owner_handle": {
"id": "agt_...",
"handle": "scheduler",
"is_me": true,
"service": "iMessage",
"status": "active",
"joined_at": "2026-08-27T18:00:00.000Z",
"left_at": null
},
"health_status": {
"status": "HEALTHY",
"doc_url": "https://docs.linqapp.com/guides/chats/chat-health#healthy",
"updated_at": "2026-08-27T18:06:02.000Z"
}
},
"id": "msg_...",
"direction": "inbound",
"sender_handle": {
"id": "usr_...",
"handle": "morrison",
"is_me": false,
"service": "iMessage",
"status": "active",
"joined_at": "2026-08-27T18:00:00.000Z",
"left_at": null
},
"parts": [{ "type": "text", "value": "Hello" }],
"effect": null,
"reply_to": null,
"sent_at": "2026-08-27T18:06:02.000Z",
"delivered_at": null,
"read_at": null,
"service": "iMessage"
}
}
```
Supported names are `message.sent`, `message.received`, `message.delivered`,
`message.read`, `reaction.added`, `reaction.removed`, `chat.created`,
`chat.group_name_updated`, `chat.group_icon_updated`, `participant.added`, and
`participant.removed`. Ignore unknown event types.
## Reply
One send is one message. Mint `message_id` before the first attempt and reuse
it on retry. It is both the canonical ID and idempotency key.
POST /v1/messages
{
"message_id": "msg_...",
"chat_id": "cnv_...",
"parts": [{ "type": "text", "text": "Hello" }]
}
1-32 parts per message, order = presentation order. Text and media sent
together stay together as parts of the one message:
- { "type": "text", "text": "…", "mention"?, "mention_range"?, "styles"? } (≤ 8 KB)
- { "type": "link", "url": "https://…", "title"?, "description"? }
- { "type": "data", "data": { any JSON } } (≤ 16 KB)
- { "type": "media", "url" | "attachment_id" } (exactly one of the two)
- { "type": "media", "url" | "attachment_id", "duration_ms"? }
Text styles are bold, italic, underline, and strikethrough; anything else is 422.
Request body ceiling 512 KB.
Response 202 { message_id, message }. The conversation-scoped forms are
POST /v1/chats/{id}/messages → 201 { messages: [message] } and
POST /v2/chats/{id}/messages → 201 { message }, which requires
message_id and rejects unknown fields with 422.
Mark the inbound message read with
`POST /v1/chats/{chatId}/read { "message_id": "msg_..." }`. Typing is a
separate ephemeral call to `POST /v1/chats/{chatId}/typing` with
`{ "started": true|false }`.
## State
Sent means Relay committed the message. Delivered means the recipient runtime
accepted it. Read means it was consumed or viewed. Read implies Delivered.
Typing is independent and temporary.
Message content is immutable. A reply pointer is
`reply_to: { message_id, part_id? }`. An agent in a group is an ordinary
member and uses Linq's `data.chat.is_group` discriminator.