indably

The Indably API and webhooks

Create, schedule and track social posts in Indably from your own systems, with idempotent writes and signed webhooks.

By Indably, . 5 minute read. Read as Markdown

For any system that posts through Indably. The machine-readable reference is at https://app.indably.com/openapi/v1.json (OpenAPI 3); this page is the guide.

Keys

An Owner makes a key under Settings → API and webhooks. A key:

  • belongs to the account and acts as the Owner who made it, in the brands (workspaces) that person belongs to;
  • is capped at Manager (write, approve, schedule) or Contributor (write and send for review), whichever was chosen; the approval workflow applies exactly as in the app;
  • is shown once, stored only as a SHA-256 fingerprint, and stops working when revoked or when its maker leaves the account.

Send it on every request:

Authorization: Bearer ind_k7Qm2x…

Without a valid key the API answers 401 with a problem-details body. It never accepts a browser's sign-in cookie. Limit: 120 requests a minute per key (429 above that).

Conventions

  • JSON in and out, camelCase. Times are UTC, ISO 8601 (2026-10-20T07:00:00Z).
  • Ids are GUIDs. Enum values are snake_case: draft, in_review, approved, scheduled, published, failed; targets also pending, publishing, needs_check, ready_to_post.
  • Errors are RFC 9457 problem details: {"status": 409, "title": "…"}. title is written for a person and safe to show.
    • 400 bad input, 401 no or bad key, 403 the key's role can't do that here, 404 not visible to this key, 409 the post isn't in a state that allows it (the title says why), 422 Idempotency-Key reused for a different request, 429 slow down.

Idempotency: never post twice

Every write accepts an Idempotency-Key header (any string up to 100 characters, unique per intended action, e.g. your order id plus “-post”). If Indably's answer is lost and you send the same request with the same key, you get the first answer back (with Idempotent-Replayed: true) instead of a second post. The same key with a different body is refused (422); a retry while the first is still running gets 409, so wait a second and try again. Keys are remembered for seven days.

Always send one on POST /posts. It is the API's version of Indably's own rule: never retry an unknown outcome blind.

Endpoints

Method and path What it does
GET /api/v1/me The key, its account, its role cap, and the brands it can reach
GET /api/v1/workspaces The brands (id, name, time zone, whether approval is required, the key's role there)
GET /api/v1/workspaces/{id}/connections The accounts a brand can publish to; use their ids in a post's connections
POST /api/v1/workspaces/{id}/media Upload a photo or video (multipart field file, optional altText). Photos are re-encoded to JPEG with location data removed
POST /api/v1/workspaces/{id}/posts Create a post (below)
GET /api/v1/workspaces/{id}/posts?status=&from=&to=&limit= Posts, newest first; from/to filter on the scheduled time
GET /api/v1/workspaces/{id}/posts/{postId} One post with each target's status, link, error and latest numbers
POST …/posts/{postId}/submit Send a draft for review (or approve it, if the key may)
POST …/posts/{postId}/approve Approve a post in review (Manager keys)
POST …/posts/{postId}/send-back {"note": "…"} Return it to the author with a note (Manager keys)
POST …/posts/{postId}/schedule {"at": "…"} or {"queue": true} for the brand's next free posting time
POST …/posts/{postId}/unschedule Back to approved, if publishing hasn't started
DELETE …/posts/{postId} Delete a draft or unscheduled post (published posts stay on record)
GET /api/v1/workspaces/{id}/report?from=2026-10-01&to=2026-10-31 Totals and every published post's numbers for dates in the brand's time zone

Create a post

POST /api/v1/workspaces/0199f4a2-…/posts
Authorization: Bearer ind_…
Idempotency-Key: heypaid-order-1042-post
Content-Type: application/json

{
  "caption": "Payday special: 20% off cupping sessions this Saturday.",
  "link": "https://harbourroast.co.za/cupping",
  "firstComment": "#durban #coffee",
  "connections": ["0199f4a3-…", "0199f4a3-…"],
  "media": ["0199f4b0-…"],
  "captions": { "0199f4a3-…": "A shorter version for X." },
  "scheduleAt": "2026-10-24T07:00:00Z"
}
  • connections is required. media, captions (per connection), options (per connection: TikTok's privacy and disclosure switches, YouTube's title and privacy, Google's button; see the OpenAPI schema), utmCampaign are optional.
  • What happens next depends on one of: scheduleAt (schedule at that time), queue: true (the next free posting time), submit: true (send for review). With none, it stays a draft.
  • Scheduling needs a Manager key, or a brand with approval switched off; otherwise 403, and you should use submit: true.
  • Indably checks the post against each network's rules (lengths, media types, TikTok's questions) before it schedules. A problem comes back as 409 with the reason.

Answer: 201 with the post. Poll GET …/posts/{postId} or, better, listen to webhooks.

Webhooks

An Owner adds an https address under Settings → API and webhooks, with the events it wants. Indably then POSTs JSON to it:

{
  "id": "0199f5c1-…",
  "type": "post.target.published",
  "createdAt": "2026-10-24T07:00:03Z",
  "workspace": { "id": "0199f4a2-…", "name": "Harbour Roast" },
  "data": { "post": { …the post as the API returns it… }, "target": { …the one network it's about… } }
}
Event When
post.submitted Sent for review
post.approved Approved in Indably, on WhatsApp, or by a Manager's own save
post.sent_back Returned to the author with a note
post.scheduled Given a time
post.target.published Live on one network (also when Indably confirmed a post it wasn't sure about, or a person marked an assisted post as posted)
post.target.failed The network refused it; target.error says why
post.target.needs_check The network didn't answer after the post was sent; Indably will look, and a person may need to
post.target.ready_to_post An assisted post is ready for a person to post by hand
ping “Send a test” in settings

Headers: Indably-Event, Indably-Delivery (the event id), Indably-Signature: t=1760860800,v1=<hex>.

Verify every delivery: compute HMAC-SHA256 of t + "." + raw body with your signing secret and compare with v1 in constant time; reject anything older than five minutes.

static bool Verify(string secret, string header, string body)
{
    var parts = header.Split(',').Select(p => p.Split('=', 2)).ToDictionary(p => p[0], p => p[1]);
    if (!long.TryParse(parts["t"], out var t) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;
    using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToHexStringLower(h.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{body}")));
    return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(parts["v1"]));
}

Delivery is at least once. Answer 2xx within 10 seconds; anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then given up. A slow answer can mean the same event arrives twice: ignore an Indably-Delivery id you've already handled. After 20 failures in a row the webhook is switched off and the account's Owners are told. Indably only calls public https addresses: never private networks, never redirects.

What isn't in version 1

Ads, analytics beyond the report, connecting accounts (people do that in the app, because the networks' sign-ins need a person), and member management.

Next week, planned by Friday.

Every feature switched on from the first day. No card until you decide to stay.