Developers
REST API v1
Schedule and publish across every connected channel from your own code. Bearer-token auth, single-request multi-target publishing, hosted media via URL, signed webhooks for delivery status, and a sandbox environment for wiring up without touching real accounts. Included on every paid Unavoidably Social plan.
Quickstart
Get a live post out in three calls. Replace usw_live_YOUR_KEY with the token you copied from Workspace Settings → API keys.
# 1. List connected accounts to find the ids you want to target.
curl -s https://www.unavoidablysocial.com/api/v1/accounts \
-H "Authorization: Bearer usw_live_YOUR_KEY"
# 2. Create a scheduled post targeting one or more accounts at once.
curl -s -X POST https://www.unavoidablysocial.com/api/v1/posts \
-H "Authorization: Bearer usw_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Fresh drop, cotton canvas totes back in stock.",
"media": [{ "url": "https://cdn.example.com/tote.jpg" }],
"targets": [
{ "platform": "linkedin", "account_id": "…-uuid-from-step-1" },
{ "platform": "instagram", "account_id": "…-uuid-from-step-1" }
],
"publish_at": "2026-09-01T09:00:00+01:00"
}'
# 3. Register a webhook so your app knows when it lands (or fails).
curl -s -X POST https://www.unavoidablysocial.com/api/v1/webhooks \
-H "Authorization: Bearer usw_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/hooks/us", "events": ["post.published", "post.failed"] }'Authentication
Every request needs an Authorization: Bearer header. Tokens are workspace-scoped: one key acts as one member of the workspace, with a role that determines what it can do.
usw_live_...— real key. Publishes to live platforms.usw_test_...— sandbox key. Validates request shape and returns a plausible response, but does not hit any platform. Use it in CI and while wiring an integration up.
Keys are minted at Workspace Settings → API keys. You see the token exactly once; store it in your secret manager. Revoke at any time with a click, or programmatically via the settings API.
Base URL and versioning
Base URL: https://www.unavoidablysocial.com/api/v1. The version is baked into the path so future breaking changes ship at /api/v2 without rugging anyone using v1.
POST /v1/posts — schedule or publish
One request, one or more targets. Every target is one platform+account combination. Media is passed as an array of URL objects, so an upstream image generator can hand you a URL and you forward it in the same call — no separate upload step required.
| Field | Type | Notes |
|---|---|---|
| content | string, required | The base caption. Per-target overrides via targets[].content_variant. |
| media | array<{ url } | { media_id }>, optional | Up to 10 items. URLs are fetched at publish time. Video: send one MP4 URL. |
| targets | array, required (1..20) | Each entry: { platform, account_id, content_variant? }. |
| publish_at | ISO 8601 string, optional | Omit for immediate publish (max ~60s latency). |
| first_comment | string, optional | Auto-posted as the first comment on platforms that support it (Instagram, LinkedIn). |
| idempotency_key | string, optional | Retries within a short window return the original response instead of duplicating. |
{
"content": "Fresh drop, cotton canvas totes back in stock.",
"media": [
{ "url": "https://cdn.example.com/tote-1.jpg", "alt_text": "Beige tote bag on wood table" }
],
"targets": [
{ "platform": "linkedin", "account_id": "3b1b…-uuid" },
{ "platform": "facebook", "account_id": "42c8…-uuid" },
{ "platform": "instagram", "account_id": "9d7a…-uuid" },
{ "platform": "x", "account_id": "aa11…-uuid", "content_variant": "Fresh drop, cotton totes back. Link in bio." }
],
"publish_at": "2026-09-01T09:00:00+01:00",
"first_comment": "Free UK P&P over £40 this week."
}Response (201):
{
"post": {
"id": "6f2c…-uuid",
"status": "scheduled",
"source": "api",
"environment": "live",
"publish_at": "2026-09-01T08:00:00.000Z",
"created_at": "2026-08-19T14:23:12.000Z",
"targets": [
{ "platform": "linkedin", "account_id": "3b1b…", "status": "scheduled", "external_post_id": null, "post_url": null },
{ "platform": "facebook", "account_id": "42c8…", "status": "scheduled", "external_post_id": null, "post_url": null },
{ "platform": "instagram", "account_id": "9d7a…", "status": "scheduled", "external_post_id": null, "post_url": null },
{ "platform": "x", "account_id": "aa11…", "status": "scheduled", "external_post_id": null, "post_url": null }
]
}
}GET /v1/posts/:id — full status
Returns the current state of the post and one row per target with the platform-native post id and (where the platform gives it back) the canonical URL of the live post.
GET /v1/posts — list
Reverse-chronological. Query params: limit (1..100, default 50), status, before (ISO cursor from a previous next_cursor).
DELETE /v1/posts/:id — cancel a scheduled post
Works only while the post is still scheduled. Anything past that has already begun talking to platform APIs and cannot be rescinded here.
GET /v1/accounts — list connected channels
The discovery endpoint. Returns every channel connected to the workspace with its id, platform, display_name, and health status. Never returns tokens.
POST /v1/media — register a hosted URL
Optional. Pre-registers a URL as a media object and returns a media_id, useful for reusing the same image across many posts or for staging in tools like Make.com and Zapier. You can skip this entirely and pass URLs straight into POST /v1/posts.
Webhooks
Register an endpoint to receive delivery events. Every payload is signed so you can verify it came from us and not a spoofer.
Events
post.published— every target succeeded.post.failed— at least one target failed. The payload shows which and why.post.cancelled— you (or the UI) cancelled the post before it went out.
Payload
{
"id": "evt_42",
"type": "post.published",
"created": 1723972800,
"data": {
"post_id": "6f2c…-uuid",
"source": "api",
"content": "Fresh drop, cotton canvas totes back in stock.",
"targets": [
{ "platform": "linkedin", "account_id": "3b1b…", "external_post_id": "urn:li:share:…", "post_url": "https://linkedin.com/…", "failure_reason": null },
{ "platform": "facebook", "account_id": "42c8…", "external_post_id": "10231…", "post_url": "https://facebook.com/…", "failure_reason": null }
]
}
}Verifying the signature
Every request carries an X-Unavoidably-Signature header in the format t=<unix seconds>,v1=<hex>. Recompute HMAC-SHA256(secret, `${t}.${rawBody}`) and compare in constant time. Reject if |now - t| > 300s.
// Node.js example
import { createHmac, timingSafeEqual } from 'crypto';
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now()/1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
}Retries
We attempt delivery three times (immediate, +30 seconds, +5 minutes). After 20 consecutive failures on the same endpoint we auto-disable it and email the workspace owners. Return any 2xx to acknowledge.
Supported platforms
Pass one of these strings as targets[].platform. The matching account_id comes from GET /v1/accounts.
linkedinfacebookinstagramxblueskythreadsyoutubetiktokpinterestgmbmastodonwordpress
Rate limits
Per API key, sliding window. Exceed a limit and we return a 429 with Retry-After plus X-RateLimit-* headers.
POST /v1/posts: 30 per minute.- Everything else: 60 per minute.
Contact us if you need higher limits for a genuine workload.
Errors
Every failure returns the same envelope so callers can key on error.code.
{ "error": { "code": "invalid_request", "message": "targets: required", "details": [...] } }| Field | Type | Notes |
|---|---|---|
| invalid_request | 400 | Body validation failed. See details. |
| unauthenticated | 401 | Missing or invalid Authorization header. |
| forbidden | 403 | This key's role cannot perform the action. |
| not_found | 404 | Referenced resource does not exist in this workspace. |
| conflict | 409 | Action not valid for the resource's current state. |
| account_not_connected | 422 | The account_id is not connected to this workspace. |
| unsupported_platform | 422 | That platform key is not supported. |
| rate_limited | 429 | Too many requests. Retry after Retry-After. |
| internal_error | 500 | Something broke on our side. Retry with backoff. |
Pricing and access
API access is included on every paid plan at no extra charge. Free trial workspaces can mint usw_test_* keys and hit the sandbox as many times as they like; live keys become available the moment the workspace is on a paid plan.
Help
Something not covered here? support@unavoidablysocial.com — we read every mail. Include the API key prefix (never the full key) and a request id from the response header X-Request-Id if you have one.
Prefer the product tour? Back to the homepage.