---
title: API Reference
description: Automate publishing and manage content programmatically
---

The Firmreader API is a REST API that lets you create posts, manage channels, and query analytics programmatically.

## Base URL

```
https://api.firmreader.com/v1
```

## Authentication

Every request requires an API key in the `Authorization` header:

<CodeGroup>
```bash cURL
curl https://api.firmreader.com/v1/posts \
  -H "Authorization: Bearer fr_live_abc123"
```

```javascript Node.js
const response = await fetch('https://api.firmreader.com/v1/posts', {
  headers: {
    'Authorization': 'Bearer fr_live_abc123',
  },
});
```

```python Python
import requests

response = requests.get(
    'https://api.firmreader.com/v1/posts',
    headers={'Authorization': 'Bearer fr_live_abc123'},
)
```
</CodeGroup>

API keys are created in **Settings > API Keys**. Each key has a scope that limits what it can access:

| Scope | Permissions |
|-------|------------|
| `posts:read` | List and retrieve posts |
| `posts:write` | Create, update, and delete posts |
| `channels:read` | List and retrieve channels |
| `channels:write` | Create, update, and delete channels |
| `analytics:read` | Query engagement metrics |

## Rate limits

| Plan | Requests per minute |
|------|-------------------|
| Starter | 60 |
| Business | 300 |
| Enterprise | 1,000 |

Rate limit headers are included in every response:

```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 1708790400
```

## Error format

All errors follow a consistent format:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The channel_id field is required.",
    "param": "channel_id"
  }
}
```

| HTTP Status | Meaning |
|-------------|---------|
| 400 | Bad request — check your parameters |
| 401 | Unauthorized — invalid or missing API key |
| 403 | Forbidden — API key lacks required scope |
| 404 | Not found — resource doesn't exist |
| 429 | Rate limited — slow down |
| 500 | Server error — try again later |

## Pagination

List endpoints return paginated results. Use `cursor` and `limit` parameters:

```bash
curl "https://api.firmreader.com/v1/posts?limit=20&cursor=post_abc123" \
  -H "Authorization: Bearer fr_live_abc123"
```

The response includes a `next_cursor` field. When it's `null`, you've reached the end.

```json
{
  "data": [...],
  "next_cursor": "post_xyz789",
  "has_more": true
}
```
