# Social by InstantDM — Developer API

> Schedule and publish content to eight social platforms, upload media, edit and
> delete posts, and read analytics — over a simple REST API. This document is the
> machine-readable reference for AI agents and developers.

- Base URL: `https://social-api.instantdm.com`
- Auth: workspace API key in the `X-Api-Key` header (or `Authorization: Bearer <key>`). Create keys in the dashboard.
- Dashboard: https://social-app.instantdm.com
- OpenAPI spec: `https://social-api.instantdm.com/v1/openapi.json` (no auth)
- Rate limit: 120 requests/minute per key (`429 rate_limited` over the limit).
- Last updated: 2026-06-23

## Authentication

Every request (except the OpenAPI spec) carries a workspace API key, either header form:

```bash
curl "https://social-api.instantdm.com/v1/accounts" -H "X-Api-Key: sk_live_xxx"
curl "https://social-api.instantdm.com/v1/accounts" -H "Authorization: Bearer sk_live_xxx"
```

Keys are workspace-scoped, shown once at creation, and stored hashed. Never embed a key in client-side code.

## Scopes

A request to an endpoint whose scope the key lacks returns `403 forbidden`.

| Scope | Allows |
| --- | --- |
| `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 |

## Errors

Standard HTTP status codes with a consistent JSON body: `{ "error": { "message": "...", "code": "..." } }`.

| HTTP | code | When |
| --- | --- | --- |
| 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. |

## Concepts — posts & targets

A **post** is created once and fans out to one **target** per account in `accountIds`. The same caption/media go to every target unless overridden per platform with `platformContent` / `platformSchedules`. The post has an overall `status`; each target has 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}` for the per-target breakdown.

Scheduling: `publishNow: true` or a `scheduledAt` within 30s publishes immediately; a future `scheduledAt` schedules it; `draft: true` saves without scheduling (activate later via PUT). `platformSchedules` lets each platform fire at its own time.

| status | meaning |
| --- | --- |
| `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. |

## Post types

Set `postType` on a post (or let it auto-detect from media). Whether a type 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

Posts are validated against these limits at create time. Some destinations (a Facebook Page, a Pinterest board) are chosen once 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. |
| 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

Three steps so large files never pass through the API:

1. `POST /v1/media/upload-url` → presigned `uploadUrl` + a `mediaId`.
2. `PUT` the file bytes directly to `uploadUrl` with the same `Content-Type` (valid 15 min).
3. `POST /v1/media/{id}/complete` → the `mediaId` is now attachable via `mediaIds` on a post.

```bash
# 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 '{}'
```

```python
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"])
```

```javascript
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);
```

## Endpoints

## Accounts

### GET /v1/accounts

**List connected accounts** — scope `accounts:read` · returns `200`

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

```bash
curl -X GET "https://social-api.instantdm.com/v1/accounts" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "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** — scope `posts:read` · returns `200`

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

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer · query | no | Page size, 1–100 (default 25). |
| `cursor` | string · query | no | Opaque pagination cursor from the previous response’s nextCursor. Omit for the first page. |

```bash
curl -X GET "https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "accountId": "acc_8fK2qz",
  "platform": "instagram",
  "posts": [
    {
      "platformPostId": "17912…",
      "caption": "Golden hour 🌅",
      "mediaType": "image",
      "permalink": "https://www.instagram.com/p/…",
      "thumbnail": "https://…",
      "timestamp": "2026-06-20T18:00:00+0000",
      "metrics": {
        "likes": 120,
        "comments": 8,
        "shares": 0,
        "impressions": 0
      },
      "inDb": true,
      "dbPostId": "pst_3kqz1a"
    }
  ],
  "nextCursor": "QVFI…"
}
```

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

### DELETE /v1/accounts/{id}/posts/{platformPostId}

**Delete a native platform post** — scope `posts:write` · returns `200`

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.

```bash
curl -X DELETE "https://social-api.instantdm.com/v1/accounts/acc_8fK2qz/posts/{platformPostId}" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "accountId": "acc_8fK2qz",
  "platform": "instagram",
  "platformPostId": "17912…",
  "deleted": true,
  "dbPostId": null
}
```

Errors: `not_found` — The account is not in your workspace.; `platform_error` — The platform API call threw (e.g. token expired / missing scope).

## Media

### POST /v1/media/upload-url

**Create an upload URL** — scope `media:write` · returns `201`

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**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `filename` | string | **yes** | Original file name, e.g. "sunset.jpg". Required. |
| `contentType` | string | **yes** | MIME type, e.g. "image/jpeg" or "video/mp4". Required. |

```bash
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"}'
```

```python
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())
```

```javascript
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);
```

Response `201`:

```json
{
  "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"
}
```

Errors: `bad_request` — filename or contentType is missing.

### POST /v1/media/{id}/complete

**Mark an upload complete** — scope `media:write` · returns `200`

Step 3 of the upload flow. Call this after the PUT succeeds to mark the asset uploaded and ready to attach.

**Body parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `size` | integer | no | Uploaded size in bytes (optional, for your records). |

```bash
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}'
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "mediaId": "md_1a2b3c",
  "status": "uploaded"
}
```

### GET /v1/media

**List media** — scope `media:read` · returns `200`

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

```bash
curl -X GET "https://social-api.instantdm.com/v1/media" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "media": [
    {
      "mediaId": "md_1a2b3c",
      "filename": "sunset.jpg",
      "kind": "image",
      "status": "uploaded",
      "previewUrl": "https://…"
    }
  ]
}
```

### DELETE /v1/media/{id}

**Delete media** — scope `media:write` · returns `200`

Remove a media asset from storage and the library.

```bash
curl -X DELETE "https://social-api.instantdm.com/v1/media/md_1a2b3c" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "mediaId": "md_1a2b3c",
  "deleted": true
}
```

## Posts

### POST /v1/posts

**Create / schedule a post** — scope `posts:write` · returns `201`

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**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `accountIds` | string[] | **yes** | Connected account IDs to publish to. Each must belong to your workspace. Fetch them from GET /v1/accounts. |
| `content` | string | no | Caption / text. Required unless mediaIds (or a thread) is provided. |
| `mediaIds` | string[] | no | IDs of uploaded media to attach (see Uploading media). |
| `postType` | string | no | One of text, image, carousel, video, reel, story, thread, article, document. Auto-detected from media if omitted. |
| `scheduledAt` | string · ISO 8601 | no | 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 | no | Publish immediately, ignoring scheduledAt. |
| `draft` | boolean | no | Save without scheduling. Activate later with PUT (set scheduledAt or publishNow). |
| `platformContent` | object | no | Per-platform caption overrides, e.g. { "instagram": "...", "linkedin": "..." }. Falls back to content. |
| `platformSchedules` | object | no | 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[] | no | For postType "thread" — an array of { text, mediaIds? } segments posted as a reply chain (X, Threads). |
| `article` | object | no | For postType "article" — { url, title?, description? } rich link share (LinkedIn, Facebook). |
| `link` | string | no | Destination URL for the post (e.g. the Pinterest pin link). |
| `coverMediaId` | string | no | Media ID to use as the video/reel cover thumbnail. |
| `coverOffsetMs` | integer | no | Frame offset (ms) for an auto-generated video cover, when coverMediaId is not set. |
| `documentTitle` | string | no | For postType "document" (LinkedIn) — the title shown on the PDF document card. Falls back to the caption. |

```bash
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"}'
```

```python
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())
```

```javascript
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);
```

Response `201`:

```json
{
  "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"
    }
  ]
}
```

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.

### GET /v1/posts

**List posts** — scope `posts:read` · returns `200`

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

```bash
curl -X GET "https://social-api.instantdm.com/v1/posts" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "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** — scope `posts:read` · returns `200`

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.

```bash
curl -X GET "https://social-api.instantdm.com/v1/posts/pst_3kqz1a" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "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** — scope `posts:read` · returns `200`

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.

```bash
curl -X GET "https://social-api.instantdm.com/v1/posts/pst_3kqz1a/status" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "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** — scope `posts:write` · returns `200`

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**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `content / postType / mediaIds` | mixed | no | 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 | no | Replace, or pass an empty value to clear the field. |
| `scheduledAt` | string · ISO 8601 | no | Reschedule a scheduled post, or (on a draft) activate it. Rebuilds the underlying schedule. Timezone rule as above: no offset = workspace timezone. |
| `accountIds` | string[] | no | Retarget to a different set of accounts (recreates the per-platform targets). |
| `publishNow` | boolean | no | Publish the edited post immediately (also activates a draft). |

```bash
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"}'
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "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"
    }
  ]
}
```

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.

### DELETE /v1/posts/{id}

**Delete a post** — scope `posts:write` · returns `200`

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.

```bash
curl -X DELETE "https://social-api.instantdm.com/v1/posts/pst_3kqz1a" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "postId": "pst_3kqz1a",
  "status": "canceled",
  "removedFrom": [
    "facebook"
  ],
  "manual": [
    {
      "platform": "instagram",
      "reason": "platform has no delete API"
    }
  ]
}
```

Errors: `publishing` — The post is currently being published — retry in a few seconds.; `not_found` — The post does not exist in your workspace.

## Analytics

### GET /v1/analytics

**Analytics summary** — scope `analytics:read` · returns `200`

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

```bash
curl -X GET "https://social-api.instantdm.com/v1/analytics" \
  -H "X-Api-Key: $SOCIAL_API_KEY"
```

```python
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())
```

```javascript
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);
```

Response `200`:

```json
{
  "totalPosts": 128,
  "byStatus": {
    "scheduled": 12,
    "published": 110,
    "failed": 6
  },
  "engagement": {
    "likes": 4210,
    "comments": 318,
    "shares": 96,
    "impressions": 51200
  }
}
```

## Post recipes

Request bodies for `POST /v1/posts` — send each with `X-Api-Key` + `Content-Type: application/json`.

### Text post

```json
{
  "accountIds": [
    "acc_x"
  ],
  "content": "Hello world 👋"
}
```

### Single image

```json
{
  "accountIds": [
    "acc_ig"
  ],
  "postType": "image",
  "content": "Golden hour 🌅",
  "mediaIds": [
    "md_1a2b"
  ]
}
```

### Carousel (multi-image)

```json
{
  "accountIds": [
    "acc_ig"
  ],
  "postType": "carousel",
  "content": "Swipe →",
  "mediaIds": [
    "md_1",
    "md_2",
    "md_3"
  ]
}
```

### Reel with custom cover

```json
{
  "accountIds": [
    "acc_ig"
  ],
  "postType": "reel",
  "content": "New drop",
  "mediaIds": [
    "md_video"
  ],
  "coverMediaId": "md_cover"
}
```

### X / Threads thread

```json
{
  "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

```json
{
  "accountIds": [
    "acc_ig",
    "acc_li"
  ],
  "content": "Default caption",
  "platformContent": {
    "instagram": "Casual IG caption ✨",
    "linkedin": "A more professional take for LinkedIn."
  }
}
```

### Staggered times per platform

```json
{
  "accountIds": [
    "acc_ig",
    "acc_x"
  ],
  "content": "Cross-post",
  "scheduledAt": "2026-07-01T09:00:00Z",
  "platformSchedules": {
    "twitter": "2026-07-01T12:00:00Z"
  }
}
```

### Article / link share

```json
{
  "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)

```json
{
  "accountIds": [
    "acc_li"
  ],
  "postType": "document",
  "content": "Swipe through our deck 📄",
  "documentTitle": "Q3 Product Update",
  "mediaIds": [
    "md_pdf"
  ]
}
```

### Pinterest pin with link

```json
{
  "accountIds": [
    "acc_pin"
  ],
  "postType": "image",
  "content": "Weeknight pasta 🍝",
  "mediaIds": [
    "md_1"
  ],
  "link": "https://example.com/recipe"
}
```

### Publish immediately

```json
{
  "accountIds": [
    "acc_x"
  ],
  "content": "Going live right now",
  "publishNow": true
}
```

### Save as a draft

```json
{
  "accountIds": [
    "acc_x"
  ],
  "content": "Work in progress",
  "draft": true
}
```

## MCP server

The same API is a hosted [Model Context Protocol](https://modelcontextprotocol.io) 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; just point the client at the endpoint:

```
https://social-api.instantdm.com/mcp
```

**Auth:** send your key as the `X-Api-Key` header. For URL-only clients (ChatGPT, Grok) that can't set custom headers, append `?key=sk_live_xxx` to the URL instead.

**Claude / Cursor / Windsurf** — MCP client config:

```jsonc
{
  "mcpServers": {
    "social-by-idm": {
      "type": "streamable-http",
      "url": "https://social-api.instantdm.com/mcp",
      "headers": { "X-Api-Key": "sk_live_xxx" }
    }
  }
}
```

**Claude Code (CLI):**

```bash
claude mcp add --transport http social-by-idm https://social-api.instantdm.com/mcp \
  --header "X-Api-Key: sk_live_xxx"
```

**ChatGPT / Grok** — paste the URL (they can't send headers):

```
https://social-api.instantdm.com/mcp?key=sk_live_xxx
```

**Python:**

```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": {}}))
```

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