Relay API (1.0.0)

Download OpenAPI specification:

One API for Google Calendar and Outlook.

Relay connects your users' Google and Microsoft (Outlook / Microsoft 365) accounts once, keeps their OAuth tokens encrypted and fresh, and serves their calendar events in one normalized shape.

Authentication. Every /v1 route except the OAuth callback and the provider webhook endpoints requires Authorization: Bearer <api key>. Keys look like rly_live_… or rly_test_… and are issued with relayctl tenant create. Any key problem answers the same 401 {"error":"unauthorized"}. Webhooks are public; each subscription has its own secret (Google channel token, Graph clientState), checked in constant time. A wrong secret is 401 and is not enqueued.

Tenancy. Everything is scoped to the tenant that owns the key. An account of another tenant is indistinguishable from one that does not exist (404 not_found).

Errors. Error bodies are {"error": "<code>"}; query validation errors add a human-readable detail. Every response carries X-Request-ID.

Health

Probes for orchestrators (no API key).

Liveness probe

Responses

Readiness probe (database reachable)

Responses

Tenant

The tenant behind the API key.

The tenant that owns the API key

The simplest end-to-end check that a key works.

Authorizations:
apiKey

Responses

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "key_prefix": "rly_test_AbCdEfGh"
}

Connect

Link a Google or Microsoft account to the calling tenant (OAuth 2.0 authorization code + PKCE).

Start connecting a provider account

Creates a one-time state (valid 10 minutes) and a PKCE verifier, then sends the user to the provider's consent screen. Browsers cannot send your API key, so call this from your backend:

  • default: 302 with Location set to the consent URL;
  • with Accept: application/json: 200 with the URL in the body, to hand to your user's browser.

Reading is always included. features=calendar.write also asks for the scope that writes events. Connecting the same account again with it upgrades the account in place.

Authorizations:
apiKey
path Parameters
provider
required
string (Provider)
Enum: "google" "microsoft"
query Parameters
features
string
Value: "calendar.write"

Comma-separated optional capabilities.

Responses

Response samples

OAuth callback (called by the provider through the browser)

Public: the browser returns here from the consent screen without an API key. Security comes from the one-time state. Relay exchanges the code (with the PKCE verifier), checks the calendar scope, identifies the account, encrypts the tokens and stores the account. Reloading the same URL answers 400 invalid_state.

path Parameters
provider
required
string (Provider)
Enum: "google" "microsoft"
query Parameters
state
required
string
code
string

Present when the user accepted.

error
string

Present when the user declined (e.g. access_denied).

Responses

Response samples

Content type
application/json
{
  • "status": "connected",
  • "provider": "google",
  • "account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
  • "email": "user@example.com"
}

Accounts

The tenant's connected provider accounts.

List the tenant's connected accounts

Never includes tokens. Ordered by connection time; pass next_page_token back as page_token until it is absent.

Authorizations:
apiKey
query Parameters
max_results
integer [ 1 .. 100 ]
Default: 50
page_token
string

Opaque next_page_token from the previous page.

Responses

Response samples

Content type
application/json
{
  • "accounts": [
    ],
  • "next_page_token": "string"
}

Disconnect an account

Deletes the account's tokens and marks it disconnected. Google tokens are also revoked at Google (best effort). Idempotent; connecting the same provider account again reactivates it.

Authorizations:
apiKey
path Parameters
id
required
string <uuid>

The account_id returned by the OAuth callback.

Responses

Response samples

Content type
application/json
{
  • "error": "unauthorized"
}

Events

Calendar events, normalized across providers.

List events of a connected account's primary calendar

Google and Outlook events in one shape. Recurring events are expanded; cancelled occurrences are dropped. Access tokens are refreshed silently when they expire.

Paging: follow next_page_token until it is absent. A page may hold fewer than max_results events, but never zero unless it is the last. page_token carries the first page's window and size, so other query parameters are ignored with it.

Authorizations:
apiKey
path Parameters
id
required
string <uuid>

The account_id returned by the OAuth callback.

query Parameters
time_min
string <date-time>

Start of the window (RFC 3339). Defaults to now.

time_max
string <date-time>

End of the window (RFC 3339). Defaults to time_min + 30 days; at most 366 days after time_min.

max_results
integer [ 1 .. 250 ]
Default: 50
page_token
string <= 4096 characters

Opaque next_page_token from the previous page.

Responses

Response samples

Content type
application/json
{
  • "events": [
    ],
  • "next_page_token": "string"
}

Create an event

The same body for both providers. Relay translates the time zone and the recurrence, and refuses what it cannot translate exactly. Needs an account connected with features=calendar.write.

Attendees receive real invitations from the provider.

Authorizations:
apiKey
path Parameters
id
required
string <uuid>

The account_id returned by the OAuth callback.

header Parameters
Idempotency-Key
string <= 255 characters

Retrying with the same key and body returns the stored answer for 24 hours instead of writing again.

Request Body schema: application/json
required
title
string
location
string
description
string

Plain text.

start
required
string

RFC 3339 with an offset, or YYYY-MM-DD with all_day.

end
required
string

Same format as start; exclusive for all-day events.

timezone
string

IANA zone. Required for timed events.

all_day
boolean
Default: false
attendees
Array of strings <email> [ items <email > ]

Bare email addresses. They receive real invitations.

object (Recurrence)

The subset of RRULE both providers represent identically.

Responses

Request samples

Content type
application/json
{
  • "title": "string",
  • "location": "string",
  • "description": "string",
  • "start": "2026-11-02T09:00:00-05:00",
  • "end": "string",
  • "timezone": "America/Bogota",
  • "all_day": false,
  • "attendees": [
    ],
  • "recurrence": {
    }
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "title": "string",
  • "start": "2026-10-20T14:00:00Z",
  • "end": "string",
  • "timezone": "America/Bogota",
  • "all_day": true,
  • "location": "string",
  • "organizer": {
    },
  • "provider": "google",
  • "version": "string"
}

Update an event or a whole series

Only the fields sent change. start and end travel together. A single occurrence cannot be edited; use the series id returned on create.

Authorizations:
apiKey
path Parameters
id
required
string <uuid>

The account_id returned by the OAuth callback.

event_id
required
string

The event id. Use the series id returned on create for a recurring event.

header Parameters
Idempotency-Key
string <= 255 characters

Retrying with the same key and body returns the stored answer for 24 hours instead of writing again.

If-Match
string

The event's version. Refuses the edit if the event changed since.

Request Body schema: application/json
required
title
string
location
string
description
string
start
string
end
string
timezone
string
all_day
boolean
attendees
Array of strings <email> [ items <email > ]

Replaces the whole list.

object (Recurrence)

The subset of RRULE both providers represent identically.

Responses

Request samples

Content type
application/json
{
  • "title": "string",
  • "location": "string",
  • "description": "string",
  • "start": "string",
  • "end": "string",
  • "timezone": "string",
  • "all_day": true,
  • "attendees": [
    ],
  • "recurrence": {
    }
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "title": "string",
  • "start": "2026-10-20T14:00:00Z",
  • "end": "string",
  • "timezone": "America/Bogota",
  • "all_day": true,
  • "location": "string",
  • "organizer": {
    },
  • "provider": "google",
  • "version": "string"
}

Delete an event or a whole series

Attendees are notified by the provider. A single occurrence cannot be deleted.

Authorizations:
apiKey
path Parameters
id
required
string <uuid>

The account_id returned by the OAuth callback.

event_id
required
string

The event id. Use the series id returned on create for a recurring event.

header Parameters
Idempotency-Key
string <= 255 characters

Retrying with the same key and body returns the stored answer for 24 hours instead of writing again.

Responses

Response samples

Content type
application/json
{
  • "error": "unsupported_recurrence",
  • "detail": "until and count cannot be combined"
}

Webhooks

Public push endpoints. Google and Microsoft call these when a calendar changes. Relay verifies the per-subscription secret, enqueues a sync job and responds without calling the provider. Absent when WEBHOOK_BASE_URL is unset.

Google Calendar push notification

Public. Google sends X-Goog-Channel-Token, which Relay compares to the stored hash in constant time. X-Goog-Resource-State: sync is the initial confirmation and is ignored (200, nothing queued). Any other state enqueues relay.sync.account and answers 202. An unknown channel is 200 and is not queued. Relay does not call Google in this request.

header Parameters
X-Goog-Channel-ID
required
string
X-Goog-Channel-Token
required
string
X-Goog-Resource-State
required
string
Enum: "sync" "exists" "not_exists"
X-Goog-Message-Number
string

Responses

Response samples

Content type
application/json
{
  • "error": "string"
}

Microsoft Graph change notification

Public. While a subscription is being created, Graph calls this URL with validationToken. Relay answers 200 text/plain with that token, decoded, and nothing else. Otherwise each item's clientState is compared to the stored hash in constant time. One sync job is enqueued per account (202). A bad clientState is 401 and nothing is queued. Relay does not call Graph in this request.

query Parameters
validationToken
string

Present only for the subscription handshake.

Request Body schema: application/json
optional
Array of objects

Responses

Request samples

Content type
application/json
{
  • "value": [
    ]
}

Response samples

Content type
application/json
{
  • "error": "string"
}

Microsoft Graph subscription lifecycle notification

Public. reauthorizationRequired enqueues a renew, subscriptionRemoved enqueues a new subscription, and missed enqueues a sync. The provider is not called in this request. The same validationToken handshake as /v1/webhooks/microsoft applies.

query Parameters
validationToken
string

Responses

Response samples

Content type
application/json
{
  • "error": "string"
}