Skip to content

API version: 2026-09-13

Developer documentation

Everything the application can do, for programs: brands, scheduled posts, media, approvals, the inbox, campaigns, SmartLinks, analytics, paid media, report share links, team rosters and webhooks. The machine-readable contract is the OpenAPI 3.1 document at /api/v1/openapi.json.

Authentication

Create a token in Settings → API access. It is shown once. Send it as a bearer token on every request:

curl https://social.iutils.net/api/v1/brands \
  -H "Authorization: Bearer social_…" \
  -H "Accept: application/json"

A token acts as the person who minted it, inside the workspace it was minted in, with that person's brand roles. It stops working when it is revoked, when it expires, when its holder leaves the workspace, or when the workspace's plan no longer includes API access — each refusal says which.

Scopes

A token is minted with scopes and every endpoint declares the one it needs. A call outside the token's scopes is refused with 403 and the code insufficient_scope.

ScopeUnlocks
brands:readList and read the brands the token holder belongs to.
brands:writeCreate and update brands.
posts:readList and read scheduled posts.
posts:writeCreate, update, cancel and publish scheduled posts.
media:readList and read media library assets.
media:writeUpload media library assets.
approvals:readList posts awaiting review.
approvals:writeApprove, reject and request changes on posts.
inbox:readList and read inbox threads.
inbox:writeReply to, resolve and assign inbox threads.
campaigns:readList content campaigns.
campaigns:writeEdit content campaigns: name, description and dates.
smartlinks:readList SmartLinks and read their statistics.
analytics:readRead analytics reports, daily series and post tables.
ads:readRead paid-media accounts, campaigns and daily metrics.
reports:readList report share links.
reports:writeCreate and revoke report share links.
team:readRead a brand's team roster.
webhooks:manageManage webhook subscriptions and deliveries.

Brand permissions

Scopes say what a token may attempt; the holder's brand role says what they may do. The API asks the same questions the screens do: analytics needs analytics_view, the inbox needs inbox_view to read and inbox_edit to reply, scheduling needs schedule_and_publish, approvals need review_posts, brand settings need brand_manage. A refusal is 403 with the code forbidden and the permission named in the message. A brand the holder is not a member of answers 404.

Pages and cursors

List endpoints answer with a data array and a meta object. Follow meta.next until it is null. per_page defaults to 25 and is capped at 100. Cursors are opaque; do not build them.

{{ "data": [ … ], "meta": @{{ "per_page": 25, "next": "https://…?cursor=…" } }

{{ __('Idempotency') }}

Send an Idempotency-Key header (any unique string, up to 128 characters) on a write. A retry with the same key within 24 hours returns the stored response with Idempotent-Replayed: true instead of running again. The same key with a different body answers 422 idempotency_key_reused; a key whose first request is still running answers 409 idempotency_in_progress.

Errors

Every error, whatever the Accept header, is JSON in one shape. Branch on code; message is for people.

{"error": {"code": "validation_failed", "message": "…", "details": {"fields": {"body": ["…"]}}}}
StatusCodeMeaning
401unauthenticatedNo token, or a revoked or expired one.
403forbidden, insufficient_scope, api_access_not_entitledThe role, the token or the plan does not allow it.
404not_foundNo such resource, or one you are not a member of.
409*_already_decided, post_not_editable, …The resource is not in a state that allows this; details says which.
422validation_faileddetails.fields names each offending field. Never a redirect.
429rate_limitedRetry after the seconds in Retry-After.
502provider_rejectedThe social network refused the request; the message is what it said.

Rate limits

120 requests a minute per source address, before authentication; 300 a minute per token, after it. Both answer 429 with Retry-After and the usual X-RateLimit-* headers. One busy integration cannot starve another token on the same workspace.

Endpoints

Request and response bodies for each are in the OpenAPI document. Scheduled posts are addressed as {network}/{id} because each network keeps its own table.

Approvals

GET /api/v1/approvals List posts awaiting review approvals:read
POST /api/v1/approvals/{network}/{post}/approve Approve a post approvals:write
POST /api/v1/approvals/{network}/{post}/reject Reject a post approvals:write
POST /api/v1/approvals/{network}/{post}/request-changes Request changes on a post approvals:write

Brands

GET /api/v1/brands List brands brands:read
POST /api/v1/brands Create a brand brands:write
GET /api/v1/brands/{brand} Get a brand brands:read
PATCH /api/v1/brands/{brand} Update a brand brands:write

Ads

GET /api/v1/brands/{brand}/ads/accounts Bound ad accounts ads:read
GET /api/v1/brands/{brand}/ads/campaigns Campaigns on one network ads:read
GET /api/v1/brands/{brand}/ads/daily Daily paid metrics ads:read

Analytics

GET /api/v1/brands/{brand}/analytics Analytics summary analytics:read
GET /api/v1/brands/{brand}/analytics/daily Daily series per network analytics:read
GET /api/v1/brands/{brand}/analytics/posts Post performance table analytics:read

Campaigns

GET /api/v1/brands/{brand}/campaigns List content campaigns campaigns:read
GET /api/v1/brands/{brand}/campaigns/{campaign} Get a content campaign campaigns:read
PATCH /api/v1/brands/{brand}/campaigns/{campaign} Update a content campaign campaigns:write

Inbox

GET /api/v1/brands/{brand}/inbox/threads List inbox threads inbox:read
GET /api/v1/brands/{brand}/inbox/threads/{thread} Get a thread with its messages inbox:read
POST /api/v1/brands/{brand}/inbox/threads/{thread}/assign Assign a thread inbox:write
POST /api/v1/brands/{brand}/inbox/threads/{thread}/reply Reply to a thread inbox:write
POST /api/v1/brands/{brand}/inbox/threads/{thread}/resolve Resolve a thread inbox:write
POST /api/v1/brands/{brand}/inbox/threads/{thread}/unresolve Reopen a thread inbox:write

Media

GET /api/v1/brands/{brand}/media List media assets media:read
POST /api/v1/brands/{brand}/media Upload a media asset media:write
GET /api/v1/brands/{brand}/media/{asset} Get a media asset media:read

Reports

GET /api/v1/brands/{brand}/reports/share-links List report share links reports:read
POST /api/v1/brands/{brand}/reports/share-links Create a report share link reports:write
POST /api/v1/brands/{brand}/reports/share-links/{shareLink}/revoke Revoke a report share link reports:write

Scheduled posts

GET /api/v1/brands/{brand}/scheduled-posts List scheduled posts posts:read
POST /api/v1/brands/{brand}/scheduled-posts Schedule a post posts:write
DELETE /api/v1/brands/{brand}/scheduled-posts/{network}/{post} Cancel a scheduled post posts:write
GET /api/v1/brands/{brand}/scheduled-posts/{network}/{post} Get a scheduled post posts:read
PATCH /api/v1/brands/{brand}/scheduled-posts/{network}/{post} Edit a scheduled post posts:write
POST /api/v1/brands/{brand}/scheduled-posts/{network}/{post}/publish Publish a scheduled post now posts:write

SmartLinks

GET /api/v1/brands/{brand}/smartlinks List SmartLinks smartlinks:read
GET /api/v1/brands/{brand}/smartlinks/{smartLink} Get a SmartLink smartlinks:read
GET /api/v1/brands/{brand}/smartlinks/{smartLink}/stats SmartLink statistics smartlinks:read

Team

GET /api/v1/brands/{brand}/team Brand roster team:read

Webhooks

GET /api/v1/webhooks List webhook subscriptions webhooks:manage
POST /api/v1/webhooks Create a webhook subscription webhooks:manage
DELETE /api/v1/webhooks/{subscription} Delete a webhook subscription webhooks:manage
GET /api/v1/webhooks/{subscription} Get a webhook subscription webhooks:manage
PATCH /api/v1/webhooks/{subscription} Update a webhook subscription webhooks:manage
GET /api/v1/webhooks/{subscription}/deliveries List deliveries webhooks:manage
GET /api/v1/webhooks/{subscription}/deliveries/{delivery} Get a delivery webhooks:manage
POST /api/v1/webhooks/{subscription}/deliveries/{delivery}/redeliver Redeliver webhooks:manage

Webhooks

Register a public HTTPS endpoint with POST /api/v1/webhooks (workspace owners and admins; scope webhooks:manage). The signing secret is returned once. Every delivery is a JSON POST carrying id, event, sent_at, attempt and data, with these headers: X-Signature, X-Social-Event, X-Webhook-Id, X-Webhook-Attempt.

Events

post.scheduledA post was added to the schedule (any network).
post.publishedA scheduled post went live on its network.
post.failedA scheduled post could not be published; the payload carries the error.
post.cancelledA pending post was cancelled or deleted before it published.
approval.requestedA post entered the review queue.
approval.approvedA reviewer approved a post.
approval.rejectedA reviewer rejected a post; the payload carries the note.
approval.changes_requestedA reviewer or client asked for changes to a post.
inbox.thread.createdA new conversation, comment, mention or review arrived in the inbox.
inbox.thread.assignedAn inbox thread was assigned to (or unassigned from) a team member.
brand.createdA brand was created in the agency.
connection.needs_reconnectA connected network revoked or expired its authorisation and needs reconnecting.

Verifying a signature

X-Signature is t=<unix timestamp>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your secret>. Reject a delivery whose t is more than 300 seconds from your clock, then compare v1 in constant time. The legacy X-Social-Signature header (HMAC of the body alone) is still sent for one release; move off it.

<?php // PHP
function verify(string $header, string $body, string $secret, int $tolerance = 300): bool
{
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        [$k, $v] = array_pad(explode('=', trim($pair), 2), 2, null);
        if ($k !== null && $v !== null) { $parts[$k] = $v; }
    }
    if (! isset($parts['t'], $parts['v1']) || ! ctype_digit($parts['t'])) { return false; }
    if (abs(time() - (int) $parts['t']) > $tolerance) { return false; }
    $expected = hash_hmac('sha256', $parts['t'].'.'.$body, $secret);
    return hash_equals($expected, $parts['v1']);
}
// Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(header, rawBody, secret, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
  if (!/^\d+$/.test(parts.t ?? '') || !parts.v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) > tolerance) return false;
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Retries and the delivery ledger

Answer 2xx within ten seconds. Anything else is retried up to 5 times, waiting 1m, 5m, 15m, 1h between attempts. A delivery that exhausts its retries counts against the subscription; 5 in a row switch it off and email the workspace owners. Every attempt is on the ledger at GET /api/v1/webhooks/{id}/deliveries, and any delivery can be sent again with POST …/deliveries/{id}/redeliver.

MCP server

AI clients that speak the Model Context Protocol can point at https://social.iutils.net/mcp with the same bearer token. The tools mirror the API: list brands, analytics summary, scheduled posts, schedule a post, list and decide approvals, list and reply to inbox threads, list media and campaigns. The same scopes and brand permissions apply.

Changelog

2026-09-13
Scoped, expiring tokens. Cursor pagination on every list. One JSON error shape on every status. Idempotency keys. Per-token rate limit. New resources: brand create/update, scheduled post show/update/cancel/publish-now, media, approvals, inbox, campaigns, SmartLinks with statistics, per-network daily analytics and the post table, read-only paid media, report share links, team roster, webhook update/show, the delivery ledger and redelivery. Ten new webhook events and the timestamped X-Signature header. Webhook payloads now carry network, status and approval_status, and the network key uses the API spelling (linkedin, youtube, tiktok — previously linked_in, you_tube, tik_tok). OpenAPI 3.1 document. MCP tools for approvals, inbox, media and campaigns.
Earlier
Brands list, analytics summary, scheduled posts list and create, webhook subscriptions with post.published and post.failed signed with X-Social-Signature. All still work unchanged.