Skip to content

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.

https://api.eidos.space/v1/relay/channels/{channelId}/messages

All requests must include a Relay API Token, generated from your Eidos.space dashboard.

Authorization: Bearer {token}

Send a single message to the relay.

Endpoint: POST /

Terminal window
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" }
}'

Request body fields:

FieldTypeRequiredDescription
bodyanyThe message payload. Can be any JSON-serializable value.
content_typestring"json" (default), "text", "bytes", or "v8"
delay_secondsnumberSeconds before the message becomes available for delivery
metadataobjectArbitrary key-value metadata attached to the message

Response:

{
"success": true,
"errors": [],
"messages": [],
"result": {
"id": "550e8400-e29b-41d4-a716-446655440000"
}
}

Send multiple messages at once.

Endpoint: POST /batch

Terminal window
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 } }
]
}'

Request body fields:

FieldTypeRequiredDescription
messagesarrayArray of message objects. Each follows the same structure as a single send.
delay_secondsnumberDefault 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"
]
}
}

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

Terminal window
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
}'

Request body fields:

FieldTypeRequiredDescription
visibility_timeout_msnumberHow long (ms) a pulled message is hidden from other consumers. Default: 30000
batch_sizenumberNumber 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:

FieldTypeDescription
bodyanyMessage body
idstringUnique message ID
timestamp_msnumberPublish timestamp (milliseconds)
attemptsnumberNumber of delivery attempts
metadataobjectCustom metadata
lease_idstringLease ID for ack/retry

Acknowledge successfully processed messages. Acknowledged messages are permanently deleted from the cloud.

Endpoint: POST /ack

Terminal window
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"]
}'

Request body fields:

FieldTypeRequiredDescription
lease_idsstring[]Array of lease_id values returned by the pull endpoint

Response:

{
"success": true,
"errors": [],
"messages": [],
"result": {
"acked_count": 1
}
}

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/subscribe
Upgrade: websocket
Authorization: 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" }

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: websocket
Authorization: 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" }

┌──────────┐ 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.


All errors follow this format:

{
"success": false,
"errors": [
{
"code": 400,
"message": "Missing required field: body"
}
],
"messages": [],
"result": null
}

// 1. Send a message
const 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 messages
const 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 messages
for (const msg of messages) {
await processMessage(msg.body);
}
// 4. Acknowledge all
await 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),
}),
});
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" } },
],
}),
});

LimitValueDescription
delay_secondsMax 86400 (24 hours)Maximum delay before message becomes available for consumption
visibility_timeout_msMax 43200000 (12 hours)Maximum time a message is invisible after being pulled
batch_sizeMax 100Maximum number of messages per pull request
Message size128 KBMaximum size of a single message (body + metadata)
Max retries3Maximum delivery attempts before message is discarded
Message retention14 daysMaximum time a message is kept in the queue
FeatureFree PlanSpark Plan
Max storage10 MB100 MB
Max channels5100
Max message size128 KB128 KB
Message retention3 days14 days
ParameterDefaultDescription
visibility_timeout_ms30000 (30 seconds)Time before message becomes visible again if not acknowledged
batch_size10Number of messages returned per pull request
content_type”json”Default content type for messages
  • Max tenant storage: 100 MB per user (across all channels)
  • Max message size: 1 MB (SQLite safeguard)
  • Max channels per user: 100 (Spark plan)