# REST API conventions: pagination and errors
URL: https://skedcast.com/developers/conventions

This REST API pagination and errors guide covers the conventions every SkedCast endpoint shares: response envelopes, keyset (cursor) pagination, idempotency keys for safe retries, correlation IDs for support requests, and the RFC 9457 problem+json error format. Learn them once and they apply across the whole API.

## Response envelopes
Single resources return `{ data, meta }`. Collections return `{ data, page, meta }`. `meta.correlationId` is the request's trace id — include it when contacting support.

## List envelope.
```json
{
  "data": [ { "object": "client", "id": "…" }, … ],
  "page": { "cursor": null, "nextCursor": "eyJ…", "hasMore": true },
  "meta": { "correlationId": "c-…" }
}
```

## REST API pagination
Lists use keyset (cursor) pagination — stable under inserts and fast at any depth. Pass `limit` (1–100) and the `nextCursor` from the previous page as `cursor`. Iterate until `hasMore` is false.

## Example
```bash
curl "https://api.skedcast.com/v1/posts?limit=50&cursor=eyJ…" \
  -H "Authorization: Bearer sked_live_YOUR_KEY"
```

## Idempotency
Every mutating request — POST, PATCH and DELETE — must carry an `Idempotency-Key` header (any unique string — a UUID is ideal). Replaying the same key returns the original result instead of creating a duplicate, so a network retry is always safe.

## Standard headers
Header | Direction | Purpose
`Authorization` | request | `Bearer sked_…` (API key) or `Bearer <token>` (OAuth)
`Idempotency-Key` | request | Required on POST, PATCH and DELETE; dedupes retries
`X-Correlation-Id` | both | Trace id; echoed in `meta.correlationId`
`RateLimit-*` | response | Your budget, what is left, and seconds to reset (on a 429: `Retry-After`)

## Rate limits
Programmatic access is limited per client: each API key or OAuth app has its own request budget for the minute, set by your plan (below). A batch request counts one unit per item, so a call that creates 50 posts uses 50 units. Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets); a `429` adds `Retry-After`. Read those headers and back off — don't hard-code delays.
The MCP server has one shared budget per connected client, and a tighter share of it for the tools that delete or cancel (delete a client, campaign or webhook, cancel a post, and the batch action that can cancel many at once). Scheduling and publishing are ordinary writes and use the shared budget only. The `429` says which budget ran out, so an agent can back off on that class alone.
A token is only charged for what it could actually do: a call to a delete, cancel or batch tool that the token's scopes or role do not allow costs the ordinary one unit, so a read-only token cannot use up the allowance your other tokens share. In Settings → Developer, each API key has a Usage view showing its requests and refused requests per day and over the last 24 hours, next to the budget it is counted against.
You can also set a WRITE limit on an individual API key, or on a connected app for your whole workspace, in Settings → Developer. Reads keep your plan's budget; only writes — anything that is not a GET on the REST API, and any tool that changes something over MCP — draw on it. A batch counts its items, and a bulk action counts the destinations it touches, so one call that cancels 500 posts uses 500. A write limit can only tighten your plan's budget, never raise it, so it is a way to let a new agent read freely while it publishes carefully. Leave it unset and nothing changes. A `429` from a write limit says which budget ran out; note that a refused request still counts toward your plan's overall budget, so back off rather than retrying immediately.
Separately, every route has an outer per-IP bound against abuse, and first-party credential endpoints (sign-in and password reset) are tighter at 10 requests per minute per IP. What is actually published still respects each platform's own limits and your plan's post caps, however fast you call.

## Reference
Plan | API requests / min | MCP requests / min | MCP destructive / min
Solo | Not included | 60 | 10
Studio | 120 | 60 | 20
Agency | 300 | 120 | 40
Agency+ | 600 | 240 | 80
Enterprise | 1200 | 480 | 160

## Errors
Errors are RFC 9457 `application/problem+json`. Every error carries a stable machine `code`, an HTTP `status`, a human `title`/`detail`, and the `correlationId`. Validation failures (422) add a per-field `errors` array.

## Error shape.
```json
{
  "type": "https://skedcast.com/errors/validation_failed",
  "title": "Validation failed",
  "status": 422,
  "category": "bad-body",
  "code": "validation_failed",
  "detail": "One or more fields are invalid.",
  "correlationId": "c-…",
  "errors": [ { "path": "content", "message": "Required" } ]
}
```

## OAuth endpoints use the OAuth error format
The /v1 REST API uses RFC 9457 problem+json (above). The OAuth wire endpoints — `/oauth/token`, `/oauth/register`, `/oauth/revoke` — instead return the RFC 6749 §5.2 shape, `{ "error": "invalid_grant", "error_description": "…" }` as `application/json` with a 4xx status, because that's what OAuth client libraries parse.

## Status codes
Status | Meaning
400 / 422 | Malformed request / validation failed
401 | Missing or invalid credentials
403 | Authenticated but lacking the scope/role
404 | Resource not found
409 | Conflict (idempotency or state)
429 | Rate limited — back off per `Retry-After`
5xx | Transient/server error — safe to retry

## FAQ
**Q: Why keyset pagination instead of page numbers?**
A: Keyset (cursor) pagination is stable when rows are inserted or deleted mid-iteration and stays fast at any depth, unlike offset pagination. Just follow nextCursor until hasMore is false.
