# API Reference

Base URL, authentication, response format, errors, limits and the full list of Relaycast REST endpoints.


The Relaycast API is a JSON REST API. You can call it with any HTTP client: the examples use cURL, `fetch` in Node.js, and Python `requests`. There is no official SDK.

## Base URL

```http
https://social-publish-api.onrender.com
```

All resource endpoints are versioned under `/v1`. The health endpoints (`/health`, `/ready`) are at the root and need no authentication.

## Authentication

Every `/v1` request needs an API key. Create one in the dashboard under **[API Keys](https://social-publish-kappa.vercel.app/dashboard/api-keys)**. Send it in either header:

| Header | Example |
| --- | --- |
| `x-access-key` | `x-access-key: sp_live_…` |
| `Authorization` | `Authorization: Bearer sp_live_…` |

A key belongs to one project; everything you create with it (posts, accounts) is scoped to that project. Revoked keys stop working immediately. See [Authentication](/docs/authentication).

```bash tab="cURL"
curl https://social-publish-api.onrender.com/v1/accounts \
  -H "x-access-key: $RELAYCAST_API_KEY"
```

```ts tab="Node.js"
const res = await fetch("https://social-publish-api.onrender.com/v1/accounts", {
  headers: { "x-access-key": process.env.RELAYCAST_API_KEY! },
});
const { data } = await res.json();
```

```python tab="Python"
import os, requests

res = requests.get(
    "https://social-publish-api.onrender.com/v1/accounts",
    headers={"x-access-key": os.environ["RELAYCAST_API_KEY"]},
)
data = res.json()["data"]
```

## Requests

- Send JSON bodies with `content-type: application/json`.
- Request bodies are limited to **1 MB**. Media is never uploaded in the request body: pass a public `https` URL or a media library reference (`media://<id>`). See [Media & Uploads](/docs/media).
- Timestamps are ISO 8601 strings. `scheduledFor` must include a time zone (`Z` or an offset such as `+02:00`).
- IDs are UUIDs.

## Responses

Successful responses wrap the result in a `data` field, for single objects and lists alike:

```json title="Response 200"
{
  "data": [
    { "id": "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f", "platform": "twitter", "handle": "acme" }
  ]
}
```

## Errors

Errors use a single envelope with a machine-readable `code`, a human-readable `message`, and sometimes `details`:

```json title="Response 400"
{
  "error": {
    "code": "validation_error",
    "message": "Content is not valid for some platforms",
    "details": {
      "6f1c2a7e-4b8d-4f7a-9a51-2d3c4b5a6e7f": [
        { "field": "content", "message": "X posts allow 280 characters (links count as 23, emoji/CJK as 2); turn on Long post for Premium accounts; this is 312" }
      ]
    }
  }
}
```

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_json` | The body isn't valid JSON. |
| 400 | `validation_error` | The body doesn't match the schema, or content isn't valid for a platform. |
| 400 | `unknown_account` | An `accountId` doesn't exist in this project. |
| 400 | `account_inactive` | An account needs reconnecting. |
| 400 | `unknown_media` | A `media://` reference doesn't exist in this project's media library. |
| 400 | `invalid_credentials` | Bluesky rejected the handle or app password. |
| 401 | `unauthorized` | Missing or invalid API key. |
| 402 | `insufficient_credits` | Not enough credits for the post. |
| 404 | `not_found` | The resource or route doesn't exist. |
| 413 | `payload_too_large` | The request body is larger than 1 MB. |
| 429 | `rate_limited` | Too many requests for this API key. |
| 500 | `internal_error` | Unexpected server error. |

`validation_error` has two `details` shapes:

- **Schema errors** (wrong types, missing fields): an array of issues, each with `path` (for example `["targets", 0, "accountId"]`), `message` and `code`.
- **Platform errors**: an object keyed by account ID, each holding a list of `{ field, message }`.

See [Errors](/docs/errors) for handling advice.

## Rate limits

Each API key may make **120 requests per minute** by default (fixed one-minute window; self-hosted deployments set `RATE_LIMIT_PER_MINUTE`). Responses include:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per minute |
| `X-RateLimit-Remaining` | Requests left in the current minute |

Over the limit, the API returns `429` with code `rate_limited`; retry after the minute rolls over. See [Rate Limits](/docs/limits).

## Lists and limits

`GET /v1/posts` returns the most recent posts first. Use `limit` (1–100, default 20). There is no cursor pagination; to browse or filter older posts, use **Posts** in the dashboard.

| Limit | Value |
| --- | --- |
| Accounts per post (`targets`) | 1–20 |
| Media items per post | 10 |
| `content` | 1–10,000 characters (platforms have lower limits) |
| Media URL | 2,048 characters |
| Request body | 1 MB |

## Idempotency

`POST /v1/posts` accepts an `Idempotency-Key` header. Retrying with the same key returns the post created by the first request (`200`) instead of creating and charging for a new one (`201`). See [Idempotency & Retries](/docs/idempotency).

## Credits

Each post costs **1 credit per target account**, charged when the post is created. Targets that fail permanently are refunded. See [Credits & Usage](/docs/credits).

## Endpoints

| Method | Path | Description |
| --- | --- | --- |
| `GET` | [`/v1/accounts`](/docs/api/list-accounts) | List connected social accounts |
| `POST` | [`/v1/accounts/connect-links`](/docs/api/create-connect-link) | Create a hosted link your user opens to connect an OAuth account |
| `GET` | [`/v1/accounts/connect-links/{id}`](/docs/api/get-connect-link) | Check a connect link's result |
| `POST` | [`/v1/accounts/bluesky`](/docs/api/connect-bluesky) | Connect a Bluesky account with an app password |
| `POST` | [`/v1/posts`](/docs/api/create-post) | Create a post (publish now or schedule) |
| `GET` | [`/v1/posts`](/docs/api/list-posts) | List recent posts |
| `GET` | [`/v1/posts/{id}`](/docs/api/get-post) | Get a post and its per-platform targets |
| `POST` | [`/v1/live`](/docs/api/create-live) | Start or schedule a live stream |
| `GET` | [`/v1/live`](/docs/api/list-live) | List live streams |
| `GET` | [`/v1/live/usage`](/docs/api/live-usage) | Live minutes used this month |
| `GET` | [`/v1/live/{id}`](/docs/api/get-live) | Get a live stream and its destinations |
| `POST` | [`/v1/live/{id}/stop`](/docs/api/stop-live) | Stop (or cancel) a live stream |
| `POST` | [`/v1/live/{id}/relaunch`](/docs/api/relaunch-live) | Go live again with the same setup |
| `GET` | [`/health`](/docs/api/health) | Liveness check |
| `GET` | [`/ready`](/docs/api/health) | Readiness check (database and queue) |

Events are delivered to your server as [webhooks](/docs/api/webhook-events).

> [!NOTE]
> Some operations are only available in the dashboard: connecting OAuth platforms (**Integrations**), uploading files to the media library, deleting or cancelling posts, post analytics, managing API keys, and configuring notifications.

## Request logs

Every authenticated `/v1` request is recorded in the dashboard under **[Logs](https://social-publish-kappa.vercel.app/dashboard/logs)**, including requests rejected by the rate limiter. See [Request Logs](/docs/logs).
