For agents

Errors and fixes

The machine catalog — HTTP statuses, reason slugs, per-channel validation codes, the five fix kinds with their parameters, and the upgrade markers — everything an agent branches on.

Inklate’s errors are written to be acted on, and every machine-readable part of them is stable. This page is the full catalog: what a failed request looks like on each door, the reason slugs you can branch on, the validation codes that appear inside a successful post response, the five fix kinds and their parameters, and the upgrade markers on plan refusals.

Two places errors appear

  1. Request errors — the call itself was refused (bad input, missing auth, rate limit). REST returns a non-2xx status with the one error envelope; MCP returns the same facts as a prefixed text line.
  2. Validation errors — the call succeeded (a draft with problems saves fine) and the response’s per-channel errors[] tell you what would stop publishing. These never use HTTP status; read them off the 2xx response.

The bridge between the two: arming (setting a scheduled_at or publishing) a post that isn’t ready is refused as a request error — 400 BAD_REQUEST with reason: "not_ready" — and the whole write rolls back.

The refusal answers with the post it refused. You do not need a second call to find out why: the error carries the same post object a successful call would have returned, per-channel errors[] and machine fixes[] included. On a refused create its id is null, because the rollback means nothing was stored; on a refused update or publish the id is real and the post still stands at its previous state.

Request errors: one code table, every door

Every refusal maps through one kind→status table, so the same failure reads the same over REST, MCP, and the CLI:

CodeHTTP statusMeaning
BAD_REQUEST400Corrigible input — the message says what to change.
UNAUTHORIZED401Missing or invalid credential.
FORBIDDEN403Not allowed — permissions, or a plan limit (see upgrade markers below).
NOT_FOUND404The resource doesn’t exist in this organization.
CONFLICT409A concurrent write won (e.g. a stale expected_updated_at).
TOO_MANY_REQUESTS429Request budget exhausted — back off and retry.
INTERNAL_SERVER_ERROR500Nothing you can correct; the message is deliberately opaque.

REST envelope:

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "This post has 2 channels that are not ready — fix the errors first",
    "reason": "not_ready",
    "request_id": "req_…",
    "post": {
      "id": null,
      "ready": false,
      "channels": [
        {
          "channel_id": "ch_…",
          "provider": "x",
          "ready": false,
          "errors": [
            {
              "code": "validation",
              "field": "content",
              "message": "319 characters is over X's 280 limit",
              "fixes": [{ "kind": "trim_to_fold", "label": "Trim to 280", "max_length": 280 }]
            }
          ]
        }
      ]
    }
  }
}

error.post appears only on refusals that were decided by inspecting a post — today, arming. Every other refusal omits it.

Over MCP the same facts ride inline, so an agent parses a slug, not prose. The post follows the marker line as plain JSON:

BAD_REQUEST: This post has 2 channels that are not ready — fix the errors first [reason:not_ready]
{
  "id": null,
  "ready": false,
  "channels": [ … ]
}

The bracket marker carries reason:<slug>, index:<n> (which entry of a batch input failed), and retry_after_ms:<n> when present. The CLI exits 4 on any typed refusal, prints the same per-channel ✓/✗ summary a successful write prints, and puts the full refusal — error.post included — on stdout.

Reason slugs

reason is the finer machine slug on a request error, present where one exists:

ReasonOn codeWhenWhat to do
not_readyBAD_REQUESTYou tried to schedule/publish a post whose channels, media, or content aren’t ready.Read error.post — it carries the per-channel errors[] and fixes[].
unknown_channelBAD_REQUESTA channels entry matched no channel id and no connected provider.Call channels_list / GET /channels; the message lists valid providers.
ambiguous_channelBAD_REQUESTA provider alias (e.g. "linkedin") matched more than one channel.Use the exact channel id; the message lists the candidates.
invalid_cursorBAD_REQUESTA pagination cursor was malformed or hand-built.Cursors are opaque — re-read from the first page.
rate_limitedTOO_MANY_REQUESTSThe per-organization request budget is spent. Carries retry_after_ms (REST: Retry-After).Wait that long, then retry.
slow_downTOO_MANY_REQUESTSDevice-flow polling faster than the interval.Increase the polling interval.

Media failure reasons

An uploaded or URL-ingested file that fails carries error.reason on the file object (and failed: true in a post response’s media[] entry). The reasons are a closed set:

ReasonMeaning
too_largeOver the size limit for its type.
unsupported_typeA type Inklate doesn’t accept.
content_mismatchThe bytes don’t match the declared type (spoofed magic bytes).
emptyZero bytes arrived.
upload_incompleteThe upload never finished.
fetch_failedA URL reference couldn’t be fetched.
decode_failedThe file couldn’t be decoded/processed.
infectedFlagged by the virus scan.

All of them are terminal: remove or replace the file. A file that is merely still processing is not an error — wait, then re-check.

Validation codes on post responses

Every post response (create, update, get, validate) carries channels[], each with ready and errors[]. An error is:

{
  "code": "validation",
  "field": "content",
  "message": "Text is 312 characters — X allows 280",
  "fixes": [{ "kind": "trim_to_fold", "label": "Trim to 280 characters", "max_length": 280 }]
}

code is the verdict category, field names the input to change, message carries the real numbers, and fixes the machine repairs where any exist.

CodeFieldMeaning
authchannelThe channel is disconnected, its grant expired/revoked, or a scope is missing — reconnect it.
validationcontentThe content breaks the platform’s rules (length, media count, aspect, options). Often fixable.
unsupportedchannelThe provider doesn’t support this operation or format.
platform_unavailablechannelThe provider is down — retry later.
content_policycontentThe platform refused the content itself.
unknowncontentAn indeterminate provider failure.
rate_limitscheduled_atThe provider’s publish window/quota for this channel is exhausted — schedule later.
quiet_hoursscheduled_atThe schedule time falls inside the organization’s quiet hours.
banned_termcontentThe copy contains a term the organization blocks.
blocked_linkcontentThe copy links to a domain the organization blocks.
emptycontentNo content for this channel — add text or media.
not_foundchannelThe channel vanished from the organization mid-request.

Only a validation refusal carries fixes — a policy refusal (quiet_hours, banned_term, blocked_link) names the rule but has no automatic repair. Soft issues arrive separately as warnings[] (codes: truncated, dropped_media, missing_media, dropped_mark, flattened_link) and never block publishing.

The five fix kinds

A fix is a machine repair, discriminated on kind. Applied server-side when autofix is on (the default); a caller preferring manual control writes the fixed content via per_channel.

KindParametersRepairsAuto-applied?
trim_to_foldmax_lengthOver-length text — trims to the limit.Yes
rebalance_threaditem_max, item_max_length (nullable)Too many / too-long thread items — caps and trims.Yes
set_titlemax_lengthA video format that requires a title — adds one within the limit.Yes
split_to_threaditem_max_length, item_maxOver-length post on a thread-capable provider — reshapes losslessly.No — changes the format; opt in via per_channel.
crop_toaspect_ratioAn image outside the required ratio — crops a derivative.No — needs an explicit media decision.

Every kind also carries a human label (what the composer puts on the button). Fixes that were applied by autofix are reported in the response’s fixes_applied (channel_id, provider, kind). An applied fix marks that channel copy authored, so a later story edit never silently discards it.

Upgrade markers: [upgrade:<kind>]

A plan-limit refusal is always FORBIDDEN (403), and its message ends with a structured suffix an agent can parse — "… [upgrade:seats]":

KindRefused because…What a client does
tierA feature or free-org limit of the plan.Offer a plan upgrade (a human decision — link the dashboard).
seatsInviting past the included seat count.Add seats, or remove a member.
creditsThe prepaid Ink balance is exhausted.A human tops up in the dashboard; agents cannot move money.
channelsThe connected-channel cap.Upgrade, or disconnect a channel.
brandsThe organizations-per-subscription cap.Upgrade, or consolidate organizations.
topupA top-up path failure (reserved).Send the user to billing in the dashboard.

Billing is read-only for agents: billing_plan and billing_ink report state over MCP, but checkout and top-ups happen in the dashboard, by a person.