Relay docs

suiss Relay · Docs

HTTP API

Draft reference for Relay's planned HTTP API, covering conventions, authentication, idempotency, sending events, batches, cancellation and resources.

This is a draft reference. Endpoints, fields and limits may change before release.

Relay's HTTP API is designed contract-first: the HTTP surface is described in OpenAPI 3.1 and the event-driven surface (webhook events, real-time broadcasts, agent events) in AsyncAPI 3.1, with JSON Schema 2020-12 for bodies and events. Server and client SDKs are planned to be generated from these documents.

Endpoint families

All families live under /v1, including client endpoints. Each data region has its own base address; a region's address never reaches another region's data.

FamilyCredentialUsed byCovers
SendAccess client credentials token, publish scopeTenant backends, producer productsPOST /v1/events (native JSON and CloudEvents), POST /v1/events/batch, campaign sends, cancellation
ManagementAccess client credentials token with management scopes, or an operator sessionTenant backends, console, files in version controlRecipients, devices, topics and subscriptions, workflows, templates, layouts, partials, routes, recurring rules, webhook endpoints, stop controls
QueryAccess client credentials token, read scopeTenant backends, consoleEvents, notifications, deliveries, attempt history and ledger
ClientSubscriber tokenBrowsers, mobile apps, UI componentsThe subscriber's own inbox, preferences and device registration
AgentAccess token for an agent or API client, or a subscriber tokenAgents, agent frameworks, your codeWaitpoints, mailboxes, A2A and MCP endpoints
Inbound provider eventsPer-provider signature verificationPush services, email providers, SMS providers, İYSDelivery reports, bounces, complaints, inbound messages
OperatorOperator session plus re-authenticationInstallation operatorPlatform-level operations

There is one way to express each concept. There is no per-channel send endpoint: orchestration picks the channel.

Conventions

  • Request and response bodies are application/json; charset=utf-8. Errors are application/problem+json.
  • Field names are always snake_case.
  • Unknown fields are rejected with 422 unknown_field, at the top level, in nested objects and under overrides. They are never silently ignored.
  • null means "clear this field"; an absent field means "leave it unchanged".
  • Times are RFC 3339 in UTC. Durations are ISO 8601 (PT30M, P1D).
  • Amounts and large integers are strings. Money is { "amount": "1234.50", "currency": "TRY" }.
  • HTTPS only, TLS 1.2 or later. Plain HTTP is rejected and HSTS is sent.
  • Every response has a Request-Id header. Single-resource responses carry an object field.
  • IDs are opaque, prefixed by type and time-ordered. See Identifiers and events.
  • A resource that belongs to another tenant or environment returns 404, not 403.

Authentication

Server API

The server API is called with a short-lived Access access token (Authorization: Bearer), obtained with OAuth 2.0 client credentials for the tenant's API client in Access. Relay has no API keys of its own and stores no client secrets. Mutual TLS with certificate-bound tokens (RFC 8705) can be added for enterprise tenants.

Relay validates the token against Access's published signing keys on every request: issuer, audience (the Relay server API), expiry and not-before, with an allow list of algorithms. Unsigned tokens are rejected. Tenant and environment come from the API client's registration in Access, never from the request.

  • Each API client is bound to a single tenant and a single environment (live or test).
  • Scopes are coarse. publish and read are separate. Managing API clients is never granted to an API client; it requires an operator session with step-up.
  • A missing scope returns 403 auth_insufficient_scope with required_scope and granted_scopes.
  • A 401 carries WWW-Authenticate: Bearer and an auth_* code. Invalid token attempts are rate-limited per IP.
  • A suspended tenant gets 403 auth_tenant_suspended at every entry point.

Client API

Browsers and mobile apps never hold a secret credential.

  • The client exchanges its Access token for a Relay subscriber token using RFC 8693 token exchange. No tenant backend code is needed.
  • Relay does not accept an Access token directly for inbox access.
  • Subscriber token scopes: inbox:read, inbox:write, preferences, devices:write (register, update and remove only the token holder's own devices) and presence:write (write only the holder's own device presence).
  • Client endpoints are an allow list. POST /v1/events is never open to clients.
  • Revoking a subscriber token, or suspending the subscriber, closes open real-time connections.

Idempotency

Every mutating request (POST, PUT, PATCH, DELETE) requires an Idempotency-Key header. A request without one is rejected with 400 idempotency_key_missing. The header is ignored on GET.

RuleValue
LocationHeader, never a body field
Length8 to 255 characters; [A-Za-z0-9_.:-] recommended; quoted or unquoted
Scope(tenant, environment, endpoint, key)
Lifetime24 hours, fixed by the server
FingerprintSHA-256 of the raw request body bytes
ContentNever put personal data in the key; it appears in audit records
SituationResult
Same key, same body, request completedThe original response is returned unchanged, status and body included, with Idempotency-Replayed: true
Same key, different body422 idempotency_key_reuse; the old response is never returned
Same key, first request still running409 idempotency_key_in_progress, Retry-After: 1, retryable: true

Only responses produced after business logic started are stored. 400, 401, 403 and 429 are never stored. A 5xx is stored only if execution partly progressed; a failure with no side effect is not stored and returns retryable: true.

SDKs are planned to generate the key automatically and reuse it on retries of the same call. A legitimate repeat, such as "send the code again", uses a new Idempotency-Key and a new dedup_key.

The only exception is CloudEvents intake without the header; see CloudEvents.

Send an event

POST /v1/events
Authorization: Bearer <access token>
Idempotency-Key: order-1042-shipped-a1
Content-Type: application/json
{
  "workflow": "order-shipped",
  "to": [
    {
      "id": "user-42",
      "email": "user@example.com",
      "locale": "en-US",
      "timezone": "Europe/Istanbul"
    }
  ],
  "data": {
    "order_id": "1042",
    "total": { "amount": "249.90", "currency": "TRY" }
  },
  "dedup_key": "shop:order-shipped:evt-8f2c"
}

The endpoint returns 202 Accepted. Acceptance is not delivery; nothing waits for providers.

{
  "object": "event",
  "id": "evt_01jb8x2k4m6n8p0q2r4s6t8v0w",
  "workflow": { "key": "order-shipped", "version": 7 },
  "status": "accepted",
  "recipient_count": 1,
  "notifications": [
    {
      "recipient_id": "sub_01jb8x2k4m6n8p0q2r4s6t8v1a",
      "notification_id": "ntf_01jb8x2k4m6n8p0q2r4s6t8v1b"
    }
  ],
  "created_at": "2026-01-15T09:30:00Z"
}

Response headers include Location: /v1/events/{id}, Request-Id and Idempotency-Key. A notification_id per recipient is returned immediately.

Request fields

FieldMeaning
workflowSlug of a published workflow. An unpublished workflow returns 422 workflow_not_published.
to1 to 100 recipients. Each is a recipient object ({id, email, phone, locale, timezone, ...}), an ID only, or a topic ({topic}). A request with no target returns 422.
actorWho did it: a person, a system or an agent. Sender type is a separate field from the message class.
tenantA sub-tenant within the API client's tenant
dataEvent data, at most 256 kB, validated against the published workflow version's event schema
dedup_keyBusiness key for the event
channelsFilter that narrows the workflow's steps; it never adds steps
overridesPer-channel overrides
send_atScheduled send; must be in the future and at most 90 days ahead. A past time returns 422.
expires_atNot sent after this moment; ends as expired
prioritylow, normal or high; fine-tuning within the lane only
security_subtypesecurity class only: otp_oob or email_verification
on_expiresecurity class only: drop (default) or inbox_only
cancellation_keyOptional key for later cancellation; need not be unique
retentionnone: rendered content is not stored
metadataOpaque; at most 16 keys, values at most 512 characters; returned in events and webhooks

Recipient upsert

  • If the recipient object carries contact data beyond the ID, the recipient is created or updated on the spot and the notification is sent.
  • If only an ID is given and the recipient does not exist, the request returns 422 recipient_not_found. No ghost recipients are created.
  • An inbox-only recipient can be created explicitly with an object containing only an ID (to: [{ "id": "..." }]); its address is its ID. A bare string ID does not have this meaning.
  • Phone numbers are E.164.

Priority

The lane comes only from the message class; priority cannot move a message to another lane. Each class has a priority ceiling (for example marketing at most normal). A higher value is lowered to the ceiling and the response includes a priority_clamped warning. Platform priority is derived from class plus priority through one mapping table. priority is not a channel SLA.

Overrides

  • Keys are channel names (email, sms, push, in_app, whatsapp, webhook), not provider names. overrides.<channel>.provider can only narrow the choice among verified providers.
  • Precedence is one-way: request, then step, then workflow, then tenant default.
  • A raw provider-specific block exists only for push and WhatsApp.
  • A raw block cannot override policy fields such as push interruption level, priority, push type, or the critical and time-sensitive rules. Such a field returns 422 policy_field_not_overridable.
  • overrides.email.from must use a verified domain, otherwise 403 channel_not_configured.

Security subtypes and content retention

For otp_oob and email_verification, expires_at is required and rendered content is never stored. For other classes, retention: none turns off content storage for that message. Combining retention: none with the in-app channel is rejected, because the inbox has to keep the rendered item.

Sub-tenant

tenant selects a sub-tenant inside the API client's tenant; the client's own tenant cannot be overridden. A delivery to an undefined or mismatched sub-tenant is recorded as skipped with scope_mismatch.

CloudEvents

The same intake accepts CloudEvents 1.0 over HTTP in binary mode (ce-* headers), structured mode (application/cloudevents+json) and batched mode (application/cloudevents-batch+json).

  • The CloudEvents type is bound to a workflow through a mapping table the tenant defines. An unmapped type returns 422 event_type_not_mapped; this is a contract error, not a policy decision.
  • (tenant, source, id) is the event's dedup_key.
  • traceparent is carried end to end.
  • If the request has no Idempotency-Key header, the idempotency key is derived from (tenant, source, id). If the header is present, it wins. This exception applies only to CloudEvents intake.

How and in which order events are produced on the customer side is not Relay's responsibility. Relay does not read customer databases.

Batch

POST /v1/events/batch is not all-or-nothing.

StatusMeaning
202All rows accepted
207Some rows accepted, some rejected
422No row accepted; full failure never returns 2xx
400Malformed envelope
413Row or size limit exceeded (batch_too_large, with limit and actual)
401, 403, 429Credential, permission, rate or quota problem for the whole request
  • Give each row a ref (at most 128 characters) and match results by ref; without one, index is used.
  • Each row error is a full RFC 9457 problem object. A summary field is always present.
  • One Idempotency-Key covers the whole batch; there is no per-row key and ref is not an idempotency unit. Resend rejected rows as a new batch with a new key.
  • A batch is neither atomic nor ordered. results follows input order, but processing order is not guaranteed.
  • A batch consumes rate limit and quota per row.
LimitPlanned default
Events per batch500
Recipients per event100
Body size5 MB
data per row256 kB

Limits are also announced in the Relay-Batch-Max-Events and Relay-Batch-Max-Recipients-Per-Event response headers.

Campaigns. Large list or topic sends use a separate campaign endpoint: prepare, then trigger, with its own progress record, deadline and cancellation. The tenant passes a list or picks a topic; there are no attribute-query segments.

Rate limits and quotas

  • Headers: RateLimit and RateLimit-Policy, plus X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (delta seconds) for compatibility. Retry-After is always delta seconds.
  • API rate limits are tenant-scoped with a short burst window and a longer sustained window; the more restrictive is reported. An API client can have its own lower limit.
  • Requests are weighted: a GET or a single event counts 1, a batch counts its rows, starting a campaign has a fixed high weight, and template writes and previews have a medium weight.
SituationResponse
API rate limit429 rate_limited, retryable
Send quota exhausted429 quota_exhausted, not retryable, no Retry-After
System overload503, not 429
Provider limitsNever surface as 429; they slow delivery and show in delivery state

Under overload, admission control rejects only deferrable classes; security is never rejected by admission control. When the quota is exhausted, security is still accepted within a limited extra allowance (planned default 10%). A class that fills its own quota share gets the same quota_exhausted error, and the body names the quota and class.

Versioning

  • The major version is in the path (/v1) and is permanent. A new major path opens only if the conceptual model changes.
  • Behavior within /v1 is selected with a dated header: Relay-Version: YYYY-MM-DD. Without the header, the tenant's pinned version applies. A new account is pinned to the current version on its first successful request. "Latest" is never the default. An invalid value returns 400 unsupported_api_version with the list of supported versions.
  • Each dated version is planned to be supported for at least 24 months, and removal announced at least 12 months ahead, including Deprecation (RFC 9745), Sunset (RFC 8594) and Link rel="deprecation" headers.
  • The webhook payload version is separate and pinned per endpoint. SDK versions are pinned to an API version.
  • Beta features are enabled with Relay-Beta: <feature>, sit outside the version contract and return "beta": true.
Not breakingBreaking
New response field; new enum value (clients must handle unknown values); new endpoint, optional field or query parameter; detail text; field order; new event type; new response header; narrowing nullabilityRemoving or renaming a field; changing a type; removing an enum value; adding a required field; changing default behavior; changing a status code; changing meaning; lowering a limit; tightening authentication

Pagination

  • List endpoints use opaque cursors; there are no offsets or page numbers.
  • Parameters: limit (1 to 100, default 50), starting_after, ending_before.
  • Response shape: { "object": "list", "data": [...], "has_more": true, "next_cursor": "..." }.
  • Cursors are signed and bound to the tenant (and to the subscriber on client endpoints).
  • Related objects are not expanded in lists; expand works only on single resources.

Cancellation

EndpointCancels
POST /v1/events/{id}/cancelEverything pending for an event
POST /v1/notifications/{id}/cancelOne notification
POST /v1/events/cancel_by_keyAll pending work matching a cancellation_key, optionally filtered by recipient and channel

Campaigns and recurring rules also have cancellation endpoints. No key needs to be declared in advance; an event or notification ID is enough. The response is 200 with counts (cancelled, already_sent, not_found, details[]), never 204.

Cancellation is processed on the same ordering key as the trigger. If the run does not exist yet, a tombstone is written and the later run ends as cancelled. Cancellation stops only deliveries not yet handed to a provider; sent SMS, email and push cannot be recalled. A delivered in-app item can be retracted.

Preview, test and send mode

EndpointBehavior
POST /v1/workflows/{key}/previewSynchronous 200: per step would_send, rule_id, rendered content and warnings such as missing_variable; for SMS, encoding, segments and characters. Works on draft or published versions.
POST /v1/templates/{key}/previewSame, for a template
POST /v1/workflows/{key}/testSends through real providers in live to at most 5 of the tenant's verified test recipients or devices. Marked is_test: billed and counted separately, with a small separate quota.

send_mode values are live, shadow, dry_run and off; see Messages and delivery. A per-request mode is accepted only in the test environment.

Resources

Subscribers

EndpointBehavior
POST /v1/subscribersCreate; 409 on conflict
PUT /v1/subscribers/{id}Upsert with full replacement; 201 on first creation
PATCH /v1/subscribers/{id}Partial update
GET /v1/subscribers/{id}Read; includes per-channel reachability (channels.*.reachable plus reason)
List and bulk upsertBulk upsert follows the batch rules and returns 207 on partial success
DELETE /v1/subscribers/{id}202 and an erasure request applied to queued work, scheduled sends, digest buffers and the inbox. Personal fields are made unreadable by crypto-shredding; there is no physical deletion.

Devices

EndpointBehavior
POST /v1/subscribers/{id}/devicesRegister a device; re-registering the same token is idempotent and updates last_seen_at
GET /v1/subscribers/{id}/devicesList devices
DELETE /v1/subscribers/{id}/devices/{device_id}Remove a device
DELETE /v1/devices/by-tokenRemove a device by its token
  • A device token is never returned; only a fingerprint is.
  • If the token belongs to another subscriber, it is moved to the new one and device.reassigned is emitted.
  • apns_environment (production or sandbox) is a separate field and is not the same as the Relay environment.
  • A device record carries the OS permission state (granted, denied, provisional, not_determined) and the Android channel importance.
  • There is an upper limit on active devices per subscriber.

Queries

EndpointReturns
GET /v1/events/{id}The event
GET /v1/notifications/{id}Summary by default; expand=deliveries adds deliveries
GET /v1/notificationsFilter by subscriber, workflow, status and time
GET /v1/notifications/{id}/deliveriesDeliveries of a notification

A separate endpoint returns attempt history and ledger events. The external projection is described in Delivery status and is designed to stay stable even when the internal model changes.

Management

EndpointBehavior
GET /v1/workflows, POST /v1/workflowsList and create
GET /v1/workflows/{key}, PUT /v1/workflows/{key}Read; PUT creates a new draft
POST /v1/workflows/{key}/publishPublish; returns 202
Version listPublished versions of a workflow
POST /v1/workflows/{key}/versions/{n}/promoteRoll back to an older version

Templates, layouts, partials, routes, topics and subscriptions, recurring rules, throttle resets, webhook endpoints and stop controls follow the same common rules. Workflow and template management endpoints accept the same definition format as files.

SDK behavior

Every SDK is planned to include:

  • automatic Idempotency-Key, kept the same across retries;
  • retries only on retryable: true errors, with exponential backoff and jitter, honoring Retry-After;
  • a typed error hierarchy: authentication, validation with errors[], rate limit with retry_after, idempotency, server;
  • a webhook verifier that works on the raw body.