- An agent and its Agent Token. See Create and connect an agent.
- A read of the transport contract. See Delivery model and Webhooks.
Choose this pattern deliberately
Relay’s public contract supports more than one integration shape. Pick the narrowest one that fits.
A channel plugin is the right shape only when the host runtime, not Relay,
decides which local agent handles a turn.
Pick a transport
Scope every inbound turn to an owner
CallGET /v1/agents/me once at startup and pin the returned owner user id.
Drop any inbound message whose sender is not the owner, or not on an explicit
allowlist you maintain, before your plugin interprets its content. The released
OpenClaw and coding-agent integrations enforce the same rule: default-deny, not
default-allow.
Map events to your runtime’s turns
Your plugin owns the mapping from a Relaychat_id to your host’s own
session or thread state. Relay does not prescribe one. Persist that mapping to
disk. Persist the long-poll next_cursor in the same atomic write as any
locally queued work. Webhooks need no cursor, because Relay’s outbox already
retries. A crash must never be able to acknowledge an event your runtime never
queued.
Deduplicate on event_id regardless of transport: both webhooks and long-poll
redeliver at least once. If your host processes a rapid burst of messages as
separate turns, consider a short debounce window before starting work. Relay’s
own released plugins coalesce a fast sequence of messages into one turn instead
of running the agent once per message.
Reply idempotently
message_id you mint is the send’s idempotency key. Derive it from the
triggering event_id so a retried turn replays the stored message instead of
posting twice, whether the retry comes from your host’s crash recovery or a
Relay redelivery. Store the mapping from event to message_id before you
attempt the request, not after. A msg_ id another sender already committed
answers 409 idempotency_conflict.
Handle failure without losing state
Follow the recovery rule: conversation history is the source of truth for what a thread contains. After a crash, a401, or any
ambiguous response, reconcile from
conversation history. Do not reconstruct state
from your own retry attempts.
Next steps
- Delivery model for the full transport and idempotency contract
- Webhooks for registration, verification, and rotation
- OpenClaw for a shipped channel plugin built on this contract
- Integration troubleshooting for shared symptoms by failure signature
- Developer data access for what a plugin may read and store

