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 alsopending,publishing,needs_check,ready_to_post. - Errors are RFC 9457 problem details:
{"status": 409, "title": "…"}.titleis written for a person and safe to show.400bad input,401no or bad key,403the key's role can't do that here,404not visible to this key,409the post isn't in a state that allows it (the title says why),422Idempotency-Key reused for a different request,429slow 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"
}
connectionsis required.media,captions(per connection),options(per connection: TikTok'sprivacyand disclosure switches, YouTube'stitleandprivacy, Google'sbutton; see the OpenAPI schema),utmCampaignare 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 usesubmit: true. - Indably checks the post against each network's rules (lengths, media types, TikTok's questions) before it schedules. A problem comes back as
409with 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.