> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ziplined.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Basics

> Auth, scopes, paging, idempotency and rate limits.

Six rules that hold for every endpoint in the reference.

## Base URL and auth

```bash theme={null}
curl -H "Authorization: Bearer $ZIPLINED_API_KEY" \
  "https://api.ziplined.io/v1/people?limit=5"
```

Two kinds of token work: a `zpl_` key from **Settings, Integrations, API keys**, or a Clerk
OAuth token. The workspace comes from the credential, never from a parameter.

> [accordion] Which paths need no credential
>
> `GET /v1/health` and `GET /v1/openapi.json` are open.
>
> A key belongs to one workspace. A Clerk token gets its workspace from the organization you
> picked at consent.

## Scopes

A key carries the scopes you ticked when you made it.

| Scope | Grants |
| - | - |
| `read` | Read people, companies, posts, engagement |
| `write` | Create and update those records |
| `runs` | Queue background jobs |
| `contacts` | `email`, `phone` and their status |

A missing scope is a 403 `forbidden`. A person on OAuth has every scope their membership
allows.

> [accordion] Admin, and the contacts scope
>
> `admin` exists inside the product but is never handed out on a key. A key that could revoke
> keys is refused by the route and by the screen that issues them.
>
> `contacts` is part of the API vocabulary. The key screen currently offers `read`, `write`
> and `runs`. Without `contacts` the contact fields are absent, not `null`.

## Paging

```bash theme={null}
curl -H "Authorization: Bearer $ZIPLINED_API_KEY" \
  "https://api.ziplined.io/v1/people?limit=100&cursor=eyJvIjoxMDB9"
```

Every list answers `{ "data": [...], "next_cursor": "..." }`. Pass `limit` (default 50,
maximum 200) and send `next_cursor` back as `?cursor=`. Stop when it is `null`.

> [accordion] Envelope, filters and formats
>
> A single object answers `{ "data": { ... } }`. `GET /v1/people` adds `total`, which is how
> many rows match your filters before paging.
>
> The cursor is opaque. Do not parse one or construct one.
>
> Filters take several values comma separated: `?country=NL,BE`. Dates are UTC days as
> `YYYY-MM-DD`. Booleans are literally `true` or `false`.
>
> An unknown query parameter is a 400. A filter typo never reads as a filtered list.

## Idempotency

```bash theme={null}
curl -X POST https://api.ziplined.io/v1/people/7b1f.../touches \
  -H "Authorization: Bearer $ZIPLINED_API_KEY" \
  -H "Idempotency-Key: 3f1b2c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" \
  -H "Content-Type: application/json" \
  -d '{"channel":"email","note":"Sent the Q4 deck"}'
```

Every POST that creates something requires an `Idempotency-Key`. Any client-chosen value
works, a UUID for instance. `PATCH` and `DELETE` do not take the header.

> [accordion] What a repeat does
>
> The same request twice with the same key has one effect and returns the same response
> twice.
>
> The same key with a different body is a 409 `conflict`. So is a repeat while the first
> request is still in flight. A POST without the header is a 400.

## Rate limits

```
RateLimit-Limit: 60
RateLimit-Remaining: 43
RateLimit-Reset: 17
```

60 requests a minute per credential by default, or your plan's limit if it is lower. The
REST API and MCP share one bucket per credential.

> [accordion] Going over
>
> Over the limit is a 429 `rate_limited` with a `Retry-After` header in seconds.
>
> `Retry-After` is the time until one request is allowed, not until the bucket is full.
> `RateLimit-Reset` is the time until it is full again. Back off on `Retry-After`.

## Errors

```json theme={null}
{
  "error": {
    "code": "invalid_input",
    "message": "limit: Number must be less than or equal to 200",
    "details": { "field": "limit" }
  },
  "request_id": "01J9K..."
}
```

`code` is the contract and is stable. `message` is for humans and may change. The full list
is on [Errors](https://docs.ziplined.io/api/errors.md).

> [accordion] Plan gating
>
> The REST API sits behind the `api_keys` feature and MCP behind `mcp`. A plan with neither
> answers 402 `feature_not_in_plan` on every call.
>
> `details` then carries `feature` and an `upgrade_url`. A usage cap that was exceeded
> answers 402 `limit_exceeded` in the same shape. Show the URL rather than retrying.
>
> ```json theme={null}
> {
>   "error": {
>     "code": "feature_not_in_plan",
>     "message": "api_keys is not part of this plan",
>     "details": {
>       "feature": "api_keys",
>       "upgrade_url": "https://app.ziplined.io/settings/billing?feature=api_keys"
>     }
>   },
>   "request_id": "01J9K..."
> }
> ```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.