Post tags API: label posts and compare what works
Tags are your own labels on your posts: the hook a video opens with ("Question hook"), its format ("Talking head"), the campaign it belongs to ("Black Friday"). They answer one question: which kind of post works best for you.
Tags stay in OmniSocials. They are never sent to a platform and never change a caption, so you can tag a post that is already live.
| Endpoint | What it does | Scope |
|---|---|---|
GET /tags | List the workspace's tags | posts:read |
POST /tags | Create a tag | posts:write |
PATCH /tags/:id | Rename or recolour a tag | posts:write |
DELETE /tags/:id | Delete a tag; it comes off every post | posts:write |
POST /tags/apply | Add, remove or replace the tags of up to 500 posts | posts:write |
GET /analytics/tags | Compare each tag with your normal | analytics:read |
The full request and response schemas are in the API reference under Tags. Posts also take tags on POST /posts/create and PATCH /posts/:id, and GET /posts and GET /analytics/overview take a tag filter. Tags belong to a workspace. The dashboard uses the same tags, and its Analytics page shows the same comparison under Tags.
The tag object
JSON
| Field | Meaning |
|---|---|
id | The tag id. Use it, or the name, wherever a tag is asked for. |
name | Up to 40 characters, unique in the workspace (case-insensitive). Runs of spaces become one space. |
color | blue, green, orange, red, purple, pink, teal, yellow, brown or gray. Only the app uses it. Leave it out on create to get the next colour of the palette. |
created_at | When the tag was made. |
The tags of a post are tag objects without created_at.
Create, rename and delete tags
Terminal
The response is 201 with the new tag in data. A name the workspace already has answers 409 tag_exists, with that tag in error.tag, so you can use its id instead.
You rarely need POST /tags: wherever you give a tag by name (tags on a post, add or set on POST /tags/apply), a name that does not exist yet becomes a new tag.
PATCH /tags/:id renames or recolours a tag. A key you leave out keeps its value, and the posts keep the tag:
Terminal
DELETE /tags/:id deletes the tag and takes it off every post of the workspace. The posts stay. The response names the tag and how many posts carried it:
JSON
Tag a post
Pass tags when you create a post:
JSON
tags takes tag names or ids, mixed:
- A name matches case-insensitively. A name that does not exist yet becomes a new tag of the workspace.
- A number, or a string of digits such as
"12", is read as a tag id. An unknown id answers404 tag_not_found. - At most 20 tags per post. A tag named twice counts once.
The post comes back with tags as a list of tag objects. GET /posts and GET /posts/:id return them too.
PATCH /posts/:id with tags replaces the post's tags; null or [] removes them all. A PATCH that sends only tags works on every post, published and failed posts included. A PATCH with other fields still follows the normal rule: a published or failed post cannot change.
Terminal
Tag many posts at once
POST /tags/apply changes the tags of up to 500 posts in one call, published posts included. Use it to tag your older posts, so the comparison has enough posts to work with.
Terminal
| Field | Meaning |
|---|---|
post_ids | Required. 1 to 500 OmniSocials post ids (the id from GET /posts). |
add | Tags to add, by name or id. A new name becomes a tag. The post's other tags stay. |
remove | Tags to take off, by name or id. An unknown name answers 404 tag_not_found. |
set | Replaces each post's tags with exactly these. [] removes them all. |
Give at least one of add, remove and set. When you give more than one, set goes first, then add, then remove.
JSON
postslists every post that was found, with its tags now.updatedcounts the posts whose tags changed. Here post 1024 already had the tag, so only 1025 counts.not_foundlists the ids that are not posts of this workspace. The other posts are still tagged.
The call is all or nothing for the posts it finds: when one post would end up with more than 20 tags, it answers 400 too_many_tags with that post in error.post_id, and no post changes.
Filter by tag
GET /posts?tag= returns only the posts with that tag. Comma separate several tags to get the posts that have any of them. Names and ids both work:
Terminal
GET /analytics/overview?tag= works the same way: the post numbers (total_posts, engagement, impressions, the platform breakdown) then cover only the posts with the tag. Account numbers such as followers stay those of the whole account.
A filter never makes a tag: an unknown name or id answers 404 tag_not_found. For a tag whose name holds a comma, filter by its id.
Compare tags
GET /analytics/tags shows how the posts of each tag did against your normal, for the posts published in a period.
Terminal
| Parameter | Meaning |
|---|---|
start_date | YYYY-MM-DD, YYYY-MM or ISO 8601. Default: 90 days before end_date. |
end_date | Same formats, inclusive. Default: today. |
platform | Only this channel's numbers, for example tiktok. It must be a connected channel. Default: every connected channel. |
min_posts | How many measured posts a tag needs before enough_posts is true. Default 5, from 1 to 50. |
JSON
| Field | Meaning |
|---|---|
posts | Posts with the tag in the period |
measured | Those posts that have numbers yet |
median_impressions | The median of the measured posts' impressions, each post summed over its channels |
engagement_rate | Engagement divided by impressions over all measured posts, in percent (5.4 = 5.4%) |
vs_normal | How far above or below your normal the tag's posts are: 0.38 = 38% above, -0.12 = 12% below, 0 = normal. null without numbers |
best_channel | The channel where the tag's posts beat your normal by the most. Only channels with at least 3 of the tag's posts count; null when none has that many |
enough_posts | false below min_posts measured posts. Read vs_normal then as a hint, not a result |
tags lists the tags with enough posts first, best vs_normal first, then the tags with too few posts. untagged has the same numbers for the posts without a tag. normals is your normal per channel.
How vs_normal is computed
- Your normal per channel. For each channel, the median impressions of every measured post on that channel in the period. These are the
normals. - An index per post. Each post's impressions on a channel are divided by that channel's normal, and the post's index is the mean over its channels. A TikTok post is compared with your TikTok posts, a LinkedIn post with your LinkedIn posts.
- A value per tag.
vs_normalis the median index of the tag's posts, minus 1.
Raw views would let a tag win only because its posts went to the channel with the most views. The median keeps one viral post from carrying a tag. Impressions here are the same number as in the rest of the analytics: views where the platform gives them, else impressions, else reach (see How engagement is calculated).
Which posts count:
- Published posts (
publishedorwarning) whose publish time is in the period, on a connected channel (or onplatformwhen you pass it). - A post with two tags counts in both rows.
- Posts made outside OmniSocials do not count.
A post that published minutes ago has no numbers yet and is not measured. See How metrics are collected.
Trial reels
Posts carry trial_reel: { "strategy": "MANUAL" } or { "strategy": "SS_PERFORMANCE" } when the post went to Instagram as a Trial Reel (instagram.is_trial_reel), and null on every other post. It is on GET /posts, GET /posts/:id and each entry of GET /analytics/posts, so you can compare trial reels with your other reels.
JSON
Instagram's API does not say whether a trial reel was shared with your followers later. trial_reel is the setting the post went out with, not what happened after. To see trial reels in the tag comparison, tag them, for example "Trial reel".
Limits
| Limit | Value |
|---|---|
| Tags per workspace | 200 |
| Tags per post | 20 |
Posts per POST /tags/apply | 500 |
| Tag name | 40 characters |
Errors
Tag errors follow the shared error shape. Some add a field on error, named in the table.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_tag_name | The name is empty or longer than 40 characters |
| 400 | invalid_tag_color | color is not one of the 10 colours |
| 400 | invalid_tags | tags, add, remove or set is not a list of names or ids |
| 400 | invalid_tag_ids | POST /tags/apply without add, remove or set |
| 400 | invalid_post_ids | POST /tags/apply without a post id |
| 400 | too_many_posts | More than 500 post ids |
| 400 | too_many_tags | More than 20 tags on one post; on POST /tags/apply the post is in error.post_id |
| 400 | invalid_date, invalid_date_range, invalid_platform | GET /analytics/tags: a date it cannot read, a start after the end, or a channel that is not connected |
| 404 | tag_not_found | An unknown tag id (in error.tag_ids), or an unknown name in remove or a tag filter (in error.name) |
| 409 | tag_exists | Another tag has that name; it is in error.tag |
| 409 | tag_limit_reached | The workspace has 200 tags. Delete one you no longer use first |
Using it from an AI assistant
The MCP server has list_tags, create_tag, update_tag, delete_tag, tag_posts (the bulk call) and get_tag_analytics. create_post, create_and_publish_post and update_post take tags, and list_posts and get_analytics_overview take tag. get_post and list_posts show each post's tags and mark trial reels.
An assistant can tag your older posts for you: it reads the captions with list_posts, tags them with tag_posts, and then reads the result with get_tag_analytics. It reuses the names from list_tags, so no near-duplicate tags appear.
The CLI has tags:list, tags:create, tags:update, tags:delete and tags:apply, and analytics:tags for the comparison.