Skip to content

Quickstart

This quickstart connects an existing HTTP backend to Relay. The backend receives a signed webhook when the user sends a text and replies with one POST.

  1. Create an agent in the Relay dashboard.

    Open Developers -> Agents -> New agent and enter:

    • Display name: Scheduler
    • Handle: scheduler
    • Webhook URL: https://your-domain.example/relay/webhook
    • Capabilities: text, file, image, link_preview, card, buttons

    Relay returns:

    {
    "agent_id": "agt_01JZ8A1Relay",
    "handle": "scheduler",
    "api_key": "relay_live_your_api_key",
    "webhook_secret": "whsec_your_webhook_secret"
    }
  2. Receive message.received.

    Relay sends the event below to your webhook URL. Return any 2xx response quickly, then run your agent work.

    {
    "id": "evt_01JZ8F2W6R4S7E2D3Q9M0N1P2A",
    "type": "message.received",
    "created_at": "2026-07-06T17:24:00Z",
    "agent": {
    "id": "agt_01JZ8A1Relay",
    "handle": "scheduler"
    },
    "conversation": {
    "id": "cnv_01JZ8D9H3J7K4M2N1P6Q8R0S2T",
    "kind": "direct",
    "relay_user_id": "usr_01JZ8C0X2B5N7M9Q1R3S4T6V8W",
    "thread_id": null,
    "created_at": "2026-07-06T17:20:00Z"
    },
    "message": {
    "id": "msg_01JZ8F2W7A8B9C0D1E2F3G4H5J",
    "sender": {
    "kind": "user",
    "id": "usr_01JZ8C0X2B5N7M9Q1R3S4T6V8W",
    "display_name": "Advait"
    },
    "created_at": "2026-07-06T17:24:00Z",
    "reply_to_message_id": null,
    "content": [
    {
    "id": "blk_01",
    "type": "text",
    "text": "Can you look at this PDF?"
    },
    {
    "id": "blk_02",
    "type": "file",
    "attachment_id": "att_01JZ8F4K9M2N6P8Q0R1S3T5V7W",
    "name": "labs.pdf",
    "mime_type": "application/pdf",
    "size_bytes": 241882,
    "download_url": "https://cdn.relay.app/signed/att_01JZ8F4K9M2N6P8Q0R1S3T5V7W?expires=1800"
    }
    ],
    "fallback_text": "Can you look at this PDF?"
    },
    "capabilities": {
    "content_blocks": ["text", "image", "audio", "video", "file", "link_preview", "card", "buttons"],
    "typing": true,
    "receipts": true,
    "streaming": ["http_stream", "message_delta_events"]
    }
    }
  3. Reply with POST /v1/conversations/{conversationId}/messages.

    Use the conversation.id from the webhook. Always send an Idempotency-Key so retries do not create duplicate visible messages.

Terminal window
export RELAY_API_KEY="relay_live_your_api_key"
export RELAY_CONVERSATION_ID="cnv_01JZ8D9H3J7K4M2N1P6Q8R0S2T"
curl -sS "https://api.relay.app/v1/conversations/${RELAY_CONVERSATION_ID}/messages" \
-H "Authorization: Bearer ${RELAY_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-$(date +%s)" \
-d '{
"content": [
{
"type": "text",
"text": "I can review it. The PDF is attached to this thread."
},
{
"type": "actions",
"layout": "quick_replies",
"actions": [
{
"actionId": "act_summarize",
"label": "Summarize",
"behavior": { "type": "postback", "value": "summarize" }
},
{
"actionId": "act_extract_values",
"label": "Extract values",
"behavior": { "type": "postback", "value": "extract_values" }
}
]
}
],
"fallbackText": "I can review it. Summarize or extract values?",
"replyToMessageId": "msg_01JZ8F2W7A8B9C0D1E2F3G4H5J"
}'

This server receives Relay webhooks, verifies the signature, and echoes the text back to the same conversation.

import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const relayApiKey = process.env.RELAY_API_KEY;
const webhookSecret = process.env.RELAY_WEBHOOK_SECRET;
if (!relayApiKey || !webhookSecret) {
throw new Error("Set RELAY_API_KEY and RELAY_WEBHOOK_SECRET");
}
function verifySignature(rawBody: Buffer, timestamp: string, signature: string) {
const expected = createHmac("sha256", webhookSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = signature.replace(/^v1=/, "");
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(received, "hex"));
}
createServer(async (request, response) => {
if (request.method !== "POST" || request.url !== "/relay/webhook") {
response.writeHead(404).end();
return;
}
const chunks: Buffer[] = [];
for await (const chunk of request) chunks.push(Buffer.from(chunk));
const rawBody = Buffer.concat(chunks);
const timestamp = request.headers["relay-timestamp"];
const signature = request.headers["relay-signature"];
if (typeof timestamp !== "string" || typeof signature !== "string") {
response.writeHead(400).end("Missing Relay signature headers");
return;
}
if (!verifySignature(rawBody, timestamp, signature)) {
response.writeHead(401).end("Invalid signature");
return;
}
const event = JSON.parse(rawBody.toString("utf-8"));
response.writeHead(204).end();
if (event.type !== "message.received") return;
const conversationId = event.conversation.id;
const text = event.message.fallback_text || "Received your message.";
await fetch(`https://api.relay.app/v1/conversations/${conversationId}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${relayApiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `echo-${event.id}`,
},
body: JSON.stringify({
content: [{ type: "text", text: `Echo: ${text}` }],
fallbackText: `Echo: ${text}`,
}),
});
}).listen(8787, () => {
console.log("Relay webhook listening on http://localhost:8787/relay/webhook");
});