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.

FieldTypeNotes
contentstring, requiredThe base caption. Per-target overrides via targets[].content_variant.
mediaarray<{ url } | { media_id }>, optionalUp to 10 items. URLs are fetched at publish time. Video: send one MP4 URL.
targetsarray, required (1..20)Each entry: { platform, account_id, content_variant? }.
publish_atISO 8601 string, optionalOmit for immediate publish (max ~60s latency).
first_commentstring, optionalAuto-posted as the first comment on platforms that support it (Instagram, LinkedIn).
idempotency_keystring, optionalRetries 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.

  • linkedin
  • facebook
  • instagram
  • x
  • bluesky
  • threads
  • youtube
  • tiktok
  • pinterest
  • gmb
  • mastodon
  • wordpress

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": [...] } }
FieldTypeNotes
invalid_request400Body validation failed. See details.
unauthenticated401Missing or invalid Authorization header.
forbidden403This key's role cannot perform the action.
not_found404Referenced resource does not exist in this workspace.
conflict409Action not valid for the resource's current state.
account_not_connected422The account_id is not connected to this workspace.
unsupported_platform422That platform key is not supported.
rate_limited429Too many requests. Retry after Retry-After.
internal_error500Something 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.