Docs

REST API

Create, schedule and track posts on your connected LinkedIn and X accounts from a server, with an API key. Five endpoints: create, check, list and delete posts, and list accounts.

Versionv1AuthAPI keyRate limit10,000/day
https://antwork.io/api/v1
01

Authentication

4 scopes

Every request carries an API key as a bearer token. Keys are for servers. If you are connecting an assistant rather than writing code, use the MCP server instead, which signs in through your browser and needs no key at all.

Getting a key

Create one from your account settings. The full key is shown once, at the moment it is created, and cannot be retrieved afterwards because only its hash is stored. Losing it means creating another.

Authorization
Authorization: Bearer ak_a1b2c3d4e5f6a7b8_<secret>

Keys look like ak_<id>_<secret>. The <id> half is public and identifies the key in your settings; the secret half is the part that must never reach a repository, a log or a browser.

A revoked key stops working immediately on every server except one that used it in the last minute, which keeps a short cache. Revoke and rotate rather than waiting.

Scopes

A key carries scopes. write, publish and media each imply read. write and publish are separate rather than nested: creating a draft needs write, and creating a post that will actually go out needs publish. Keys created from settings carry all four scopes; there is no way to narrow a key there yet.

ScopeAllows
readRead posts and connected accounts.
writeCreate drafts, and delete or cancel posts.
publishCreate a post that will actually go out, now or on a schedule. Sufficient on its own.
mediaReserved for media endpoints. No endpoint checks it yet, so granting it changes nothing today.
02

Creating posts

POST /v1/posts

One request creates one post per target account. Each is an independent post with its own schedule and its own outcome; they share only a campaign id.

Request
curl https://antwork.io/api/v1/posts \
  -H "Authorization: Bearer $ANTWORK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "accounts": ["acc_linkedin", "acc_x"],
    "text": "Shipped the API today.",
    "scheduledFor": "2026-10-01T09:00:00Z"
  }'

Request body

FieldMeaning
accountsRequired. Account ids from GET /api/v1/accounts, at most 25. One post is created per account.
textThe post text, used for every account that has no entry in texts.
textsPer-account text, keyed by account id. Overrides text for that account. Use it when one platform needs a shorter version.
mediaAntwork media URLs, not arbitrary ones. A file has to be in your library before it can be attached; an external URL is refused with media_not_hosted rather than accepted and then failed at publish time. Uploading through the API is not available yet.
scheduledForISO 8601 time to publish at, at most 30 days ahead. Omit it to publish immediately.
drafttrue creates a draft and sends nothing, and needs only the write scope. Anything else creates a post that will go out, which needs publish.
optionsPlatform-specific settings, keyed by platform. See Platform options.
workspaceIdThe workspace to post from. Required only when you own more than one workspace: without it the request is refused with workspace_required. Every account must belong to it.
campaignIdGroups the posts under one id. Set for you when more than one account is accepted; pass an existing id to add these posts to that group.
The API publishes to LinkedIn and X (Twitter) accounts. An account on any other platform is refused on its own with platform_not_available, and the rest of the request goes ahead. GET /api/v1/accounts lists every connected account, so check platform before sending.

The answer is 202 Accepted, never 200. Nothing publishes inside the request: a post is queued and a worker sends it, so the response tells you what was accepted rather than what was published.

202 Accepted
{
  "object": "post_batch",
  "campaignId": "b3f1...",
  "posts": [
    {
      "object": "post_result",
      "id": "pQ7x...",
      "accountId": "acc_linkedin",
      "platform": "linkedin",
      "status": "accepted",
      "reason": null,
      "message": null
    },
    {
      "object": "post_result",
      "id": null,
      "accountId": "acc_x",
      "platform": "x",
      "status": "rejected",
      "reason": "text_too_long",
      "message": "@antwork: Text exceeds x character limit"
    }
  ]
}

What each target status means

  • accepted — written, and scheduled (or saved, for a draft). Poll the id to see what happened.
  • rejected — refused before anything was written. Nothing exists; fix reason and send again.
  • failed — the post was created but could not be scheduled. It exists and will not go out.
A target fails alone. One account over its character limit does not cancel the others, so a 202 can contain rejections. Read every entry in posts, not just the status code. Only when nothing at all was accepted is the answer a 400, and the array still ships with it.

Scheduling

Omit scheduledFor to publish immediately. Supply an ISO 8601 time to schedule, at most 30 days ahead. The queue behind this will not accept a task further out, and the error names the latest time it will take. A time in the past is refused with scheduled_for_in_past.

03

Checking a post

GET /v1/posts/{id}

The create call returns ids; this is where you learn what became of them. Poll rather than waiting on the create request, which returns long before the platform has answered.

200 OK
{
  "object": "post",
  "id": "pQ7x...",
  "workspaceId": "ws_...",
  "accountId": "acc_linkedin",
  "platform": "linkedin",
  "campaignId": "b3f1...",
  "status": "published",
  "text": "Shipped the API today.",
  "mediaUrls": [],
  "scheduledFor": "2026-10-01T09:00:00.000Z",
  "publishedAt": "2026-10-01T09:00:04.000Z",
  "publishedUrl": "https://www.linkedin.com/feed/update/...",
  "platformPostId": "urn:li:share:...",
  "error": null,
  "createdAt": "2026-09-22T10:00:00.000Z",
  "updatedAt": "2026-10-01T09:00:04.000Z"
}

Once it has gone out, publishedUrl and platformPostId are filled in. If it failed, error.message says what the platform said and error.advice says what to do about it.

A post that does not exist, belongs to someone else or has been deleted answers 404 with post_not_found. Needs the read scope.

Post statuses

  • draft — saved, not scheduled. It will not go out.
  • scheduled — waiting for its time, or being sent right now.
  • published — live on the platform. publishedUrl and platformPostId are set.
  • failed — did not go out. error says why.

Listing posts

GET /api/v1/posts is the recovery path: the only other way to reach a post is an id the create call returned once. Newest first, across every workspace you own. Optional status filter (one of the four above), limit from 1 to 100 (default 25), and cursor from the previous page's nextCursor. A null nextCursor means there are no more.

GET /v1/posts
curl "https://antwork.io/api/v1/posts?status=scheduled&limit=25" \
  -H "Authorization: Bearer $ANTWORK_API_KEY"

{ "object": "list", "posts": [ ... ], "nextCursor": "MTc5MDA3..." }
04

Listing accounts

GET /v1/accounts

The first call any integration makes, because creating a post takes account ids and there is no other way to learn them.

200 OK
{
  "object": "list",
  "accounts": [
    {
      "object": "account",
      "id": "acc_linkedin",
      "workspaceId": "ws_...",
      "platform": "linkedin",
      "name": "Iker on LinkedIn",
      "handle": "iker",
      "accountType": "personal",
      "active": true,
      "tokenHealth": "healthy",
      "connectedAt": "2026-01-05T09:00:00.000Z"
    }
  ]
}

Narrow with ?workspaceId= or ?platform=.

The list includes every connected account, including platforms the API cannot post to yet. Only LinkedIn and X (Twitter) accounts can be used in accounts when creating a post.

tokenHealth is worth reading before you post. A connection that needs reauthorising fails at publish time, which may be days after the request that scheduled it.
05

Cancelling and deleting

DELETE /v1/posts/{id}

DELETE /api/v1/posts/{id} cancels a scheduled post or removes a draft. A published post is soft-deleted so its engagement history survives, and disappears from every read on this API either way. Needs the write scope.

The post on the platform is left alone unless you ask. ?deleteFromPlatform=true also queues deletion of the live post, which is irreversible and visible to other people. Removing a post from Antwork and deleting it from LinkedIn are different intentions, so the API does not guess.

200 OK
{
  "object": "post_deleted",
  "id": "pQ7x...",
  "softDeleted": true,
  "platformDeletionQueuedFor": ["linkedin"]
}
softDeleted: true in the response means the document survives for analytics, not that the post is still visible. platformDeletionQueuedFor lists the platforms a deletion was queued for, which happens in the background like publishing does.
06

Platform options

Anything platform-specific goes in options, keyed by platform and applied to every account of that platform in the request. Only the platforms listed here accept options through the API.

PlatformAccepts
linkedinaccepts no options
xcommunityId, shareWithFollowers
options
{
  "accounts": ["acc_x"],
  "text": "Shipped the API today.",
  "options": {
    "x": { "communityId": "1493446837214", "shareWithFollowers": true }
  }
}
Unknown keys are refused rather than ignored. A typo is a 400 naming the field, not a post that quietly goes out without the setting you asked for.
07

Idempotency

Idempotency-Key

Send an Idempotency-Key header on every create. Without one, a network timeout followed by the retry your HTTP client makes on its own produces a second post.

The key is remembered for 24 hours, scoped to your account and to the exact request body.

  • Same key, same body, already finished — the original response is returned again and nothing new is created.
  • Same key while the first request is still running — 409. Retry in a moment.
  • Same key, different body — 422. Returning the first answer for a second request would be worse than refusing.
Only a completed create is remembered. A request rejected by validation is not, so fixing the body and sending it again under the same key works.
08

Rate limits

10,000/day

Each key allows 10,000 requests a day and 120 a minute. The ceilings exist to catch a runaway loop, not to ration normal use. If you are near either one, get in touch rather than working around it.

Headers
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9812
X-RateLimit-Reset: 1790035200
Retry-After: 37

Every response carries the current budget, not just the ones that are refused, so you never have to exhaust the limit to discover it. X-RateLimit-Reset is a Unix timestamp; Retry-After appears only on a 429 and is in seconds.

Requests are counted but never billed. Antwork charges per connected account, not per call, so this is a fair-use ceiling rather than a meter.
09

Errors

Every failure has the same shape. type is the class to branch on, code is the stable identifier for the specific problem, and param names the offending field where there is one. Treat message as copy for a human reading a log; it can change.

4xx / 5xx
{
  "error": {
    "type": "invalid_request_error",
    "code": "scheduled_for_too_far",
    "message": "Posts can be scheduled at most 30 days ahead. The latest accepted time is 2026-10-22T10:00:00.000Z.",
    "param": "scheduledFor"
  }
}
TypeMeans
authentication_errorThe key is missing, malformed, unknown or revoked. Always 401; code says which.
permission_errorThe key is valid but lacks the scope, or the account behind it has no active subscription. 403.
invalid_request_errorSomething about the request is wrong. 400, or 404 for a post that does not exist on your account.
rate_limit_errorA ceiling was reached. 429, with Retry-After.
api_errorSomething broke on our side. 500. Safe to retry with the same idempotency key.

Branch on code, never on message. The codes are part of the contract; the sentences are not.

Error codes

CodeStatus and meaning
api_key_missing401 — no Authorization: Bearer header.
api_key_malformed401 — the value is not shaped like an Antwork key.
api_key_unknown401 — no key with this id and secret exists.
api_key_revoked401 — the key was revoked in settings.
insufficient_scope403 — the key lacks the scope this call needs.
subscription_inactive403 — the account behind the key has no active subscription.
invalid_body400 — the body is not a JSON object.
accounts_required400accounts is missing or empty.
too_many_accounts400 — more accounts than one request allows.
invalid_text400text is not a string.
invalid_texts400texts is not an object of strings keyed by account id.
invalid_media400media is not an array of strings.
media_not_hosted400 — a media URL is not in your Antwork library.
invalid_scheduled_for400scheduledFor is not a valid ISO 8601 time.
scheduled_for_in_past400scheduledFor is in the past.
scheduled_for_too_far400scheduledFor is beyond the scheduling horizon.
workspace_required400 — you own several workspaces and sent no workspaceId.
workspace_not_found400workspaceId is not a workspace you own.
invalid_option400 — an options key is unknown, mistyped or for a platform not in the request.
no_targets_accepted400 — no account was accepted and none gave a more specific reason.
invalid_status400 — the status filter is not one of the four post statuses.
invalid_limit400limit is not a whole number in range.
invalid_cursor400cursor did not come from this API.
post_not_found404 — no such post on your account.
idempotency_in_flight409 — a request with this Idempotency-Key is still running.
idempotency_key_reused422 — this Idempotency-Key was used for a different body.
rate_limit_burst429 — over the per-minute limit.
rate_limit_daily429 — over the daily limit.
internal_error500 — something broke on our side.

When no account in a create request is accepted, the answer is a 400 whose code is the first account's rejection reason (see the next table), with the full posts array beside it.

Why an account was rejected

The reason on a rejected or failed entry in a create response.

CodeStatus and meaning
account_unresolvedThe id is not a connected account in this workspace.
platform_not_availableThe account is on a platform the API does not publish to yet.
text_emptyNo text for this account, and the platform needs some.
text_too_longThe text is over this platform's character limit.
media_requiredThe platform needs an image or video and none was attached.
unsupported_mediaThe platform does not accept one of the attached file types.
video_too_longThe video is longer than the platform allows.
token_invalidThe account's connection has expired. Reconnect it in Antwork.
token_refresh_failedThe connection could not be refreshed. Reconnect it in Antwork.
quota_exceededYour plan's post allowance for this cycle is used up.
schedule_failedThe post was created but could not be queued (failed). It exists and will not go out.