Agent recipes
Copyable end-to-end sequences — create, validate, and schedule a cross-platform post in one call, then pull analytics — using real Inklate tools and endpoints.
Two flows an agent can follow literally. Each is shown as an ordered sequence of MCP tool calls, with the equivalent REST path beside it. Names and arguments are exactly those the API exposes — do not invent others.
Recipe A — one call from nothing to a scheduled cross-platform post
The goal: one post published natively to a LinkedIn page and an X account, validated before it ships, scheduled for a future instant.
1. Find the channels
Call channels_list (REST: GET /channels) to get channel ids and connection health. Optionally call capabilities (GET /capabilities) to confirm each target can publish right now and to read its text limits. If the organization has exactly one channel per provider, you can skip this — channels accepts provider names ("x", "linkedin") and resolves them server-side.
2. Create and schedule — one call
Call posts_create (REST: POST /posts) with the content, the target channels, and the schedule:
{
"content": "We just closed our Series A — here's what's next.",
"channels": ["chn_linkedin_page", "x"],
"scheduled_at": "2026-07-21T09:00:00-07:00",
"idempotency_key": "series-a-announce-1"
}content is markdown, and it is the only way to write copy — one field, one form, on every door. Copy with no markup posts exactly as typed, so handles, URLs and snake_case survive verbatim; when a literal delimiter has to appear, escape it (\*\*not bold\*\*). Reads answer with the same string in content.markdown, so a response is a legal request: read it, edit it, send it back.
Your content fans out into a copy per channel automatically; per_channel overrides one channel’s copy or format when you want to tailor it (an X thread beside a LinkedIn post, say):
{ "per_channel": { "x": { "format": "thread" } } }The response carries everything: the post id, top-level ready, and a channels[] entry per target with its adapted copy (counted length against the provider limit), errors[], warnings[], and any fixes_applied — autofix (default on) repairs what it safely can, server-side, in the same call.
3. If the schedule was refused
Arming is atomic: if any channel isn’t ready, the scheduled write is refused (400, reason: "not_ready") and nothing is saved. The refusal answers with the post it refused — error.post over REST, JSON below the error line over MCP — so read errors[] straight off the call that failed instead of re-sending the write without scheduled_at. Each error names a code, the field to change, a message with the real numbers, and machine fixes where a repair exists — see Errors and fixes for the catalog. Apply a fix by writing the repaired content (or a new format) via posts_update / PATCH /posts/{id} with per_channel, or simply leave autofix on and let the server take the safe repairs.
To check without saving anything, posts_validate (REST: POST /posts/validate) is the dry run — pass the same inline draft, or a stored post’s id to re-check exactly what is saved, media readiness included.
4. Reschedule or change anything
posts_update (REST: PATCH /posts/{id}) changes content, channels, media, labels, campaign — and the schedule: a new scheduled_at time rebooks it, "now" publishes it, null returns it to draft. Every update re-validates and returns the same response shape. Pass expected_updated_at from your last read to make concurrent edits refuse instead of clobber.
Editing the shared content never rewrites a channel you tailored — that is the whole point of per_channel. When you do want an edit pushed out, name the channels:
{ "per_channel": { "x": { "copy_from": { "channel": "shared" } } } }To tailor one channel outright, read its content.markdown, edit the string, and send it back as per_channel: { "x": { "content": "…" } } — the same form in both directions. channel can also name another channel this post targets ("linkedin", or a channel id) to reuse its wording, and text / media scope which halves are overwritten (both default to true). per_channel: { "x": null } is the blunt version: reset that channel to the adapted shared content. An unknown key inside a per_channel entry is a 400 — nothing you send is silently ignored.
5. Confirm it shipped
After the scheduled instant, poll posts_get (REST: GET /posts/{id}) — there is no separate status call. Each channel entry fills in its publish result: status, external_post_id, the live url, or a sanitized error. To publish immediately instead of scheduling, either send "scheduled_at": "now" on create, or call posts_publish (REST: POST /posts/{id}/publish) on an existing draft with an idempotency_key so retries replay safely.
Recipe B — pull analytics for recent posts
The goal: gather engagement for the posts you published lately.
1. List recent published posts
Call posts_list (REST: GET /posts) filtered to status = published and, optionally, a scheduled-time range. Narrow further with campaign, label, or channel (a channel id or a provider name like linkedin) rather than pulling a range down and filtering locally. This returns { items, next_cursor } — posts with their per-channel targets and ids. It is keyset-paginated on both doors: pass next_cursor back as cursor for the following page, and limit to size it.
Rows are excerpts and carry no fit verdict by default, because a calendar range must not run a fit check per row. When the question is “which of these can I publish?”, ask for the field: include: ["ready"] (REST: ?include=ready) adds ready per row — the same verdict posts_get reports. That page is capped at 25 rows, since each judged row costs a full post read; page through next_cursor for more. An absent ready means the read never checked, which is not the same as false.
2. Pull per-post metrics
For each post id, call posts_metrics (REST: GET /posts/{id}/metrics). You get per-channel totals — impressions, reactions, comments, shares, clicks — plus the full snapshot time series for that post.
3. Or roll up by channel
To see a channel’s aggregate performance rather than one post’s, call channels_metrics (REST: GET /channels/{id}/metrics) for per-day engagement across that channel’s published posts, optionally restricted to a time range.
Metrics are synced from each platform in the background, not on every read. A freshly published post may show zero until the first sync lands. See Analytics syncing for how and when snapshots refresh.
Notes for agents
- Absolute timestamps always carry an offset. Send
2026-07-21T09:00:00-07:00or aZinstant. Naive local times are rejected with a corrective error. Thescheduled_atfield also accepts30m/2h/3d/1wshorthands, anchored to the server clock. - Idempotency. Pass an
idempotency_key(MCP) orIdempotency-Keyheader (REST) onposts_createandposts_publishso a retried call replays instead of duplicating. - Approvals. When the organization requires review, a post arms only once approved. Over MCP,
approvals_rulereads the org’s rule (so you know whether the gate applies),approvals_requestsubmits a post for review, andapprovals_deciderecords an approver’s verdict — only users the rule names as approvers may call it. - One call, not a saga. There is no separate workflow surface to start and poll.
posts_createalready validates every target, applies fixes, and arms the schedule or the immediate publish in one transaction — so a refused arm leaves nothing behind and there is nothing for you to compensate.
API keys
Create an Organization API key in the dashboard under Settings → API, copy it once, and send it as a bearer token to MCP or REST.
Billing in your account
How billing works inside the dashboard — what a subscription covers, seats, the Ink wallet, plan changes, statuses, and who handles payments.