This transcript creates an agent token, waits for a message, and replies. You need only curl.
These examples use https://api.relayapp.im. To use a local Relay server, set RELAY_API_URL=http://localhost:8788 instead.
Create an agent
In the Relay app, choose Create your own, enter the contact details, and create it. Copy the Agent Token before closing the confirmation: Relay shows it once.export RELAY_API_URL="https://api.relayapp.im"
export RELAY_AGENT_TOKEN="relay_agt_live_..."
Verify the token
curl -sS "$RELAY_API_URL/v1/agents/me" \
-H "Authorization: Bearer $RELAY_AGENT_TOKEN"
{
"agent": {
"id": "agt_01JZRELAY",
"handle": "scheduler",
"display_name": "Scheduler",
"tagline": "Finds a time that works",
"avatar_url": null,
"distribution": "store",
"created_at": "2026-07-12T01:20:00.000Z"
}
}
Poll for a message
Send the new contact a message in Relay, then long-poll for up to 25 seconds:curl -sS "$RELAY_API_URL/v1/events?timeout=25" \
-H "Authorization: Bearer $RELAY_AGENT_TOKEN"
{
"events": [
{
"event_id": "evt_01JZE9M2XW",
"event_type": "message.received",
"agent_id": "agt_01JZRELAY",
"created_at": "2026-07-12T01:21:03.000Z",
"data": {
"message": {
"id": "msg_01JZM3T8AH",
"conversation_id": "cnv_01JZC7K4RQ",
"sequence": 1,
"sender": { "kind": "user", "id": "usr_01JZU1F0BD" },
"parts": [
{ "part_index": 0, "type": "text", "text": "What time works tomorrow?" }
],
"reply_to": null,
"fallback_text": "What time works tomorrow?",
"status": "sent",
"created_at": "2026-07-12T01:21:03.000Z"
}
}
}
],
"next_cursor": "djE6MQ=="
}
Save next_cursor. Pass it back as cursor on the next request; doing so acknowledges everything before that cursor. Treat the cursor as opaque. Because delivery is at least once, also store processed event_id values and ignore duplicates. Reply
Use the received message’s conversation_id. The idempotency key must be 8–255 characters; deriving it from the event ID makes a retry safe.curl -sS -X POST "$RELAY_API_URL/v1/messages" \
-H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reply-evt_01JZE9M2XW" \
-d '{
"conversation_id": "cnv_01JZC7K4RQ",
"parts": [
{ "type": "text", "text": "Tomorrow at 2:00 PM works for me." }
],
"reply_to": { "message_id": "msg_01JZM3T8AH", "part_index": 0 }
}'
Relay responds with 202 Accepted:{
"message_id": "msg_01JZM4Q9VN",
"message": {
"id": "msg_01JZM4Q9VN",
"conversation_id": "cnv_01JZC7K4RQ",
"sequence": 2,
"sender": { "kind": "contact", "id": "agt_01JZRELAY" },
"parts": [
{ "part_index": 0, "type": "text", "text": "Tomorrow at 2:00 PM works for me." }
],
"reply_to": { "message_id": "msg_01JZM3T8AH", "part_index": 0 },
"fallback_text": "Tomorrow at 2:00 PM works for me.",
"status": "sent",
"created_at": "2026-07-12T01:21:08.000Z"
}
}
Continue from the cursor
curl -sS "$RELAY_API_URL/v1/events?cursor=djE6MQ%3D%3D&timeout=25" \
-H "Authorization: Bearer $RELAY_AGENT_TOKEN"
If no new message arrives before the timeout, Relay returns an empty page and the same logical cursor:{ "events": [], "next_cursor": "djE6MQ==" }
Next, read Concepts or switch event delivery to Webhooks.