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.jsonThat 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 (prefixinklate_) or an OAuth 2.1 access token.x-api-key: <token>is also accepted;Authorization: Beareris canonical. - Content type:
application/jsonfor 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:
- Find your destinations —
GET /channels, thenGET /capabilitiesfor what each one will actually accept right now. (channelsalso takes provider names like"x"directly.) - Post —
POST /postscreates a draft, a scheduled post, or a published post in one request, decided byschedule. Every response carries per-channel validation —ready, errors with machine fixes, warnings, applied autofixes.POST /posts/validateis the same verdict as a dry run, persisting nothing. - Change —
PATCH /posts/{id}edits anything, the schedule included (nullunschedules);POST /posts/{id}/publishpublishes an existing draft. Writes are safe to retry with an idempotency key. - Track —
GET /posts/{id}: each channel entry fills in its publish result (status, external id, URL, last error). There is no separate status operation. - Measure —
GET /posts/{id}/metricsandGET /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.