API Reference

The REST API at api.inklate.com/api/v1 — bearer auth, one error envelope, keyset pagination, idempotent writes, and an operation-by-operation reference generated from the live endpoint definitions.

Every operation on this reference is generated from the endpoint definitions the API actually serves, so nothing here can describe a route that doesn’t exist. If you would rather read the machine-readable form, it is one request away:

curl https://api.inklate.com/api/v1/openapi.json

That document is OpenAPI 3.1, served unauthenticated, and is the contract to generate a client against — Swagger UI, openapi-generator, @hey-api/openapi-ts, or your own codegen. The inklate CLI and the MCP server are themselves thin clients over these same operations.

Base URL and auth

  • Base URL: https://api.inklate.com/api/v1
  • Auth header: Authorization: Bearer <token> — either an Organization API key (prefix inklate_) or an OAuth 2.1 access token. x-api-key: <token> is also accepted; Authorization: Bearer is canonical.
  • Content type: application/json for request bodies.

Create a key under Settings → API in the dashboard — see API keys. A key is bound to one organization, so requests act on that organization automatically. An OAuth token can reach several: pass ?organization=<slug> to choose per request.

A missing or invalid credential returns 401. The only unauthenticated operations are the device authorization pair — how a CLI obtains a credential without ever handling a password — and /openapi.json.

One error envelope

Every failure, from any operation, has the same shape:

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "No connected channel matches \"linkedin\". Connected providers: x, facebook.",
    "reason": "unknown_channel",
    "request_id": "req_…"
  }
}

code is the stable class to branch on (UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, TOO_MANY_REQUESTS, BAD_REQUEST, INTERNAL). reason is a finer slug where one exists, index points at the failing element for batch inputs, and request_id is what to quote in a support thread. message is written to be shown to a human — or read by an agent deciding what to do next. The full catalog of codes, reasons, and the validation errors that ride inside successful post responses is on Errors and fixes.

Pagination

List operations are keyset-paginated: pass limit and an opaque cursor, and read the next one off the response.

{ "items": [], "next_cursor": "eyJ…" }

A null next_cursor means the end. Cursors are opaque — don’t parse or construct them; page until the cursor is null rather than counting. A few lists are deliberately unpaginated because their size is structurally bounded (channels, labels, projects, organizations); those return a plain array.

Idempotent writes

Writes that create or publish accept an Idempotency-Key header. A retry carrying the same key returns the original result instead of acting twice — the safe way for an at-least-once agent to recover from a timeout it cannot interpret.

curl -X POST https://api.inklate.com/api/v1/posts \
  -H "Authorization: Bearer $INKLATE_API_KEY" \
  -H "Idempotency-Key: 9f1c…" \
  -H "Content-Type: application/json" \
  -d '{"content":"Shipping today.","channels":["x"]}'

Concurrent edits

When two actors work the same post — two agents, or an agent and a person in the composer — a write can assert what it is changing. GET /posts/{id} returns an ETag; send it back as If-Match on the PATCH or the publish and a post that moved since your read is refused 409 instead of clobbering the newer edit.

GET /api/v1/posts/post_123          →  ETag: "2026-07-29T10:04:11.802Z"
PATCH /api/v1/posts/post_123           If-Match: "2026-07-29T10:04:11.802Z"

The entity tag is the post’s updated_at, so the header and the expected_updated_at body field (the spelling every other door uses) can never name different versions. Send either; the body field wins if you send both. Omit both for last-write-wins.

The shape of the work

The operations group by resource in the sidebar, but the lifecycle is one call wide:

  1. Find your destinationsGET /channels, then GET /capabilities for what each one will actually accept right now. (channels also takes provider names like "x" directly.)
  2. PostPOST /posts creates a draft, a scheduled post, or a published post in one request, decided by schedule. Every response carries per-channel validation — ready, errors with machine fixes, warnings, applied autofixes. POST /posts/validate is the same verdict as a dry run, persisting nothing.
  3. ChangePATCH /posts/{id} edits anything, the schedule included (null unschedules); POST /posts/{id}/publish publishes an existing draft. Writes are safe to retry with an idempotency key.
  4. TrackGET /posts/{id}: each channel entry fills in its publish result (status, external id, URL, last error). There is no separate status operation.
  5. MeasureGET /posts/{id}/metrics and GET /channels/{id}/metrics.

Media never transits this API: POST /files/upload returns a presigned target and your client sends the bytes straight to storage — see Files & media.

Rate limits

Requests are charged against a per-organization budget. Exceeding it returns 429 with the standard envelope; back off and retry. Publishing additionally respects each platform’s own limits, surfaced as a per-channel validation error rather than a request failure.