Relay API Reference
The Relay API allows external services to push data into a relay queue associated with your Eidos Space. Messages are held in the cloud and delivered to your local Eidos instance the next time it comes online.
API design aligned with Cloudflare Queues.
Core Principle: No explicit retry endpoint - not acknowledging a message = automatic retry.
Base URL
Section titled “Base URL”https://api.eidos.space/v1/relay/channels/{channelId}/messagesAuthentication
Section titled “Authentication”All requests must include a Relay API Token, generated from your Eidos.space dashboard.
Authorization: Bearer {token}Endpoints
Section titled “Endpoints”1. Send a Message
Section titled “1. Send a Message”Send a single message to the relay.
Endpoint: POST /
curl -X POST https://api.eidos.space/v1/relay/channels/{channelId}/messages \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "body": { "event": "order.created", "orderId": "123" } }'await fetch("https://api.eidos.space/v1/relay/channels/{channelId}/messages", { method: "POST", headers: { "Authorization": "Bearer {token}", "Content-Type": "application/json", }, body: JSON.stringify({ body: { event: "order.created", orderId: "123" }, }),});Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
body | any | ✅ | The message payload. Can be any JSON-serializable value. |
content_type | string | — | "json" (default), "text", "bytes", or "v8" |
delay_seconds | number | — | Seconds before the message becomes available for delivery |
metadata | object | — | Arbitrary key-value metadata attached to the message |
Response:
{ "success": true, "errors": [], "messages": [], "result": { "id": "550e8400-e29b-41d4-a716-446655440000" }}2. Send Batch Messages
Section titled “2. Send Batch Messages”Send multiple messages at once.
Endpoint: POST /batch
curl -X POST https://api.eidos.space/v1/relay/channels/{channelId}/messages/batch \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "messages": [ { "body": { "event": "user.signup", "id": 1 } }, { "body": { "event": "user.signup", "id": 2 } } ] }'await fetch("https://api.eidos.space/v1/relay/channels/{channelId}/messages/batch", { method: "POST", headers: { "Authorization": "Bearer {token}", "Content-Type": "application/json", }, body: JSON.stringify({ messages: [ { body: { event: "user.signup", id: 1 } }, { body: { event: "user.signup", id: 2 } }, ], }),});Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
messages | array | ✅ | Array of message objects. Each follows the same structure as a single send. |
delay_seconds | number | — | Default delay applied to all messages that don’t specify their own |
Response:
{ "success": true, "errors": [], "messages": [], "result": { "ids": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440001" ] }}3. Pull Messages
Section titled “3. Pull Messages”Pull messages from the relay for consumption. This is used internally by Eidos Desktop, but can also be called directly if you’re building a custom consumer.
Endpoint: POST /pull
curl -X POST https://api.eidos.space/v1/relay/channels/{channelId}/messages/pull \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "batch_size": 10, "visibility_timeout_ms": 30000 }'const response = await fetch("https://api.eidos.space/v1/relay/channels/{channelId}/messages/pull", { method: "POST", headers: { "Authorization": "Bearer {token}", "Content-Type": "application/json", }, body: JSON.stringify({ batch_size: 10, visibility_timeout_ms: 30000, }),});const { result } = await response.json();console.log(result.messages);Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
visibility_timeout_ms | number | — | How long (ms) a pulled message is hidden from other consumers. Default: 30000 |
batch_size | number | — | Number of messages to pull. Default: 10, max: 100 |
Response:
{ "success": true, "errors": [], "messages": [], "result": { "message_backlog_count": 42, "messages": [ { "body": { "event": "user.created" }, "id": "550e8400-e29b-41d4-a716-446655440000", "timestamp_ms": 1689615013586, "attempts": 1, "metadata": { "key": "value" }, "lease_id": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..." } ] }}Message Fields:
| Field | Type | Description |
|---|---|---|
body | any | Message body |
id | string | Unique message ID |
timestamp_ms | number | Publish timestamp (milliseconds) |
attempts | number | Number of delivery attempts |
metadata | object | Custom metadata |
lease_id | string | Lease ID for ack/retry |
4. Acknowledge Messages
Section titled “4. Acknowledge Messages”Acknowledge successfully processed messages. Acknowledged messages are permanently deleted from the cloud.
Endpoint: POST /ack
curl -X POST https://api.eidos.space/v1/relay/channels/{channelId}/messages/ack \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "lease_ids": ["LEASE_ID_1", "LEASE_ID_2"] }'await fetch("https://api.eidos.space/v1/relay/channels/{channelId}/messages/ack", { method: "POST", headers: { "Authorization": "Bearer {token}", "Content-Type": "application/json", }, body: JSON.stringify({ lease_ids: ["LEASE_ID_1", "LEASE_ID_2"], }),});Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
lease_ids | string[] | ✅ | Array of lease_id values returned by the pull endpoint |
Response:
{ "success": true, "errors": [], "messages": [], "result": { "acked_count": 1 }}5. Subscribe (WebSocket)
Section titled “5. Subscribe (WebSocket)”Connect to a WebSocket to receive real-time notifications when new messages are available in the relay. This eliminates the need for polling.
Endpoint: GET /subscribe
GET https://api.eidos.space/v1/relay/channels/{channelId}/messages/subscribeUpgrade: websocketAuthorization: Bearer {token}WebSocket Events:
When connecting, you will receive a welcome event:
{ "type": "subscribed", "channelId": "your-channel" }When someone sends a message to this relay, you will receive:
{ "type": "new_message", "channelId": "your-channel" }6. Global Subscribe (WebSocket)
Section titled “6. Global Subscribe (WebSocket)”Connect to a WebSocket to receive real-time notifications for multiple channels simultaneously.
Endpoint: GET /v1/relay/subscribe?channels={channelId1},{channelId2}
GET https://api.eidos.space/v1/relay/subscribe?channels={channelId1},{channelId2}Upgrade: websocketAuthorization: Bearer {token}WebSocket Events:
When connecting, you will receive a welcome event:
{ "type": "subscribed", "channels": ["channelId1", "channelId2"] }When someone sends a message to any of these channels, you will receive the same notification as the single-channel subscribe, which includes the channelId so you know which channel has a new message:
{ "type": "new_message", "channelId": "channelId1" }Message Lifecycle
Section titled “Message Lifecycle”┌──────────┐ send ┌──────────┐ pull ┌──────────┐│ Producer │ ─────────── │ Relay │ ─────────── │ Consumer │└──────────┘ └──────────┘ └────┬─────┘ │ ┌──────────────────────────────────┤ │ ack (within visibility_timeout_ms)│ ▼ │ ┌──────────┐ │ │ Deleted │ │ └──────────┘ │ │ ┌──────────────────────────────────┘ │ no ack / timeout ▼ ┌──────────┐ │ Re-queue │ (attempts++) └────┬─────┘ │ attempts < maxRetries └──────────────────────┐ ▼ ┌──────────┐ │ Dead Letter │ (optional) └──────────┘If a pulled message is not acknowledged within visibility_timeout_ms, it automatically becomes available again for the next pull. The message’s attempts counter increments. When attempts reaches max_retries, the message is discarded or sent to dead letter queue.
This ensures at-least-once delivery without a separate retry mechanism.
Error Response Format
Section titled “Error Response Format”All errors follow this format:
{ "success": false, "errors": [ { "code": 400, "message": "Missing required field: body" } ], "messages": [], "result": null}Usage Examples
Section titled “Usage Examples”Send and Consume
Section titled “Send and Consume”// 1. Send a messageconst sendRes = await fetch("/v1/relay/channels/my-channel/messages", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ body: { event: "order.created", orderId: "123" }, }),});const { result: { id } } = await sendRes.json();
// 2. Pull messagesconst pullRes = await fetch("/v1/relay/channels/my-channel/messages/pull", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ batch_size: 10, visibility_timeout_ms: 30000, }),});const { result: { messages } } = await pullRes.json();
// 3. Process messagesfor (const msg of messages) { await processMessage(msg.body);}
// 4. Acknowledge allawait fetch("/v1/relay/channels/my-channel/messages/ack", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ lease_ids: messages.map((m) => m.lease_id), }),});Send Batch
Section titled “Send Batch”const res = await fetch("/v1/relay/channels/my-channel/messages/batch", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ messages: [ { body: { type: "email", to: "user1@example.com" } }, { body: { type: "email", to: "user2@example.com" } }, { body: { type: "email", to: "user3@example.com" } }, ], }),});Limits
Section titled “Limits”Message Limits
Section titled “Message Limits”| Limit | Value | Description |
|---|---|---|
delay_seconds | Max 86400 (24 hours) | Maximum delay before message becomes available for consumption |
visibility_timeout_ms | Max 43200000 (12 hours) | Maximum time a message is invisible after being pulled |
batch_size | Max 100 | Maximum number of messages per pull request |
| Message size | 128 KB | Maximum size of a single message (body + metadata) |
| Max retries | 3 | Maximum delivery attempts before message is discarded |
| Message retention | 14 days | Maximum time a message is kept in the queue |
Plan Quotas
Section titled “Plan Quotas”| Feature | Free Plan | Spark Plan |
|---|---|---|
| Max storage | 10 MB | 100 MB |
| Max channels | 5 | 100 |
| Max message size | 128 KB | 128 KB |
| Message retention | 3 days | 14 days |
Default Values
Section titled “Default Values”| Parameter | Default | Description |
|---|---|---|
visibility_timeout_ms | 30000 (30 seconds) | Time before message becomes visible again if not acknowledged |
batch_size | 10 | Number of messages returned per pull request |
content_type | ”json” | Default content type for messages |
Hard Limits
Section titled “Hard Limits”- Max tenant storage: 100 MB per user (across all channels)
- Max message size: 1 MB (SQLite safeguard)
- Max channels per user: 100 (Spark plan)