/

Product

The Slate API is out of beta and open for everyone starting today

9 min

read

Last updated

Article thumbnail

Starting today, everything you can do inside Slate, you can do from code.

The public API has been in private beta since September. Forty-three teams have been building on it. We have learned a lot, broken a few things, fixed them, and arrived at something we are confident enough to open up. This is that moment.

What follows is a complete walkthrough of what the API covers, how authentication works, how webhooks behave, and what the rate model looks like in practice. If you want to skip straight to the reference, the full documentation is live now.


Authentication

All API requests authenticate via bearer token. Tokens are workspace-scoped and generated from the API settings panel under your workspace preferences.

curl https://api.slate.app/v1/workspaces \
  --header "Authorization: Bearer $SLATE_API_KEY"
curl https://api.slate.app/v1/workspaces \
  --header "Authorization: Bearer $SLATE_API_KEY"
curl https://api.slate.app/v1/workspaces \
  --header "Authorization: Bearer $SLATE_API_KEY"

Each token carries a permission scope. You define the scope at generation time. A token scoped to projects:read cannot write. A token scoped to automations:write cannot read billing data. Scopes are additive and permanent per token. If you need a different scope combination, generate a new token.

Available scopes at launch:

  • projects:read projects:write

  • members:read members:write

  • documents:read documents:write

  • automations:read automations:write

  • webhooks:read webhooks:write

  • workspace:read

Billing and plan management are intentionally excluded from the public API at this stage.


What you can actually do with it

The API surface at v1 covers four primary resources: workspaces, projects, documents, and members. Each follows the same structural conventions.

# List all active projects in a workspace
curl https://api.slate.app/v1/projects \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --data '{"workspace_id": "ws_northwind", "status": "active"}'

# Create a project
curl https://api.slate.app/v1/projects \
  --request POST \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "workspace_id": "ws_northwind",
    "name": "Q1 Launch",
    "status": "active",
    "owner_id": "usr_chris",
    "visibility": "team"
  }'
# List all active projects in a workspace
curl https://api.slate.app/v1/projects \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --data '{"workspace_id": "ws_northwind", "status": "active"}'

# Create a project
curl https://api.slate.app/v1/projects \
  --request POST \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "workspace_id": "ws_northwind",
    "name": "Q1 Launch",
    "status": "active",
    "owner_id": "usr_chris",
    "visibility": "team"
  }'
# List all active projects in a workspace
curl https://api.slate.app/v1/projects \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --data '{"workspace_id": "ws_northwind", "status": "active"}'

# Create a project
curl https://api.slate.app/v1/projects \
  --request POST \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "workspace_id": "ws_northwind",
    "name": "Q1 Launch",
    "status": "active",
    "owner_id": "usr_chris",
    "visibility": "team"
  }'

Responses are consistent across every endpoint. You always get a data object, a meta block with pagination info where relevant, and an error field that is null on success. No endpoint returns a bare array. No endpoint returns an undocumented shape.

{
  "data": {
    "id": "proj_q1launch",
    "name": "Q1 Launch",
    "status": "active",
    "created_at": "2026-01-08T09:14:00Z",
    "owner": {
      "id": "usr_chris",
      "name": "Chris"
    }
  },
  "meta": null,
  "error"

{
  "data": {
    "id": "proj_q1launch",
    "name": "Q1 Launch",
    "status": "active",
    "created_at": "2026-01-08T09:14:00Z",
    "owner": {
      "id": "usr_chris",
      "name": "Chris"
    }
  },
  "meta": null,
  "error"

{
  "data": {
    "id": "proj_q1launch",
    "name": "Q1 Launch",
    "status": "active",
    "created_at": "2026-01-08T09:14:00Z",
    "owner": {
      "id": "usr_chris",
      "name": "Chris"
    }
  },
  "meta": null,
  "error"

This was a deliberate decision. Inconsistency in API response shapes is one of the most common sources of integration friction. We opted for predictability over brevity.


Documents

Document access through the API returns structured content, not raw markdown or HTML.

curl https://api.slate.app/v1/documents/doc_narrativeQ1 \
  --header "Authorization: Bearer $SLATE_API_KEY"
curl https://api.slate.app/v1/documents/doc_narrativeQ1 \
  --header "Authorization: Bearer $SLATE_API_KEY"
curl https://api.slate.app/v1/documents/doc_narrativeQ1 \
  --header "Authorization: Bearer $SLATE_API_KEY"

The response body includes a blocks array. Each block has a type, a content field, and optional metadata. This means you can parse, transform, and write back document content programmatically without touching a rendering layer.

Structured document output was the single most requested capability during the private beta. Teams building internal tooling, changelog generators, and status report automations all needed it. We made it a first-class part of the v1 surface rather than an afterthought.

Writing back follows the same block schema. Partial updates are supported. You pass only the blocks you want to modify along with their block IDs, and untouched blocks are preserved as-is.


Webhooks

Webhooks let Slate push events to your infrastructure the moment something changes rather than requiring you to poll.

Registering an endpoint:
curl https://api.slate.app/v1/webhooks \
  --request POST \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "workspace_id": "ws_northwind",
    "url": "https://your-service.com/hooks/slate",
    "events": [
      "project.created",
      "project.status.changed",
      "document.updated",
      "member.joined"
    ],
    "secret": "whsec_yourverificationsecret"
  }'
curl https://api.slate.app/v1/webhooks \
  --request POST \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "workspace_id": "ws_northwind",
    "url": "https://your-service.com/hooks/slate",
    "events": [
      "project.created",
      "project.status.changed",
      "document.updated",
      "member.joined"
    ],
    "secret": "whsec_yourverificationsecret"
  }'
curl https://api.slate.app/v1/webhooks \
  --request POST \
  --header "Authorization: Bearer $SLATE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "workspace_id": "ws_northwind",
    "url": "https://your-service.com/hooks/slate",
    "events": [
      "project.created",
      "project.status.changed",
      "document.updated",
      "member.joined"
    ],
    "secret": "whsec_yourverificationsecret"
  }'

Every outbound payload is signed with your webhook secret using HMAC-SHA256. Verify it on your end before processing:

# Node.js verification example
const signature = req.headers['slate-signature']
const body = req.rawBody

const expected = crypto
  .createHmac('sha256', process.env.SLATE_WEBHOOK_SECRET)
  .update(body)
  .digest('hex')

if (signature !== `sha256=${expected}`) {
  return res.status(401).send('Invalid signature'

# Node.js verification example
const signature = req.headers['slate-signature']
const body = req.rawBody

const expected = crypto
  .createHmac('sha256', process.env.SLATE_WEBHOOK_SECRET)
  .update(body)
  .digest('hex')

if (signature !== `sha256=${expected}`) {
  return res.status(401).send('Invalid signature'

# Node.js verification example
const signature = req.headers['slate-signature']
const body = req.rawBody

const expected = crypto
  .createHmac('sha256', process.env.SLATE_WEBHOOK_SECRET)
  .update(body)
  .digest('hex')

if (signature !== `sha256=${expected}`) {
  return res.status(401).send('Invalid signature'

Failed deliveries retry with exponential backoff across five attempts. The full delivery log, including response codes and retry history, is accessible in the webhook dashboard and via the API itself.


Rate limits

The rate model is per token, not per workspace. Two tokens from the same workspace have independent limits.

Plan

Requests per minute

Requests per day

Free

30

1,000

Pro

120

10,000

Team

600

100,000

When you hit a rate limit the response is a 429 with a Retry-After header indicating when the window resets. The X-RateLimit-Remaining header is present on every response so you can track headroom without waiting for a failure.


A note on what v1 does not include

Billing, plan management, and workspace-level settings are not part of the v1 surface. Atlas reasoning and AI features are also not accessible through the public API yet. Automations can be triggered and read but not fully configured through the API at this stage.

These are not permanent exclusions. They reflect a deliberate choice to ship a stable, well-documented v1 surface rather than a broad one with rough edges. Expanding the surface incrementally is how we intend to keep the API trustworthy over time.


What comes next

The things closest to shipping are a JavaScript SDK, full automation configuration via the API, and read access to the workspace activity log. A Python SDK is in progress and will follow shortly after.

# What the JS SDK will look like (preview)
import { Slate } from '@slate/sdk'

const client = new Slate({ apiKey: process.env.SLATE_API_KEY })

const projects = await client.projects.list({
  workspaceId: 'ws_northwind',
  status: 'active'
})
# What the JS SDK will look like (preview)
import { Slate } from '@slate/sdk'

const client = new Slate({ apiKey: process.env.SLATE_API_KEY })

const projects = await client.projects.list({
  workspaceId: 'ws_northwind',
  status: 'active'
})
# What the JS SDK will look like (preview)
import { Slate } from '@slate/sdk'

const client = new Slate({ apiKey: process.env.SLATE_API_KEY })

const projects = await client.projects.list({
  workspaceId: 'ws_northwind',
  status: 'active'
})

The SDK is a thin, typed wrapper around the REST API. It does not add abstractions. It adds type safety, consistent error handling, and removes the boilerplate of raw fetch calls.

The API is available on Pro and Team plans starting today. If you are on Free and want to explore what is possible before upgrading, the full reference documentation is public and requires no login to read.

We are looking forward to seeing what teams build.

Marc Navarro leads engineering at Slate, owning the API and the systems behind Atlas. He spent close to a decade in distributed infrastructure before moving into product.

Where modern teams operate.

Where modern teams operate.

Create a free website with Framer, the website builder loved by startups, designers and agencies.