Developer API
Schedule and publish content to eight social platforms, upload media, edit and delete posts, and read analytics — from your own scripts, agents or CI.
JSON over HTTPS, authenticated with a workspace API key. Base URL:
https://social-api.instantdm.com
14 endpoints across accounts, media, posts and analytics. Create and manage keys on the Developer page in your dashboard, then read Posts & targets to understand the model.
Building an AI agent? Grab the whole reference as Markdown: /docs/api.md.
# 1. list the accounts you can post to
curl "https://social-api.instantdm.com/v1/accounts" \
-H "X-Api-Key: $SOCIAL_API_KEY"
# 2. schedule a text post
curl -X POST "https://social-api.instantdm.com/v1/posts" \
-H "X-Api-Key: $SOCIAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"accountIds":["acc_8fK2qz"],
"content":"Hello from the API 👋",
"scheduledAt":"2026-07-01T09:00:00Z"}' Authentication
Every request (except the OpenAPI spec) carries a workspace API key — in the X-Api-Key header, or as a Bearer token. Keys are workspace-scoped, shown once at creation, and stored hashed. Never ship a key in client-side code.
curl "https://social-api.instantdm.com/v1/accounts" \
-H "X-Api-Key: sk_live_xxxxxxxxxxxx"
# or, equivalently
curl "https://social-api.instantdm.com/v1/accounts" \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxx" Scopes
Each key is granted a set of scopes at creation. A call to an endpoint whose scope the key lacks returns 403 forbidden. Grant the least you need.
accounts:read | List connected accounts |
posts:read | List and read posts |
posts:write | Create, edit, delete posts |
media:read | List media assets |
media:write | Upload and delete media |
analytics:read | Read the analytics summary |
platform:proxy | Call a platform’s API directly via the proxy (opt-in, advanced) |
{
"error": {
"message": "API key missing scope: posts:write",
"code": "forbidden"
}
} Rate limits
Each key is limited to 120 requests per minute. Over the limit returns 429 rate_limited; it resets at the top of the next minute.
{
"error": {
"message": "Rate limit exceeded",
"code": "rate_limited"
}
} Errors
Errors use standard HTTP status codes with a consistent JSON body — a human message and a stable machine code.
400 | bad_request | Required fields are missing or malformed. |
401 | unauthorized | Missing or invalid API key. |
403 | forbidden | Key is valid but lacks the required scope. |
404 | not_found | The post, media, or referenced account does not exist. |
409 | not_editable | The post is in a state that cannot be edited (e.g. already publishing). |
409 | publishing | DELETE on a post that is mid-publish — retry once it settles. |
422 | post_type_invalid | postType does not match the attached media (e.g. carousel with one image). |
422 | content_invalid | Caption exceeds a platform limit, or a platform requires media you did not attach. |
429 | rate_limited | More than 120 requests in one minute for this key. |
{
"error": {
"message": "A carousel needs at least two media items",
"code": "post_type_invalid"
}
} Posts & targets
A post is created once and fans out to one target per account you list in accountIds. The same caption and media go to every target, unless you override per platform with platformContent / platformSchedules.
The post carries an overall status; each target carries its own status, the platformPostId once live, and any error. A post where some targets succeed and others fail is partial — call GET /v1/posts/{id} to see the per-target breakdown.
Statuses
draft | Saved but not scheduled. Activate by editing (PUT) with a scheduledAt or publishNow. |
scheduled | Queued for a future time. |
publishing | Handed to the publish worker right now. |
published | Live on every target platform. |
partial | Live on some targets, failed on others — inspect per-target status. |
failed | Every target failed. The reason is on each target row. |
canceled | Deleted/canceled before or after going out. |
publishNow: true → publish immediately
scheduledAt ≤ now + 30s → publish immediately
scheduledAt in future → scheduled (exact time)
draft: true → saved, not scheduled
platformSchedules lets each
platform fire at its own time:
{
"scheduledAt": "2026-07-01T09:00:00Z",
"platformSchedules": {
"twitter": "2026-07-01T12:00:00Z"
}
} Post types
Set postType on a post (or let it auto-detect from the media). Whether a given type actually publishes depends on the target platform — see Platform support.
| postType | Media | Notes |
|---|---|---|
text | none | Text-only update. The default when no media is attached. |
image | 1 | A single image. The default when media is attached. |
carousel | 2–10 | Multi-image. Pinterest caps at 5 and requires a shared aspect ratio. |
video | 1 video | A standard video post. |
reel | 1 video | Short-form vertical video. Supports coverMediaId / coverOffsetMs for the thumbnail. |
story | 1 | Story format, where the platform API allows it. |
thread | per segment | A chain of posts (X / Threads). Use the thread[] array. |
article | none | A link share with a rich preview (LinkedIn, Facebook). Use the article object. |
document | 1 PDF | A swipeable PDF document post (LinkedIn only). Use documentTitle for the card title. |
Platform support
What each platform accepts. Posts are validated against these limits at create time — exceed one and you get a content_invalid / post_type_invalid error before anything is scheduled. Some platform-specific destinations (a Facebook Page, a Pinterest board) are chosen once on the Accounts page in the dashboard.
| Platform | Post types | Max text | Max media | Threads | Edit | Delete | Notes |
|---|---|---|---|---|---|---|---|
| X / Twitter | text, image, video, thread | 25,000 * | 4 per tweet | Yes | No | Yes | * Free tier is capped at 280; longer text needs Premium. Threads post as a reply chain. |
| image, video, carousel, reel, story | 2,200 | 10 (carousel) | No | No | Yes * | Media required. Reels support a custom cover. Stories are limited by the platform API. * Delete needs the instagram_manage_contents permission — accounts connected before it was added must reconnect. | |
| text, image, video, carousel, reel, article | 63,206 | 10 (carousel) | No | Yes | Yes | Publishes to a selected Page (set on the Accounts page). | |
| text, image, video, article, document | 3,000 | 1 | No | Yes | No * | One media per post. article = rich link share; document = swipeable PDF. * Personal-profile posts can NOT be deleted via the API (LinkedIn limitation — delete in the app); only Company Page posts are API-deletable. | |
| TikTok | video, carousel (photos) | 2,200 | 1 video / 35 photos | No | No | No | Photos must be JPEG ≤1920px and <10MB. Video must be on a verified public domain. |
| Threads | text, image, video, thread | 500 | 1 per segment | Yes | No | No | Strictest text limit. One image per thread segment. |
| image, carousel | 800 | 5 (carousel) | No | No | Yes | Image only. Requires a board (set on Accounts). Carousel images share an aspect ratio. Supports a destination link. | |
| YouTube | video | 5,000 | 1 video | No | No | No | Video only. Privacy is set with ytPrivacy (public / unlisted / private). |
Uploading media
Media uploads in three steps so large files never pass through the API:
- Request a URL —
POST /v1/media/upload-urlreturns a presigneduploadUrland amediaId. - PUT the bytes — upload the file directly to
uploadUrlwith the sameContent-Type. This goes straight to storage, not the API. The URL is valid for 15 minutes. - Mark complete —
POST /v1/media/{id}/complete. ThemediaIdis now attachable viamediaIdson a post.
Per-platform media rules (formats, sizes, counts) are in Platform support — e.g. TikTok photos must be JPEG ≤1920px and <10MB.
# 1. Ask for a presigned upload URL
RESP=$(curl -s -X POST "https://social-api.instantdm.com/v1/media/upload-url" \
-H "X-Api-Key: $SOCIAL_API_KEY" -H "Content-Type: application/json" \
-d '{"filename":"sunset.jpg","contentType":"image/jpeg"}')
MEDIA_ID=$(echo "$RESP" | jq -r .mediaId)
URL=$(echo "$RESP" | jq -r .uploadUrl)
# 2. PUT the bytes straight to storage (not through the API)
curl -X PUT "$URL" -H "Content-Type: image/jpeg" --data-binary @sunset.jpg
# 3. Mark it complete — now usable as a mediaId on a post
curl -X POST "https://social-api.instantdm.com/v1/media/$MEDIA_ID/complete" \
-H "X-Api-Key: $SOCIAL_API_KEY" -H "Content-Type: application/json" \
-d '{}' import requests
API = "https://social-api.instantdm.com"; KEY = "sk_..."
h = {"X-Api-Key": KEY}
# 1. presigned URL
r = requests.post(f"{API}/v1/media/upload-url", headers=h,
json={"filename": "sunset.jpg", "contentType": "image/jpeg"}).json()
# 2. PUT bytes directly to storage
with open("sunset.jpg", "rb") as f:
requests.put(r["uploadUrl"], data=f, headers={"Content-Type": "image/jpeg"})
# 3. mark complete -> r["mediaId"] is now attachable
requests.post(f"{API}/v1/media/{r['mediaId']}/complete", headers=h, json={})
print(r["mediaId"]) const API = "https://social-api.instantdm.com", KEY = process.env.SOCIAL_API_KEY;
const h = { "X-Api-Key": KEY, "Content-Type": "application/json" };
// 1. presigned URL
const r = await (await fetch(`${API}/v1/media/upload-url`, {
method: "POST", headers: h,
body: JSON.stringify({ filename: "sunset.jpg", contentType: "image/jpeg" }),
})).json();
// 2. PUT bytes directly to storage
await fetch(r.uploadUrl, { method: "PUT", headers: { "Content-Type": "image/jpeg" }, body: fileBlob });
// 3. mark complete -> r.mediaId is now attachable
await fetch(`${API}/v1/media/${r.mediaId}/complete`, { method: "POST", headers: h, body: "{}" });
console.log(r.mediaId); /v1/accounts List connected accounts
Every social account connected to your workspace. Use the accountId values as targets when creating a post.
curl -X GET "https://social-api.instantdm.com/v1/accounts" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.get(
"https://social-api.instantdm.com/v1/accounts",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/accounts", {
method: "GET",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"accounts": [
{
"accountId": "acc_8fK2qz",
"platform": "instagram",
"handle": "@acme",
"displayName": "Acme Inc"
},
{
"accountId": "acc_p1L9wd",
"platform": "linkedin",
"handle": "acme-inc",
"displayName": "Acme Inc"
}
]
} /v1/accounts/{id}/posts List native platform posts
Fetch an account’s own posts pulled LIVE from the platform — including posts not created through this API. Each post is flagged inDb (whether we also track it) + dbPostId. On Instagram, Facebook, X and Pinterest each post carries mediaUrl — the downloadable media file (the video URL for reels/videos, the image URL for photos) — plus thumbnail (cover), permalink, and children[] (per-item media) for carousels (Instagram also returns mediaProductType, e.g. "reels"). That makes repurposing one call away: take mediaUrl → upload it via POST /v1/media/upload-url (or MCP upload_media_from_url) → create a new post. TikTok and YouTube return mediaUrl=null — their APIs expose only a cover/thumbnail, not a downloadable file. Paginated: pass the returned nextCursor back as ?cursor=. Supported on Instagram, X, Facebook, TikTok, Pinterest, YouTube. NOT supported on LinkedIn personal profiles or Threads (their APIs have no post-listing endpoint → 422).
Parameters
limit | integer · query | Page size, 1–100 (default 25). |
cursor | string · query | Opaque pagination cursor from the previous response’s nextCursor. Omit for the first page. |
Errors
unsupported— The platform can’t list posts via API (LinkedIn-personal, Threads).not_found— The account is not in your workspace.platform_error— The platform API call failed (e.g. token expired / missing scope).
curl -X GET "https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.get(
"https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts", {
method: "GET",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"accountId": "acc_8fK2qz",
"platform": "instagram",
"posts": [
{
"platformPostId": "17912…",
"caption": "Golden hour 🌅",
"mediaType": "video",
"mediaProductType": "reels",
"mediaUrl": "https://…cdn…/reel.mp4",
"permalink": "https://www.instagram.com/reel/…",
"thumbnail": "https://…cover.jpg",
"timestamp": "2026-06-20T18:00:00+0000",
"metrics": {
"likes": 120,
"comments": 8,
"shares": 0,
"impressions": 0
},
"inDb": true,
"dbPostId": "pst_3kqz1a"
}
],
"nextCursor": "QVFI…"
} /v1/accounts/{id}/posts/{platformPostId} Delete a native platform post
Delete a post directly from the platform by its platformPostId — including posts NOT created through this API (use List native platform posts to find the platformPostId). If we also track the post, our record is canceled too. Always check the deleted flag, not just the HTTP status. Deletable: Instagram, Facebook, X/Twitter, Pinterest. Not deletable via API (returns deleted: false with a reason): LinkedIn personal profiles, TikTok, Threads, YouTube.
Errors
not_found— The account is not in your workspace.platform_error— The platform API call threw (e.g. token expired / missing scope).
curl -X DELETE "https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts/{platformPostId}" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.delete(
"https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts/{platformPostId}",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts/{platformPostId}", {
method: "DELETE",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"accountId": "acc_8fK2qz",
"platform": "instagram",
"platformPostId": "17912…",
"deleted": true,
"dbPostId": null
} /v1/media/upload-url Create an upload URL
Step 1 of the upload flow. Returns a presigned URL you PUT the file bytes to, plus the mediaId you will attach to a post.
Body parameters
filename | stringrequired | Original file name, e.g. "sunset.jpg". Required. |
contentType | stringrequired | MIME type, e.g. "image/jpeg" or "video/mp4". Required. |
Errors
bad_request— filename or contentType is missing.
curl -X POST "https://social-api.instantdm.com/v1/media/upload-url" \
-H "X-Api-Key: $SOCIAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"sunset.jpg","contentType":"image/jpeg"}' import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.post(
"https://social-api.instantdm.com/v1/media/upload-url",
headers={"X-Api-Key": API_KEY},
json={"filename":"sunset.jpg","contentType":"image/jpeg"},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/media/upload-url", {
method: "POST",
headers: {
"X-Api-Key": process.env.SOCIAL_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({"filename":"sunset.jpg","contentType":"image/jpeg"}),
});
const data = await resp.json();
console.log(data); {
"mediaId": "md_1a2b3c",
"uploadUrl": "https://…r2…/social-by-idm/ws_…/md_1a2b3c/sunset.jpg?X-Amz-Signature=…",
"key": "social-by-idm/ws_…/md_1a2b3c/sunset.jpg"
} /v1/media/{id}/complete Mark an upload complete
Step 3 of the upload flow. Call this after the PUT succeeds to mark the asset uploaded and ready to attach.
Body parameters
size | integer | Uploaded size in bytes (optional, for your records). |
curl -X POST "https://social-api.instantdm.com/v1/media/md_1a2b3c/complete" \
-H "X-Api-Key: $SOCIAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"size":240128}' import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.post(
"https://social-api.instantdm.com/v1/media/md_1a2b3c/complete",
headers={"X-Api-Key": API_KEY},
json={"size":240128},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/media/md_1a2b3c/complete", {
method: "POST",
headers: {
"X-Api-Key": process.env.SOCIAL_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({"size":240128}),
});
const data = await resp.json();
console.log(data); {
"mediaId": "md_1a2b3c",
"status": "uploaded"
} /v1/media List media
Every media asset in the workspace, each with a previewUrl.
curl -X GET "https://social-api.instantdm.com/v1/media" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.get(
"https://social-api.instantdm.com/v1/media",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/media", {
method: "GET",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"media": [
{
"mediaId": "md_1a2b3c",
"filename": "sunset.jpg",
"kind": "image",
"status": "uploaded",
"previewUrl": "https://…"
}
]
} /v1/media/{id} Delete media
Remove a media asset from storage and the library.
curl -X DELETE "https://social-api.instantdm.com/v1/media/md_1a2b3c" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.delete(
"https://social-api.instantdm.com/v1/media/md_1a2b3c",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/media/md_1a2b3c", {
method: "DELETE",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"mediaId": "md_1a2b3c",
"deleted": true
} /v1/posts Create / schedule a post
Create a post and publish it now or at a future time. One call fans out to one target per account. See Post recipes for every post type. Each target is validated against its platform’s limits before anything is scheduled. Returns the full post object plus a targets array.
Body parameters
accountIds | string[]required | Connected account IDs to publish to. Each must belong to your workspace. Fetch them from GET /v1/accounts. |
content | string | Caption / text. Required unless mediaIds (or a thread) is provided. |
mediaIds | string[] | IDs of uploaded media to attach (see Uploading media). |
postType | string | One of text, image, carousel, video, reel, story, thread, article, document. Auto-detected from media if omitted. |
scheduledAt | string · ISO 8601 | When to publish. With a timezone offset (e.g. 2026-07-01T11:30:00+05:30) it is exact; WITHOUT an offset it is interpreted in the workspace timezone (Settings → Timezone, default UTC) — not assumed UTC. A past / near-now time (≤30s) publishes immediately. Required unless publishNow or draft. |
publishNow | boolean | Publish immediately, ignoring scheduledAt. |
draft | boolean | Save without scheduling. Activate later with PUT (set scheduledAt or publishNow). |
platformContent | object | Per-platform caption overrides, e.g. { "instagram": "...", "linkedin": "..." }. Falls back to content. |
platformSchedules | object | Per-platform publish times, e.g. { "twitter": "2026-07-01T12:00:00+05:30" }. Same timezone rule as scheduledAt (no offset = workspace timezone). Targets not listed use scheduledAt. |
thread | object[] | For postType "thread" — an array of { text, mediaIds? } segments posted as a reply chain (X, Threads). |
article | object | For postType "article" — { url, title?, description? } rich link share (LinkedIn, Facebook). |
link | string | Destination URL for the post (e.g. the Pinterest pin link). |
coverMediaId | string | Media ID to use as the video/reel cover thumbnail. |
coverOffsetMs | integer | Frame offset (ms) for an auto-generated video cover, when coverMediaId is not set. |
documentTitle | string | For postType "document" (LinkedIn) — the title shown on the PDF document card. Falls back to the caption. |
youtubeTitle | string | YouTube video title (≤100 chars). If omitted, the first line of content is used. The caption (content) becomes the video description. |
Errors
bad_request— accountIds is empty, or neither content/mediaIds/thread was provided, or no scheduledAt/publishNow.post_type_invalid— postType does not match the media (e.g. carousel with <2 items).content_invalid— A caption exceeds a platform limit, or a platform requires media you didn’t attach.not_found— An account in accountIds is not in your workspace.
curl -X POST "https://social-api.instantdm.com/v1/posts" \
-H "X-Api-Key: $SOCIAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"accountIds":["acc_8fK2qz"],"postType":"image","content":"we shipped 🚀","mediaIds":["md_1a2b3c"],"scheduledAt":"2026-07-01T09:00:00Z"}' import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.post(
"https://social-api.instantdm.com/v1/posts",
headers={"X-Api-Key": API_KEY},
json={"accountIds":["acc_8fK2qz"],"postType":"image","content":"we shipped 🚀","mediaIds":["md_1a2b3c"],"scheduledAt":"2026-07-01T09:00:00Z"},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/posts", {
method: "POST",
headers: {
"X-Api-Key": process.env.SOCIAL_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({"accountIds":["acc_8fK2qz"],"postType":"image","content":"we shipped 🚀","mediaIds":["md_1a2b3c"],"scheduledAt":"2026-07-01T09:00:00Z"}),
});
const data = await resp.json();
console.log(data); {
"postId": "pst_3kqz1a",
"status": "scheduled",
"scheduledAt": "2026-07-01T09:00:00+00:00",
"immediate": false,
"post": {
"postId": "pst_3kqz1a",
"content": "we shipped 🚀",
"postType": "image",
"status": "scheduled",
"platforms": [
"instagram"
],
"mediaIds": [
"md_1a2b3c"
]
},
"targets": [
{
"accountId": "acc_8fK2qz",
"platform": "instagram",
"status": "scheduled"
}
]
} /v1/posts List posts
Posts in the workspace, ordered by scheduled time, across every status.
curl -X GET "https://social-api.instantdm.com/v1/posts" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.get(
"https://social-api.instantdm.com/v1/posts",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/posts", {
method: "GET",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"posts": [
{
"postId": "pst_3kqz1a",
"content": "we shipped 🚀",
"postType": "image",
"status": "scheduled",
"scheduledAt": "2026-07-01T09:00:00+00:00",
"platforms": [
"instagram"
],
"accountIds": [
"acc_8fK2qz"
],
"mediaIds": [
"md_1a2b3c"
]
}
]
} /v1/posts/{id} Get a post
A single post plus its per-target status — the platform post ID, live status, and any error, per account. Canceled/deleted posts are still returned (with that status) rather than 404, so you can audit history.
curl -X GET "https://social-api.instantdm.com/v1/posts/pst_3kqz1a" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.get(
"https://social-api.instantdm.com/v1/posts/pst_3kqz1a",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/posts/pst_3kqz1a", {
method: "GET",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"post": {
"postId": "pst_3kqz1a",
"content": "we shipped 🚀",
"postType": "image",
"status": "published",
"platforms": [
"instagram"
],
"mediaIds": [
"md_1a2b3c"
]
},
"targets": [
{
"accountId": "acc_8fK2qz",
"platform": "instagram",
"status": "published",
"platformPostId": "17912…",
"scheduledAt": "2026-07-01T09:00:00+00:00"
}
]
} /v1/posts/{id}/status Get publish status
A lightweight, poll-friendly status for a post — just the publish state, no full payload. Use it after publishNow to know when a post is live. While a post is being delivered (e.g. an Instagram carousel assembling its media), status is "publishing" and ready is false; once it goes live, status is "published" and ready is true. Per account you also get the platformPostId, permalink, and any error. Other states: scheduled (waiting for its time), partial (some accounts failed), failed.
curl -X GET "https://social-api.instantdm.com/v1/posts/pst_3kqz1a/status" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.get(
"https://social-api.instantdm.com/v1/posts/pst_3kqz1a/status",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/posts/pst_3kqz1a/status", {
method: "GET",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"postId": "pst_3kqz1a",
"status": "publishing",
"ready": false,
"scheduledAt": "2026-07-01T09:00:00+00:00",
"targets": [
{
"accountId": "acc_8fK2qz",
"platform": "instagram",
"status": "publishing",
"ready": false,
"platformPostId": null,
"permalink": null,
"error": null
}
]
} /v1/posts/{id} Edit a post
Edit a scheduled post (any field) or reschedule it. Activate a draft by passing scheduledAt or publishNow (returns activated: true). For an already-published post, only the caption can change, and only on platforms that allow it (Facebook, LinkedIn) — others report back unsupported. Returns the full updated post + targets. PATCH is an alias.
Body parameters
content / postType / mediaIds | mixed | Replace any of these on a scheduled post. On a published post, only content (caption) can change, and only on platforms that support editing (Facebook, LinkedIn). |
platformContent / thread / article / link / coverMediaId / documentTitle | mixed | Replace, or pass an empty value to clear the field. |
scheduledAt | string · ISO 8601 | Reschedule a scheduled post, or (on a draft) activate it. Rebuilds the underlying schedule. A bare scheduledAt change moves ALL targets (and clears any per-platform times). Timezone rule as above: no offset = workspace timezone. |
platformSchedules | object | Reschedule each platform independently, e.g. { "instagram": "2026-07-01T09:00:00+05:30" }. Platforms omitted use scheduledAt. Pass {} to clear and move everything to scheduledAt. Same timezone rule as scheduledAt. |
accountIds | string[] | Retarget to a different set of accounts (recreates the per-platform targets). |
publishNow | boolean | Publish the edited post immediately (also activates a draft). |
Errors
not_editable— The post is not in a draft, scheduled, or published state.not_found— The post (or a new account) does not exist in your workspace.
curl -X PUT "https://social-api.instantdm.com/v1/posts/pst_3kqz1a" \
-H "X-Api-Key: $SOCIAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content":"we shipped — now with screenshots","scheduledAt":"2026-07-02T10:00:00Z"}' import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.put(
"https://social-api.instantdm.com/v1/posts/pst_3kqz1a",
headers={"X-Api-Key": API_KEY},
json={"content":"we shipped — now with screenshots","scheduledAt":"2026-07-02T10:00:00Z"},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/posts/pst_3kqz1a", {
method: "PUT",
headers: {
"X-Api-Key": process.env.SOCIAL_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({"content":"we shipped — now with screenshots","scheduledAt":"2026-07-02T10:00:00Z"}),
});
const data = await resp.json();
console.log(data); {
"postId": "pst_3kqz1a",
"updated": true,
"rescheduled": true,
"post": {
"postId": "pst_3kqz1a",
"content": "we shipped — now with screenshots",
"status": "scheduled",
"scheduledAt": "2026-07-02T10:00:00+00:00"
},
"targets": [
{
"accountId": "acc_8fK2qz",
"platform": "instagram",
"status": "scheduled"
}
]
} /v1/posts/{id} Delete a post
Cancel a scheduled post, or delete a live one. For published posts we delete from each platform that supports it; the rest are returned under "manual" for you to remove by hand. A post that is mid-publish returns 409 — retry once it settles.
Errors
publishing— The post is currently being published — retry in a few seconds.not_found— The post does not exist in your workspace.
curl -X DELETE "https://social-api.instantdm.com/v1/posts/pst_3kqz1a" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.delete(
"https://social-api.instantdm.com/v1/posts/pst_3kqz1a",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/posts/pst_3kqz1a", {
method: "DELETE",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"postId": "pst_3kqz1a",
"status": "canceled",
"removedFrom": [
"facebook"
],
"manual": [
{
"platform": "instagram",
"reason": "platform has no delete API"
}
]
} /v1/analytics Analytics summary
Post counts by status plus total engagement across every published post.
curl -X GET "https://social-api.instantdm.com/v1/analytics" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.get(
"https://social-api.instantdm.com/v1/analytics",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/analytics", {
method: "GET",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); {
"totalPosts": 128,
"byStatus": {
"scheduled": 12,
"published": 110,
"failed": 6
},
"engagement": {
"likes": 4210,
"comments": 318,
"shares": 96,
"impressions": 51200
}
} /v1/proxy/{accountId}/{path} Platform API proxy
Call a connected account’s NATIVE platform API directly through us, with ANY method (GET/POST/PUT/PATCH/DELETE). You supply the relative platform path (incl. its API version), query, headers and body; we inject the account’s OAuth token (and any platform-specific auth, e.g. Meta’s appsecret_proof) and forward the request to the platform’s own host. The platform’s exact status code + body come straight back. Use it to reach endpoints we don’t wrap yet. Security: SSRF-locked to the platform host (relative paths only), your Authorization/Host/Cookie headers are dropped (we set auth), redirects are not followed, and the token is never returned. Supported platforms: Instagram, Facebook, Threads, LinkedIn, Pinterest, YouTube, TikTok. X/Twitter is currently disabled. Requires the platform:proxy scope (opt-in). Example: GET /v1/proxy/acc_8fK2qz/me?fields=id,username → calls graph.instagram.com/me with the token attached.
Parameters
accountId | string · pathrequired | A connected account in your workspace — its platform determines which API is called. |
{path} | string · pathrequired | The relative platform API path (no host), including the API version where applicable, e.g. "me/media?fields=id,caption,media_url" or "v2/me". |
curl -X ANY "https://social-api.instantdm.com/v1/proxy/{accountId}/{path}" \
-H "X-Api-Key: $SOCIAL_API_KEY" import requests
API_KEY = "sk_..." # create one in the dashboard
resp = requests.any(
"https://social-api.instantdm.com/v1/proxy/{accountId}/{path}",
headers={"X-Api-Key": API_KEY},
)
resp.raise_for_status()
print(resp.json()) const resp = await fetch("https://social-api.instantdm.com/v1/proxy/{accountId}/{path}", {
method: "ANY",
headers: { "X-Api-Key": process.env.SOCIAL_API_KEY },
});
const data = await resp.json();
console.log(data); Post recipes
Request bodies for POST /v1/posts, one per scenario. Send them with the same headers as any create call (X-Api-Key + Content-Type: application/json).
{
"accountIds": [
"acc_x"
],
"content": "Hello world 👋"
} {
"accountIds": [
"acc_ig"
],
"postType": "image",
"content": "Golden hour 🌅",
"mediaIds": [
"md_1a2b"
]
} {
"accountIds": [
"acc_ig"
],
"postType": "carousel",
"content": "Swipe →",
"mediaIds": [
"md_1",
"md_2",
"md_3"
]
} {
"accountIds": [
"acc_ig"
],
"postType": "reel",
"content": "New drop",
"mediaIds": [
"md_video"
],
"coverMediaId": "md_cover"
} {
"accountIds": [
"acc_x"
],
"postType": "thread",
"thread": [
{
"text": "1/ Kicking off a thread 🧵"
},
{
"text": "2/ With media on this one",
"mediaIds": [
"md_1"
]
},
{
"text": "3/ Wrapping up."
}
]
} {
"accountIds": [
"acc_ig",
"acc_li"
],
"content": "Default caption",
"platformContent": {
"instagram": "Casual IG caption ✨",
"linkedin": "A more professional take for LinkedIn."
}
} {
"accountIds": [
"acc_ig",
"acc_x"
],
"content": "Cross-post",
"scheduledAt": "2026-07-01T09:00:00Z",
"platformSchedules": {
"twitter": "2026-07-01T12:00:00Z"
}
} {
"accountIds": [
"acc_li"
],
"postType": "article",
"content": "Our latest write-up",
"article": {
"url": "https://blog.example.com/post",
"title": "How we shipped it",
"description": "A short snippet."
}
} {
"accountIds": [
"acc_li"
],
"postType": "document",
"content": "Swipe through our deck 📄",
"documentTitle": "Q3 Product Update",
"mediaIds": [
"md_pdf"
]
} {
"accountIds": [
"acc_pin"
],
"postType": "image",
"content": "Weeknight pasta 🍝",
"mediaIds": [
"md_1"
],
"link": "https://example.com/recipe"
} {
"accountIds": [
"acc_x"
],
"content": "Going live right now",
"publishNow": true
} {
"accountIds": [
"acc_x"
],
"content": "Work in progress",
"draft": true
} OpenAPI spec
A machine-readable spec is served (unauthenticated) at /v1/openapi.json — point your client generator or API explorer at it.
curl "https://social-api.instantdm.com/v1/openapi.json" MCP server
The same API is a hosted Model Context Protocol server (Streamable HTTP), so any MCP-capable agent — Claude, Cursor, ChatGPT, Grok and more — can manage your accounts with a workspace API key. No install; point the client at https://social-api.instantdm.com/mcp.
Auth: send your key as the X-Api-Key header (Claude Desktop, Cursor, Claude Code). Clients that only take a URL — Claude.ai's "Add custom connector", ChatGPT, Grok — can't set custom headers, so append ?key=sk_live_xxx to the URL and leave the OAuth fields blank.
For URL-based connectors, use a dedicated, scoped key. You can revoke it any time from the dashboard (Developer → API keys) and it stops working immediately — every request re-checks the key, so there's no cache or delay.
Tools (full parity with the REST API): list_accounts, list_posts, list_platform_posts, delete_platform_post, create_post, update_post, get_post, get_post_status, delete_post, upload_media_from_url, list_media, delete_media, get_analytics.
{
"mcpServers": {
"social-by-idm": {
"type": "streamable-http",
"url": "https://social-api.instantdm.com/mcp",
"headers": { "X-Api-Key": "sk_live_xxx" }
}
}
} claude mcp add --transport http social-by-idm https://social-api.instantdm.com/mcp \
--header "X-Api-Key: sk_live_xxx" https://social-api.instantdm.com/mcp?key=sk_live_xxx import requests
URL = "https://social-api.instantdm.com/mcp"
HEADERS = {"X-Api-Key": "sk_live_xxx"}
def call(method, params=None):
r = requests.post(URL, headers=HEADERS,
json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}})
return r.json()
print(call("tools/list"))
print(call("tools/call", {"name": "list_accounts", "arguments": {}}))