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.

Quick start
# 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.

Authenticated request
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:readList connected accounts
posts:readList and read posts
posts:writeCreate, edit, delete posts
media:readList media assets
media:writeUpload and delete media
analytics:readRead the analytics summary
platform:proxyCall a platform’s API directly via the proxy (opt-in, advanced)
Insufficient scope · 403
{
  "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.

Rate limited · 429
{
  "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.

400bad_requestRequired fields are missing or malformed.
401unauthorizedMissing or invalid API key.
403forbiddenKey is valid but lacks the required scope.
404not_foundThe post, media, or referenced account does not exist.
409not_editableThe post is in a state that cannot be edited (e.g. already publishing).
409publishingDELETE on a post that is mid-publish — retry once it settles.
422post_type_invalidpostType does not match the attached media (e.g. carousel with one image).
422content_invalidCaption exceeds a platform limit, or a platform requires media you did not attach.
429rate_limitedMore than 120 requests in one minute for this key.
Error shape
{
  "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

draftSaved but not scheduled. Activate by editing (PUT) with a scheduledAt or publishNow.
scheduledQueued for a future time.
publishingHanded to the publish worker right now.
publishedLive on every target platform.
partialLive on some targets, failed on others — inspect per-target status.
failedEvery target failed. The reason is on each target row.
canceledDeleted/canceled before or after going out.
Scheduling model
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.

postTypeMediaNotes
textnoneText-only update. The default when no media is attached.
image1A single image. The default when media is attached.
carousel2–10Multi-image. Pinterest caps at 5 and requires a shared aspect ratio.
video1 videoA standard video post.
reel1 videoShort-form vertical video. Supports coverMediaId / coverOffsetMs for the thumbnail.
story1Story format, where the platform API allows it.
threadper segmentA chain of posts (X / Threads). Use the thread[] array.
articlenoneA link share with a rich preview (LinkedIn, Facebook). Use the article object.
document1 PDFA 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.

PlatformPost typesMax textMax mediaThreadsEditDeleteNotes
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.
Instagram 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.
Facebook text, image, video, carousel, reel, article 63,206 10 (carousel) No Yes Yes Publishes to a selected Page (set on the Accounts page).
LinkedIn 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.
Pinterest 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:

  1. Request a URLPOST /v1/media/upload-url returns a presigned uploadUrl and a mediaId.
  2. PUT the bytes — upload the file directly to uploadUrl with the same Content-Type. This goes straight to storage, not the API. The URL is valid for 15 minutes.
  3. Mark completePOST /v1/media/{id}/complete. The mediaId is now attachable via mediaIds on 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);
GET /v1/accounts

List connected accounts

Every social account connected to your workspace. Use the accountId values as targets when creating a post.

Group · Accounts Scope · accounts:read Returns · 200
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);
Example response · 200
{
  "accounts": [
    {
      "accountId": "acc_8fK2qz",
      "platform": "instagram",
      "handle": "@acme",
      "displayName": "Acme Inc"
    },
    {
      "accountId": "acc_p1L9wd",
      "platform": "linkedin",
      "handle": "acme-inc",
      "displayName": "Acme Inc"
    }
  ]
}
GET /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).

Group · Accounts Scope · posts:read Returns · 200

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);
Example response · 200
{
  "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…"
}
DELETE /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.

Group · Accounts Scope · posts:write Returns · 200

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);
Example response · 200
{
  "accountId": "acc_8fK2qz",
  "platform": "instagram",
  "platformPostId": "17912…",
  "deleted": true,
  "dbPostId": null
}
POST /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.

Group · Media Scope · media:write Returns · 201

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);
Example response · 201
{
  "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"
}
POST /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.

Group · Media Scope · media:write Returns · 200

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);
Example response · 200
{
  "mediaId": "md_1a2b3c",
  "status": "uploaded"
}
GET /v1/media

List media

Every media asset in the workspace, each with a previewUrl.

Group · Media Scope · media:read Returns · 200
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);
Example response · 200
{
  "media": [
    {
      "mediaId": "md_1a2b3c",
      "filename": "sunset.jpg",
      "kind": "image",
      "status": "uploaded",
      "previewUrl": "https://…"
    }
  ]
}
DELETE /v1/media/{id}

Delete media

Remove a media asset from storage and the library.

Group · Media Scope · media:write Returns · 200
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);
Example response · 200
{
  "mediaId": "md_1a2b3c",
  "deleted": true
}
POST /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.

Group · Posts Scope · posts:write Returns · 201

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);
Example response · 201
{
  "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"
    }
  ]
}
GET /v1/posts

List posts

Posts in the workspace, ordered by scheduled time, across every status.

Group · Posts Scope · posts:read Returns · 200
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);
Example response · 200
{
  "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"
      ]
    }
  ]
}
GET /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.

Group · Posts Scope · posts:read Returns · 200
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);
Example response · 200
{
  "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"
    }
  ]
}
GET /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.

Group · Posts Scope · posts:read Returns · 200
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);
Example response · 200
{
  "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
    }
  ]
}
PUT /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.

Group · Posts Scope · posts:write Returns · 200

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);
Example response · 200
{
  "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"
    }
  ]
}
DELETE /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.

Group · Posts Scope · posts:write Returns · 200

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);
Example response · 200
{
  "postId": "pst_3kqz1a",
  "status": "canceled",
  "removedFrom": [
    "facebook"
  ],
  "manual": [
    {
      "platform": "instagram",
      "reason": "platform has no delete API"
    }
  ]
}
GET /v1/analytics

Analytics summary

Post counts by status plus total engagement across every published post.

Group · Analytics Scope · analytics:read Returns · 200
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);
Example response · 200
{
  "totalPosts": 128,
  "byStatus": {
    "scheduled": 12,
    "published": 110,
    "failed": 6
  },
  "engagement": {
    "likes": 4210,
    "comments": 318,
    "shares": 96,
    "impressions": 51200
  }
}
ANY /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.

Group · Proxy Scope · platform:proxy Returns · 200

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);
Example response · 200

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).

Text post
{
  "accountIds": [
    "acc_x"
  ],
  "content": "Hello world 👋"
}
Single image
{
  "accountIds": [
    "acc_ig"
  ],
  "postType": "image",
  "content": "Golden hour 🌅",
  "mediaIds": [
    "md_1a2b"
  ]
}
Carousel (multi-image)
{
  "accountIds": [
    "acc_ig"
  ],
  "postType": "carousel",
  "content": "Swipe →",
  "mediaIds": [
    "md_1",
    "md_2",
    "md_3"
  ]
}
Reel with custom cover
{
  "accountIds": [
    "acc_ig"
  ],
  "postType": "reel",
  "content": "New drop",
  "mediaIds": [
    "md_video"
  ],
  "coverMediaId": "md_cover"
}
X / Threads thread
{
  "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."
    }
  ]
}
Per-platform captions
{
  "accountIds": [
    "acc_ig",
    "acc_li"
  ],
  "content": "Default caption",
  "platformContent": {
    "instagram": "Casual IG caption ✨",
    "linkedin": "A more professional take for LinkedIn."
  }
}
Staggered times per platform
{
  "accountIds": [
    "acc_ig",
    "acc_x"
  ],
  "content": "Cross-post",
  "scheduledAt": "2026-07-01T09:00:00Z",
  "platformSchedules": {
    "twitter": "2026-07-01T12:00:00Z"
  }
}
Article / link share
{
  "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."
  }
}
LinkedIn document (PDF)
{
  "accountIds": [
    "acc_li"
  ],
  "postType": "document",
  "content": "Swipe through our deck 📄",
  "documentTitle": "Q3 Product Update",
  "mediaIds": [
    "md_pdf"
  ]
}
Pinterest pin with link
{
  "accountIds": [
    "acc_pin"
  ],
  "postType": "image",
  "content": "Weeknight pasta 🍝",
  "mediaIds": [
    "md_1"
  ],
  "link": "https://example.com/recipe"
}
Publish immediately
{
  "accountIds": [
    "acc_x"
  ],
  "content": "Going live right now",
  "publishNow": true
}
Save as a draft
{
  "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.

Fetch the spec
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.

Claude Desktop / Cursor / Windsurf
{
  "mcpServers": {
    "social-by-idm": {
      "type": "streamable-http",
      "url": "https://social-api.instantdm.com/mcp",
      "headers": { "X-Api-Key": "sk_live_xxx" }
    }
  }
}
Claude Code (CLI)
claude mcp add --transport http social-by-idm https://social-api.instantdm.com/mcp \
  --header "X-Api-Key: sk_live_xxx"
Claude.ai · ChatGPT · Grok (URL only)
https://social-api.instantdm.com/mcp?key=sk_live_xxx
Python
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": {}}))