# MCP tools

Source: https://postninja.app/docs/mcp/tools

The 19 tools PostNinja's MCP server gives your agent. Parameter tables come straight from the server's own input schemas. Results are a short summary followed by JSON; the same data is in `structuredContent.result`.

## All tools

| Tool | What it does | Access |
| --- | --- | --- |
| [`list_accounts`](#list_accounts) | List accounts | read |
| [`get_workspace`](#get_workspace) | Get workspace settings | read |
| [`list_profiles`](#list_profiles) | List profiles | read |
| [`list_templates`](#list_templates) | List post templates | read |
| [`list_posts`](#list_posts) | List posts | read |
| [`get_post`](#get_post) | Get a post | read |
| [`create_post`](#create_post) | Create or schedule a post | write |
| [`update_post`](#update_post) | Change a post | write |
| [`delete_post`](#delete_post) | Delete a post | write |
| [`approve_post`](#approve_post) | Approve a post | write |
| [`request_changes`](#request_changes) | Ask for changes to a post | write |
| [`get_queue`](#get_queue) | Get the posting queue | read |
| [`update_queue`](#update_queue) | Change the posting queue | write |
| [`get_analytics`](#get_analytics) | Get analytics | read |
| [`list_comments`](#list_comments) | List comments | read |
| [`reply_to_comment`](#reply_to_comment) | Reply to a comment | write |
| [`list_messages`](#list_messages) | List direct messages | read |
| [`reply_to_message`](#reply_to_message) | Reply to a direct message | write |
| [`upload_image`](#upload_image) | Upload an image | write |

> **A typical flow:** `list_accounts` → (`get_workspace` / `list_profiles` for the brand voice) → `upload_image` if there's a photo → `create_post` → later, `get_post` to check it went out.

## list_accounts

**List accounts.** The connected social accounts you can post to (every platform), with their ids.

Read-only. Needs `posts:read`.

**When to use it:** Call it first in almost every conversation: other tools take account ids. `status: needs_reconnect` means the account must be reconnected in the app before it can post. `profileId` says which profile (client or brand) an account belongs to.

Parameters: none.

Example arguments:

```json
{}
```

Example result (the text your agent receives):

```text
Connected accounts:

[
  {
    "id": "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
    "platform": "x",
    "username": "acme",
    "displayName": "Acme",
    "status": "active",
    "profileId": null
  },
  {
    "id": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
    "platform": "instagram",
    "username": "acme.studio",
    "displayName": "Acme Studio",
    "status": "active",
    "profileId": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d"
  },
  {
    "id": "c41d9e7a-2b3c-4d5e-8f60-718293a4b5c6",
    "platform": "tiktok",
    "username": "acmehq",
    "displayName": "Acme",
    "status": "needs_reconnect",
    "profileId": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d"
  }
]
```

## get_workspace

**Get workspace settings.** The workspace's name, its brand voice (how posts should sound) and whether members' posts need approval.

Read-only. Needs `posts:read`.

**When to use it:** Before writing posts, to match the workspace's brand voice, and to know whether members' posts need approval.

Parameters: none.

Example arguments:

```json
{}
```

Example result (the text your agent receives):

```text
Workspace:

{
  "name": "Acme",
  "approvalRequired": true,
  "brandVoice": "Friendly and plain-spoken. Short sentences, no jargon, one emoji at most."
}
```

## list_profiles

**List profiles.** The profiles in this workspace (usually one per client or brand: a set of connected accounts), each with its own brand voice. Use a profile's brand voice when writing for its accounts, and its id (profileId) to filter list_posts.

Read-only. Needs `posts:read`.

**When to use it:** When the workspace runs several brands or clients. Write in a profile's `brandVoice` when posting to its accounts (it wins over the workspace's), and pass its id as `profileId` to `list_posts`, `get_analytics` or `update_queue`. Profiles are part of the Pro and Agency plans; on Personal the list is empty.

Parameters: none.

Example arguments:

```json
{}
```

Example result (the text your agent receives):

```text
Profiles:

[
  {
    "id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
    "name": "Fern & Clay",
    "color": "green",
    "brandVoice": "Warm, handmade, a little playful. Talk about the makers.",
    "accountIds": [
      "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "c41d9e7a-2b3c-4d5e-8f60-718293a4b5c6"
    ],
    "reviewLink": true
  }
]
```

## list_templates

**List post templates.** Ready-made post templates (the built-in library and the team's saved ones). Each has {key} blanks described in fields: replace them with real details (drop lines whose optional blank you leave empty), then pass the text to create_post.

Read-only. Needs `posts:read`.

**When to use it:** When the user wants a post of a known kind (a sale, a launch, a restock, an event) or asks for ideas. Fill every `{key}` blank with real details, drop lines whose optional blank you leave empty, then pass the text to `create_post`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `category` | string | no | One of: promote (Sales and offers), launch (Launches and news), engage (Engagement), educate (Tips and how-tos), proof (Social proof), behind (Behind the scenes), events (Events and seasons), story (Stories and personal brand), community (Community), creator (Creators and influencers), or saved (the team's own). |
| `search` | string | no | Words to look for in the name and description. Up to 100 characters. |
| `limit` | integer | no | 1 to 100. Default `30`. |

Example arguments:

```json
{
  "category": "launch",
  "search": "stock",
  "limit": 5
}
```

Example result (the text your agent receives):

```text
Templates:

[
  {
    "id": "t_back-in-stock",
    "name": "Back in stock",
    "category": "launch",
    "description": "Let people know a popular item is available again.",
    "fields": [
      {
        "key": "product",
        "label": "Product",
        "example": "Our speckled stoneware mugs",
        "optional": false
      },
      {
        "key": "quantity",
        "label": "How many",
        "example": "We made 40 this round, and they went fast last time.",
        "optional": true
      },
      {
        "key": "link",
        "label": "Link",
        "example": "fernandclay.com/mugs",
        "optional": true
      }
    ],
    "content": "Back in stock: {product} 🙌\n\n{quantity}\n\nThank you to everyone who asked us to bring them back. If you missed out before, now's your chance.\n{link}\n\n#BackInStock",
    "platformContent": {}
  }
]
```

## list_posts

**List posts.** Posts in this workspace, newest scheduled time first. Filter by time range, status or profile.

Read-only. Needs `posts:read`.

**When to use it:** To see what's scheduled, find drafts, check what failed, or review posts waiting for approval (`status: awaiting_approval`). Times are compared with each post's scheduled time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string (ISO 8601) | no | Only posts scheduled at or after this time. |
| `to` | string (ISO 8601) | no | Only posts scheduled before this time. |
| `status` | string | no | awaiting_approval: posts waiting for an owner or admin to approve them. One of `draft`, `awaiting_approval`, `scheduled`, `publishing`, `published`, `partial`, `failed`. |
| `profileId` | string (uuid) | no | Only posts going to at least one of this profile's accounts (see list_profiles). |
| `limit` | integer | no | 1 to 100. Default `50`. |

Example arguments:

```json
{
  "from": "2026-10-12T00:00:00Z",
  "to": "2026-10-19T00:00:00Z",
  "status": "scheduled",
  "limit": 20
}
```

Example result (the text your agent receives):

```text
Posts:

[
  {
    "id": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
    "status": "scheduled",
    "content": "We just shipped dark mode. Here's the full story.",
    "platformContent": {
      "x": "Dark mode is here 🌙"
    },
    "xThread": [],
    "xThreadMedia": [],
    "instagramFormat": "post",
    "tiktok": {},
    "firstComment": null,
    "youtubeTitle": null,
    "youtubeThumbnail": null,
    "queued": false,
    "scheduledAt": "2026-10-12T09:00:00.000Z",
    "publishedAt": null,
    "createdAt": "2026-10-06T14:21:07.512Z",
    "media": [
      {
        "id": "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e",
        "kind": "image",
        "url": "https://postninja.app/media/5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e.jpg",
        "previewUrl": null
      }
    ],
    "targets": [
      {
        "accountId": "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
        "platform": "x",
        "username": "acme",
        "status": "pending",
        "url": null,
        "error": null
      },
      {
        "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
        "platform": "instagram",
        "username": "acme.studio",
        "status": "pending",
        "url": null,
        "error": null
      }
    ]
  }
]
```

## get_post

**Get a post.** One post with its per-account status, links to the published posts, any errors, and its approval history.

Read-only. Needs `posts:read`.

**When to use it:** To check whether a post went out: each target has its own `status`, a link (`url`) once published, or the reason it failed (`error`). Also returns the approval history.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `postId` | string (uuid) | yes | The post's id. |

Example arguments:

```json
{
  "postId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9"
}
```

Example result (the text your agent receives):

```text
Post:

{
  "id": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "status": "awaiting_approval",
  "content": "We just shipped dark mode. Here's the full story.",
  "platformContent": {
    "x": "Dark mode is here 🌙"
  },
  "xThread": [],
  "xThreadMedia": [],
  "instagramFormat": "post",
  "tiktok": {},
  "firstComment": null,
  "youtubeTitle": null,
  "youtubeThumbnail": null,
  "queued": false,
  "scheduledAt": "2026-10-12T09:00:00.000Z",
  "publishedAt": null,
  "createdAt": "2026-10-06T14:21:07.512Z",
  "media": [
    {
      "id": "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e",
      "kind": "image",
      "url": "https://postninja.app/media/5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e.jpg",
      "previewUrl": null
    }
  ],
  "targets": [
    {
      "accountId": "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
      "platform": "x",
      "username": "acme",
      "status": "pending",
      "url": null,
      "error": null
    },
    {
      "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "platform": "instagram",
      "username": "acme.studio",
      "status": "pending",
      "url": null,
      "error": null
    }
  ],
  "approvalHistory": [
    {
      "action": "submitted",
      "by": "Sam Lee",
      "note": null,
      "at": "2026-10-06T10:02:11.000Z"
    },
    {
      "action": "changes_requested",
      "by": "Alex Kim",
      "note": "Use the photo from Saturday.",
      "at": "2026-10-06T11:40:52.000Z"
    }
  ]
}
```

## create_post

**Create or schedule a post.** Creates a post for one or more accounts. Set scheduledAt to schedule it, queue to put it in the next free queue slot, publishNow to post right away, or draft to save it without scheduling. Every platform's rules are checked (X: 280 characters, up to 4 photos or 1 video; Instagram: 1-10 photos/videos required, 2,200-character caption; Threads: 500 characters; TikTok: a video or photos; YouTube Shorts: one vertical video up to 3 minutes; Pinterest: photos or a video, 800-character description).

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** To schedule (`scheduledAt`), publish right away (`publishNow: true`), queue (`queue: true`) or save a draft (`draft: true`). Every selected platform's rules are checked first, so an error message tells you exactly what to fix. Give platforms their own text with `platformContent`.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `content` | string | no | The post's text. Used on every platform that doesn't have its own text in platformContent. Up to 10,000 characters. |
| `platformContent` | object | no | Optional text per platform that replaces `content` there, e.g. {"x": "Short version"}. Keys: `x`, `instagram`, `threads`, `facebook`, `tiktok`, `youtube`, `pinterest`. |
| `xThread` | (string or object)[] | no | X only: follow-up posts, in order, that turn the X version into a thread. The first post is the X text (platformContent.x or content). Each is a string, or { "text": "...", "mediaIds": [...] } to give that post its own photos or video. Each is up to 280 characters and costs credits like any X post. Up to 24 items. |
| `xThread[].text` | string | yes | Up to 10,000 characters. |
| `xThread[].mediaIds` | string (uuid)[] | no | This post's own photos (up to 4) or one video. Up to 4 items. |
| `instagramFormat` | string | no | Instagram only: "post" (feed photo, carousel or Reel, the default) or "story" (exactly one photo or video; captions are not shown on Stories). Default `"post"`. |
| `tiktok` | object | no | TikTok only: who can see the post, which interactions to allow, and commercial content disclosure. |
| `tiktok.privacy` | string | no | Who can see the TikTok post. Left out: public if the account allows it. Ask the user rather than guessing. One of `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY`. |
| `tiktok.allowComments` | boolean | no | Allow comments. Default true. |
| `tiktok.allowDuet` | boolean | no | Allow Duet (videos only). Default true. |
| `tiktok.allowStitch` | boolean | no | Allow Stitch (videos only). Default true. |
| `tiktok.yourBrand` | boolean | no | Commercial content promoting the user's own business. TikTok labels it "Promotional content". |
| `tiktok.brandedContent` | boolean | no | Paid partnership promoting a third party. TikTok labels it "Paid partnership"; it can't be private (SELF_ONLY). |
| `tiktok.coverTimestampMs` | integer | no | Videos: which moment of the video is the cover, in milliseconds from the start. |
| `firstComment` | string | no | Optional first comment, posted under the post right after it goes out, where the platform allows it (e.g. links or hashtags). Empty removes it. Up to 2,200 characters. |
| `youtubeTitle` | string | no | YouTube only: the video title (up to 100 characters). Left out: the first line of the text. |
| `youtubeThumbnailId` | string (uuid) or null | no | YouTube only: an uploaded photo (under 2 MB) to use as the video's custom thumbnail. null removes it. |
| `queue` | boolean | no | Put the post in the next free slot of the posting queue (the accounts' profile's slots, else the workspace's) instead of at scheduledAt. |
| `accountIds` | string (uuid)[] | no | Accounts to post to (from list accounts). Up to 200 items. |
| `mediaIds` | string (uuid)[] | no | Uploaded media, in display order (from upload media). Up to 10 items. |
| `scheduledAt` | string (ISO 8601) | no | When to publish, ISO 8601 with a time zone, e.g. 2026-10-05T09:00:00Z. |
| `publishNow` | boolean | no | Publish as soon as possible instead of at scheduledAt. |
| `draft` | boolean | no | Save as a draft without scheduling it. |

Example arguments:

```json
{
  "content": "We just shipped dark mode. Here's the full story.",
  "platformContent": {
    "x": "Dark mode is here 🌙"
  },
  "accountIds": [
    "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
    "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40"
  ],
  "mediaIds": [
    "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"
  ],
  "scheduledAt": "2026-10-12T09:00:00Z"
}
```

Example result (the text your agent receives):

```text
Created post 0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9. Scheduled for 2026-10-12T09:00:00.000Z.

{
  "id": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "status": "scheduled",
  "content": "We just shipped dark mode. Here's the full story.",
  "platformContent": {
    "x": "Dark mode is here 🌙"
  },
  "xThread": [],
  "xThreadMedia": [],
  "instagramFormat": "post",
  "tiktok": {},
  "firstComment": null,
  "youtubeTitle": null,
  "youtubeThumbnail": null,
  "queued": false,
  "scheduledAt": "2026-10-12T09:00:00.000Z",
  "publishedAt": null,
  "createdAt": "2026-10-06T14:21:07.512Z",
  "media": [
    {
      "id": "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e",
      "kind": "image",
      "url": "https://postninja.app/media/5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e.jpg",
      "previewUrl": null
    }
  ],
  "targets": [
    {
      "accountId": "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
      "platform": "x",
      "username": "acme",
      "status": "pending",
      "url": null,
      "error": null
    },
    {
      "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "platform": "instagram",
      "username": "acme.studio",
      "status": "pending",
      "url": null,
      "error": null
    }
  ]
}
```

> **Note:** When approvals are on and the person who signed in is a member, the summary says the post was sent for approval and `status` is `awaiting_approval`.

> **Note:** X posts use credits; if the workspace can't afford them the call fails with a message saying how many are needed.

## update_post

**Change a post.** Changes a draft, scheduled or waiting-for-approval post. Only the fields you pass change; the rest stay as they are. When an owner or admin (or an API key) schedules a post that was waiting for approval, that approves it.

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** To change the text, accounts, media, time or options of a draft, scheduled or waiting post. Only the fields you pass change. Send `scheduledAt` to move a post, `draft: true` to unschedule it, or `queue: true` to put it in the queue. Posts that already went out can't be changed.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `content` | string | no | The post's text. Used on every platform that doesn't have its own text in platformContent. Up to 10,000 characters. |
| `platformContent` | object | no | Optional text per platform that replaces `content` there, e.g. {"x": "Short version"}. Keys: `x`, `instagram`, `threads`, `facebook`, `tiktok`, `youtube`, `pinterest`. |
| `xThread` | (string or object)[] | no | X only: follow-up posts, in order, that turn the X version into a thread. The first post is the X text (platformContent.x or content). Each is a string, or { "text": "...", "mediaIds": [...] } to give that post its own photos or video. Each is up to 280 characters and costs credits like any X post. Up to 24 items. |
| `xThread[].text` | string | yes | Up to 10,000 characters. |
| `xThread[].mediaIds` | string (uuid)[] | no | This post's own photos (up to 4) or one video. Up to 4 items. |
| `instagramFormat` | string | no | Instagram only: "post" (feed photo, carousel or Reel, the default) or "story" (exactly one photo or video; captions are not shown on Stories). |
| `tiktok` | object | no | TikTok only: who can see the post, which interactions to allow, and commercial content disclosure. |
| `tiktok.privacy` | string | no | Who can see the TikTok post. Left out: public if the account allows it. Ask the user rather than guessing. One of `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY`. |
| `tiktok.allowComments` | boolean | no | Allow comments. Default true. |
| `tiktok.allowDuet` | boolean | no | Allow Duet (videos only). Default true. |
| `tiktok.allowStitch` | boolean | no | Allow Stitch (videos only). Default true. |
| `tiktok.yourBrand` | boolean | no | Commercial content promoting the user's own business. TikTok labels it "Promotional content". |
| `tiktok.brandedContent` | boolean | no | Paid partnership promoting a third party. TikTok labels it "Paid partnership"; it can't be private (SELF_ONLY). |
| `tiktok.coverTimestampMs` | integer | no | Videos: which moment of the video is the cover, in milliseconds from the start. |
| `firstComment` | string | no | Optional first comment, posted under the post right after it goes out, where the platform allows it (e.g. links or hashtags). Empty removes it. Up to 2,200 characters. |
| `youtubeTitle` | string | no | YouTube only: the video title (up to 100 characters). Left out: the first line of the text. |
| `youtubeThumbnailId` | string (uuid) or null | no | YouTube only: an uploaded photo (under 2 MB) to use as the video's custom thumbnail. null removes it. |
| `queue` | boolean | no | Put the post in the next free slot of the posting queue (the accounts' profile's slots, else the workspace's) instead of at scheduledAt. |
| `accountIds` | string (uuid)[] | no | Accounts to post to (from list accounts). Up to 200 items. |
| `mediaIds` | string (uuid)[] | no | Uploaded media, in display order (from upload media). Up to 10 items. |
| `scheduledAt` | string (ISO 8601) | no | When to publish, ISO 8601 with a time zone, e.g. 2026-10-05T09:00:00Z. |
| `publishNow` | boolean | no | Publish as soon as possible instead of at scheduledAt. |
| `draft` | boolean | no | Save as a draft without scheduling it. |
| `postId` | string (uuid) | yes | The post's id. |

Example arguments:

```json
{
  "postId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "scheduledAt": "2026-10-13T09:00:00Z",
  "platformContent": {
    "x": "Dark mode is finally here 🌙"
  }
}
```

Example result (the text your agent receives):

```text
Updated post 0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9.

{
  "id": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "status": "scheduled",
  "content": "We just shipped dark mode. Here's the full story.",
  "platformContent": {
    "x": "Dark mode is finally here 🌙"
  },
  "xThread": [],
  "xThreadMedia": [],
  "instagramFormat": "post",
  "tiktok": {},
  "firstComment": null,
  "youtubeTitle": null,
  "youtubeThumbnail": null,
  "queued": false,
  "scheduledAt": "2026-10-13T09:00:00.000Z",
  "publishedAt": null,
  "createdAt": "2026-10-06T14:21:07.512Z",
  "media": [
    {
      "id": "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e",
      "kind": "image",
      "url": "https://postninja.app/media/5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e.jpg",
      "previewUrl": null
    }
  ],
  "targets": [
    {
      "accountId": "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
      "platform": "x",
      "username": "acme",
      "status": "pending",
      "url": null,
      "error": null
    },
    {
      "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "platform": "instagram",
      "username": "acme.studio",
      "status": "pending",
      "url": null,
      "error": null
    }
  ]
}
```

## delete_post

**Delete a post.** Deletes a post (draft, scheduled, waiting or sent). A post that was already published stays live on the platform; it's only removed from PostNinja.

Changes data. Needs `posts:write` (or a Read & write API key). Destructive: it can't be undone.

**When to use it:** To cancel a draft or scheduled post. Deleting a post that already went out only removes it from PostNinja; it stays live on the platforms. Credits held for accounts it hadn't reached yet are given back.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `postId` | string (uuid) | yes | The post's id. |

Example arguments:

```json
{
  "postId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9"
}
```

Example result (the text your agent receives):

```text
Deleted post 0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9.
```

## approve_post

**Approve a post.** Approves a post waiting for approval (status awaiting_approval), so it's scheduled for its time, or goes out now if that time has passed. Only the workspace owner or an admin can approve.

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** When an owner or admin asks you to review what the team submitted: list posts with `status: awaiting_approval`, show them, and approve the ones they're happy with. A post whose time has passed goes out right away.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `postId` | string (uuid) | yes | The post's id. |
| `note` | string | no | Optional note for the author. Up to 1,000 characters. |

Example arguments:

```json
{
  "postId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "note": "Looks great."
}
```

Example result (the text your agent receives):

```text
Approved post 0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9.

{
  "id": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "status": "scheduled",
  "content": "We just shipped dark mode. Here's the full story.",
  "platformContent": {
    "x": "Dark mode is here 🌙"
  },
  "xThread": [],
  "xThreadMedia": [],
  "instagramFormat": "post",
  "tiktok": {},
  "firstComment": null,
  "youtubeTitle": null,
  "youtubeThumbnail": null,
  "queued": false,
  "scheduledAt": "2026-10-12T09:00:00.000Z",
  "publishedAt": null,
  "createdAt": "2026-10-06T14:21:07.512Z",
  "media": [
    {
      "id": "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e",
      "kind": "image",
      "url": "https://postninja.app/media/5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e.jpg",
      "previewUrl": null
    }
  ],
  "targets": [
    {
      "accountId": "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
      "platform": "x",
      "username": "acme",
      "status": "pending",
      "url": null,
      "error": null
    },
    {
      "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "platform": "instagram",
      "username": "acme.studio",
      "status": "pending",
      "url": null,
      "error": null
    }
  ],
  "approvalHistory": [
    {
      "action": "submitted",
      "by": "Sam Lee",
      "note": null,
      "at": "2026-10-06T10:02:11.000Z"
    },
    {
      "action": "approved",
      "by": "Alex Kim",
      "note": "Looks great.",
      "at": "2026-10-06T12:01:09.000Z"
    }
  ]
}
```

## request_changes

**Ask for changes to a post.** Sends a post waiting for approval back to its author's drafts with a note saying what to change; the author gets it by email. Only the workspace owner or an admin can do this.

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** To send a waiting post back to its author's drafts. The note is required and emailed to the author, so say exactly what to change.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `postId` | string (uuid) | yes | The post's id. |
| `note` | string | yes | What needs changing. The author gets it by email. Up to 1,000 characters. |

Example arguments:

```json
{
  "postId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "note": "Use the photo from Saturday and mention free delivery."
}
```

Example result (the text your agent receives):

```text
Sent post 0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9 back to its author.

{
  "id": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
  "status": "draft",
  "content": "We just shipped dark mode. Here's the full story.",
  "platformContent": {
    "x": "Dark mode is here 🌙"
  },
  "xThread": [],
  "xThreadMedia": [],
  "instagramFormat": "post",
  "tiktok": {},
  "firstComment": null,
  "youtubeTitle": null,
  "youtubeThumbnail": null,
  "queued": false,
  "scheduledAt": "2026-10-12T09:00:00.000Z",
  "publishedAt": null,
  "createdAt": "2026-10-06T14:21:07.512Z",
  "media": [
    {
      "id": "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e",
      "kind": "image",
      "url": "https://postninja.app/media/5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e.jpg",
      "previewUrl": null
    }
  ],
  "targets": [
    {
      "accountId": "3f2c8a10-6d1e-4b7a-9c55-1e2f3a4b5c6d",
      "platform": "x",
      "username": "acme",
      "status": "pending",
      "url": null,
      "error": null
    },
    {
      "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "platform": "instagram",
      "username": "acme.studio",
      "status": "pending",
      "url": null,
      "error": null
    }
  ],
  "approvalHistory": [
    {
      "action": "submitted",
      "by": "Sam Lee",
      "note": null,
      "at": "2026-10-06T10:02:11.000Z"
    },
    {
      "action": "changes_requested",
      "by": "Alex Kim",
      "note": "Use the photo from Saturday.",
      "at": "2026-10-06T11:40:52.000Z"
    }
  ]
}
```

## get_queue

**Get the posting queue.** The posting queue's weekly time slots (weekday 0 = Monday … 6 = Sunday, 24-hour HH:MM in the queue's time zone): the workspace's (profileId null) and each profile's own. create_post with queue: true takes the next free slot of the accounts' profile, else the workspace's.

Read-only. Needs `posts:read`.

**When to use it:** Before queueing posts, to see the weekly slots and their time zone. `profileId: null` slots are the workspace's; a profile with its own slots uses those instead.

Parameters: none.

Example arguments:

```json
{}
```

Example result (the text your agent receives):

```text
Posting queue:

{
  "timeZone": "Europe/London",
  "slots": [
    {
      "weekday": 0,
      "time": "09:00",
      "profileId": null
    },
    {
      "weekday": 2,
      "time": "09:00",
      "profileId": null
    },
    {
      "weekday": 4,
      "time": "12:30",
      "profileId": null
    },
    {
      "weekday": 1,
      "time": "18:00",
      "profileId": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d"
    }
  ]
}
```

## update_queue

**Change the posting queue.** Replaces one set of weekly queue slots: a profile's (profileId) or the workspace's (profileId null). Send the complete new list; an empty list removes a profile's own slots so it uses the workspace's. Only the workspace owner or an admin can do this. Posts already queued keep their times.

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** To set up or change posting times. Send the complete list for one set (it replaces the old one). An empty list removes a profile's own slots, so it falls back to the workspace's. Posts already queued keep their times.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `profileId` | string (uuid) or null | no | Whose slots to replace: a profile's id (from list_profiles), or null for the workspace's own slots. Default `null`. |
| `slots` | object[] | yes | The complete new list of weekly slots. An empty list removes them (a profile then uses the workspace's slots). Up to 100 items. |
| `slots[].weekday` | integer | yes | 0 = Monday … 6 = Sunday. 0 to 6. |
| `slots[].time` | string | yes | 24-hour HH:MM in the queue's time zone. |

Example arguments:

```json
{
  "profileId": null,
  "slots": [
    {
      "weekday": 0,
      "time": "09:00"
    },
    {
      "weekday": 2,
      "time": "09:00"
    },
    {
      "weekday": 4,
      "time": "12:30"
    }
  ]
}
```

Example result (the text your agent receives):

```text
Updated the posting queue.

{
  "timeZone": "Europe/London",
  "slots": [
    {
      "weekday": 0,
      "time": "09:00",
      "profileId": null
    },
    {
      "weekday": 2,
      "time": "09:00",
      "profileId": null
    },
    {
      "weekday": 4,
      "time": "12:30",
      "profileId": null
    }
  ]
}
```

## get_analytics

**Get analytics.** Published posts' stats and follower growth for a date range (UTC days, default the last 30), compared with the period before: headline numbers, one row per account and the top posts. Filter by profile, account or platform.

Read-only. Needs `posts:read`.

**When to use it:** For reports: how posts did over a period, which accounts grew, and the top posts. Numbers come from stats already synced, so this costs nothing and is fast. Compare `kpis` with `kpis.previous` (the same number of days before).

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | no | First day, YYYY-MM-DD (send with to). |
| `to` | string | no | Last day, YYYY-MM-DD (send with from). |
| `profileId` | string (uuid) | no | Only this profile's accounts (see list_profiles). |
| `accountId` | string (uuid) | no | Only this account (see list_accounts). |
| `platform` | string | no | Only this platform, e.g. instagram or youtube. |

Example arguments:

```json
{
  "from": "2026-09-07",
  "to": "2026-10-06"
}
```

Example result (the text your agent receives):

```text
Analytics:

{
  "range": {
    "from": "2026-09-07",
    "to": "2026-10-06",
    "days": 30,
    "timeZone": "UTC"
  },
  "previousRange": {
    "from": "2026-08-08",
    "to": "2026-09-06"
  },
  "kpis": {
    "postsPublished": 12,
    "postsWithStats": 11,
    "impressions": 48210,
    "engagements": 2391,
    "likes": 1874,
    "reposts": 143,
    "replies": 262,
    "quotes": 0,
    "bookmarks": 112,
    "engagementRate": 0.0496,
    "followers": 18422,
    "followerGrowth": 611,
    "bestPlatform": {
      "platform": "instagram",
      "engagementRate": 0.0712,
      "posts": 6
    },
    "previous": {
      "postsPublished": 12,
      "postsWithStats": 11,
      "impressions": 39120,
      "engagements": 1830,
      "likes": 1874,
      "reposts": 143,
      "replies": 262,
      "quotes": 0,
      "bookmarks": 112,
      "engagementRate": 0.0468,
      "followerGrowth": 402
    }
  },
  "accounts": [
    {
      "id": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "platform": "instagram",
      "username": "acme.studio",
      "displayName": "Acme Studio",
      "profileId": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
      "followers": 12904,
      "followersCountedOn": "2026-10-06",
      "followerGrowth": 488,
      "postsPublished": 12,
      "postsWithStats": 11,
      "impressions": 48210,
      "engagements": 2391,
      "likes": 1874,
      "reposts": 143,
      "replies": 262,
      "quotes": 0,
      "bookmarks": 112,
      "engagementRate": 0.0496,
      "averageImpressions": 4383,
      "topPostId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9"
    }
  ],
  "topPosts": [
    {
      "id": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9",
      "text": "Back in stock: our speckled stoneware mugs 🙌",
      "platforms": [
        "instagram",
        "threads"
      ],
      "type": "photo",
      "publishedAt": "2026-09-18T09:00:04.000Z",
      "impressions": 9120,
      "engagements": 811,
      "engagementRate": 0.0889,
      "url": "https://www.instagram.com/p/DA1b2C3d4E5/"
    }
  ],
  "notes": {
    "…": "Short caveats per platform, e.g. which platforms have no post stats."
  }
}
```

> **Note:** X posts have no stats in PostNinja, so they count as published without numbers. Instagram, Threads, Facebook, TikTok, YouTube and Pinterest posts have stats.

## list_comments

**List comments.** Comment conversations on the connected accounts' posts (Instagram, Facebook Pages, Threads, YouTube), latest first. Pass commentId to get one conversation with every message. Reply with reply_to_comment.

Read-only. Needs `posts:read`.

**When to use it:** To answer comments: list conversations with `filter: needs_reply`, then open one with `commentId` to read every message and the post it's on. Reading here doesn't mark comments as read in the app.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `filter` | string | no | needs_reply: the latest message isn't ours. One of `all`, `unread`, `needs_reply`, `replied`, `hidden`. Default `"all"`. |
| `platform` | string | no | One of `instagram`, `facebook`, `threads`, `youtube`. |
| `accountId` | string (uuid) | no |   |
| `commentId` | string (uuid) | no | Get this conversation in full instead of the list. |
| `limit` | integer | no | 1 to 100. Default `30`. |

Example arguments:

```json
{
  "filter": "needs_reply",
  "platform": "instagram",
  "limit": 10
}
```

Example result (the text your agent receives):

```text
Comment conversations:

{
  "threads": [
    {
      "id": "9e2a4f5b-6c7d-4e8f-a091-0c1d2e3f4a5b",
      "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "provider": "instagram",
      "authorName": "Jamie",
      "authorHandle": "jamie.makes",
      "authorAvatarUrl": null,
      "text": "Do these ship to Canada?",
      "postedAt": "2026-10-06T08:14:22.000Z",
      "count": 1,
      "unread": 1,
      "needsReply": true,
      "replied": false,
      "hidden": false,
      "postId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9"
    }
  ],
  "hasMore": false
}
```

Example result with commentId: one conversation, every message:

```text
Conversation:

{
  "id": "9e2a4f5b-6c7d-4e8f-a091-0c1d2e3f4a5b",
  "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
  "provider": "instagram",
  "post": {
    "text": "Back in stock: our speckled stoneware mugs 🙌",
    "thumbnail": null,
    "permalink": "https://www.instagram.com/p/DA1b2C3d4E5/",
    "postedAt": "2026-10-05T09:00:04.000Z",
    "postId": "0d3c4b5a-6978-4e1f-a2b3-c4d5e6f7a8b9"
  },
  "messages": [
    {
      "id": "9e2a4f5b-6c7d-4e8f-a091-0c1d2e3f4a5b",
      "kind": "comment",
      "fromUs": false,
      "authorName": "Jamie",
      "authorHandle": "jamie.makes",
      "authorAvatarUrl": null,
      "text": "Do these ship to Canada?",
      "postedAt": "2026-10-06T08:14:22.000Z",
      "hidden": false,
      "permalink": null,
      "sentByName": null,
      "readAt": null
    }
  ],
  "canWrite": true,
  "writeNote": null,
  "hideLabel": "Hide",
  "maxLength": 2200
}
```

## reply_to_comment

**Reply to a comment.** Replies publicly to a comment as the connected account it was left on. Use a commentId from list_comments.

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** To reply publicly as the account the comment was left on. Use the conversation's `id` from `list_comments`. Keep replies within the platform's limit (`maxLength` in the conversation).

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `commentId` | string (uuid) | yes |   |
| `text` | string | yes | Up to 2,200 characters. |

Example arguments:

```json
{
  "commentId": "9e2a4f5b-6c7d-4e8f-a091-0c1d2e3f4a5b",
  "text": "They do! Shipping to Canada takes 5-7 days."
}
```

Example result (the text your agent receives):

```text
Reply sent.

{
  "id": "a3b4c5d6-e7f8-4a9b-8c0d-1e2f3a4b5c6d",
  "kind": "reply",
  "fromUs": true,
  "authorName": "Acme Studio",
  "authorHandle": "acme.studio",
  "authorAvatarUrl": null,
  "text": "They do! Shipping to Canada takes 5-7 days.",
  "postedAt": "2026-10-06T09:02:40.000Z",
  "hidden": false,
  "permalink": null,
  "sentByName": null,
  "readAt": "2026-10-06T09:02:40.000Z"
}
```

## list_messages

**List direct messages.** Direct-message conversations on the connected Instagram and Facebook Page accounts, latest first, with each one's reply window. Pass conversationId to get one conversation with its messages. Reply with reply_to_message while window.state isn't "closed".

Read-only. Needs `posts:read`.

**When to use it:** To answer direct messages on Instagram and Facebook Pages: list conversations with `filter: needs_reply`, then open one with `conversationId` to read its messages. Check `window`: replies are only possible while `window.state` is `open` (24 hours after the person's last message). Reading here doesn't mark messages as read in the app.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `filter` | string | no | needs_reply: they wrote last. One of `all`, `unread`, `needs_reply`. Default `"all"`. |
| `platform` | string | no | One of `instagram`, `facebook`. |
| `accountId` | string (uuid) | no |   |
| `conversationId` | string (uuid) | no | Get this conversation in full instead of the list. |
| `limit` | integer | no | 1 to 100. Default `30`. |

Example arguments:

```json
{
  "filter": "needs_reply",
  "limit": 10
}
```

Example result (the text your agent receives):

```text
Message conversations:

{
  "conversations": [
    {
      "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
      "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
      "provider": "instagram",
      "participantName": "Jamie Lee",
      "participantUsername": "jamie.makes",
      "participantAvatarUrl": null,
      "lastText": "Hi! Is the large mug back in stock?",
      "lastFromUs": false,
      "lastAttachments": 0,
      "lastMessageAt": "2026-10-06T08:14:22.000Z",
      "unread": 1,
      "needsReply": true,
      "window": {
        "state": "open",
        "closesAt": "2026-10-07T08:14:22.000Z"
      }
    }
  ],
  "hasMore": false
}
```

Example result with conversationId: one conversation with its messages:

```text
Conversation:

{
  "id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
  "accountId": "8b7e1d22-0c4f-4e8a-a1b2-9d8c7b6a5f40",
  "provider": "instagram",
  "participant": {
    "id": "1784001234567890",
    "name": "Jamie Lee",
    "username": "jamie.makes",
    "avatarUrl": null
  },
  "permalink": "https://ig.me/m/jamie.makes",
  "messages": [
    {
      "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
      "fromUs": false,
      "text": "Hi! Is the large mug back in stock?",
      "attachments": [
        {
          "type": "image",
          "url": "https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=…",
          "previewUrl": null,
          "name": null
        }
      ],
      "sentAt": "2026-10-06T08:14:22.000Z",
      "readAt": null,
      "sentByName": null
    }
  ],
  "window": {
    "state": "open",
    "closesAt": "2026-10-07T08:14:22.000Z"
  },
  "canReply": true,
  "replyNote": null,
  "maxLength": 1000,
  "lengthUnit": "bytes"
}
```

> **Note:** Attachment links point at Instagram's or Facebook's servers and stop working after a while.

## reply_to_message

**Reply to a direct message.** Sends a private direct message in a conversation, as the connected account. Instagram and Messenger only allow it within 24 hours of the person's last message; after that it's refused. Instagram messages can be up to 1,000 bytes, Facebook up to 2,000 characters.

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** To answer a direct message privately as the account it was sent to. Use the conversation's `id` from `list_messages`. Only while the reply window is open; once it has closed the tool returns an error and the person has to write again first.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `conversationId` | string (uuid) | yes |   |
| `text` | string | yes | Up to 2,000 characters. |

Example arguments:

```json
{
  "conversationId": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
  "text": "It is! It's back on the shop today."
}
```

Example result (the text your agent receives):

```text
Message sent.

{
  "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
  "fromUs": true,
  "text": "It is! It's back on the shop today.",
  "attachments": [],
  "sentAt": "2026-10-06T09:02:40.000Z",
  "readAt": "2026-10-06T09:02:40.000Z",
  "sentByName": null
}
```

## upload_image

**Upload an image.** Uploads a JPEG, PNG or WebP image (base64, up to about 3 MB) and returns a media id to use in create_post. For videos and larger files, use the REST API: POST /api/v1/media.

Changes data. Needs `posts:write` (or a Read & write API key).

**When to use it:** When the user gives the agent an image to post. Send it base64-encoded (no `data:` prefix), then pass the returned id in `mediaIds`. For videos or images over about 3 MB, upload with the REST API (`POST /api/v1/media`) and give the agent the id.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | string | yes | The image file, base64-encoded (no data: prefix). |
| `mimeType` | string | yes | One of `image/jpeg`, `image/png`, `image/webp`. |

Example arguments:

```json
{
  "data": "/9j/4AAQSkZJRgABAQAAAQABAAD…",
  "mimeType": "image/jpeg"
}
```

Example result (the text your agent receives):

```text
Uploaded. Use mediaIds: ["5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"] in create_post.

{
  "id": "5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e",
  "kind": "image",
  "width": 1080,
  "height": 1350,
  "url": "https://postninja.app/media/5b9d1c2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e.jpg"
}
```
