Adding content
Most of the Analyst API reads your workspace. The content routes are the exception: they let a key add posts to your team the way Add New does in the Siftsy app, so a pipeline can feed Siftsy instead of a teammate pasting links.
They work exactly like the dashboard:
- Same credits. Every accepted post reserves one MAP (or one Ad Hoc credit when MAPs run out), or five with a Full Read. Posts already on the team and links Siftsy cannot process are never charged.
- Same team. Content lands on the key’s team. Enterprise keys can name a sub-team with
teamId, and that sub-team’s credits are used. - Same processing. Posts show up in the app as they process and become readable through
GET /postswhen they finish, taggedaddedVia: api. (Posts a teammate added aredashboard; posts a Siftsy Automation pulled in on a schedule areautomation.)
You get back a content job that tells you what was accepted, what was skipped, what was rejected, and how the accepted posts are progressing.
The content routes need the posts:write / accounts:write scopes, which are off by default. A team admin enables them per key under Team Settings → API. Keys with these scopes can spend your team’s credits — treat them like a payment token.
How adding content works
Both routes feed the same pipeline the app uses. Knowing the order matters, because it decides what you are charged for and when a post becomes readable.
Posts by URL
- Validate. Each link is classified by host. Hosts Siftsy does not process are set aside as
rejected(unsupported_platform/invalid_url) and never reach the credit step. Malformed bodies fail the whole request with400. - Target team. Content lands on the key’s team, or — for Enterprise keys — the sub-team named in
teamId. Anything else is404. The target must be on a plan that includes the API. - Collections.
collections[]refs resolve by id or name before any credit is touched. A name that matches nothing creates a campaign; a name that matches more than one active collection stops the request with409 collection_name_ambiguous. - Replay check. If an identical request (same team, links, collections, tags, credits, capture) has a job within the retention window, that job is returned with
200andmeta.replayed: true. Nothing runs again. - Dedupe. Links are normalized and checked against the team’s existing posts. A link already on the team is
skipped(already_complete/already_processing); the same link twice in one call isduplicate_in_batch. Collections and tags are still applied to skipped posts. Nothing is charged. - Fund check. Siftsy estimates the batch cost (1 credit per post, 5 with
capture.fullRead) against the team’s remaining MAPs and Ad Hoc credits. If none of it can be funded, the request returns402and no post is created. This is why a402never leaves half a batch behind. - Reserve and create. For each remaining link, in order: reserve one post’s worth of credits (MAPs first, then Ad Hoc — or Ad Hoc only with
credits.source: adhoc), then create the post withstatus: fetching, stampedaddedVia: api. If credits run out partway, the rest of the batch isrejectedwithinsufficient_creditsand the job still succeeds for the posts that were funded. A post that fails to create isrejectedwithcreate_failedand its reservation is returned. - Queue. Created posts are handed to the processing queue and the request returns
202with the job. Everything up to here happened inside the request, soaccepted[],skipped[], andrejected[]are final at this point. - Process. Workers fetch the post and its comments, run scoring, and move the post to
complete. This is the part you follow on the job.
Public accounts
Validation, target team, collections, and the replay check run inside the request, then the account is saved on the team (as when you add an account in the app) and the job is returned immediately as discovering. A worker takes it from there:
- Discover. Siftsy pulls the account’s recent public posts from its data provider, newest first, honoring
since/until,limit, andkeywordsat the source. The pull is retried up to three times on provider errors; if it still fails, the job isfailedwith anerrorand nothing is charged. - Filter.
minViews,minLikes, andpostTypeare applied to what came back, duplicates are dropped, and the list is cut tolimit.filterson the job echoes exactly what was applied. WithdryRun: truethe job completes here:discovered[]lists the matches andcredits.estimatedis what adding them would cost. - Add. The discovered links go through the same dedupe → fund check → reserve + create → queue path as a posts request. Posts already on the team are
skipped; if the batch cannot be funded at all the job isfailedwitherror.codenaming why (insufficient_credits,no_active_plan, …); partial funding gives apartialjob. The job moves tosyncingand settles like a posts job.
An account that matches nothing completes with counts.discovered: 0 and no charge.
What a post goes through
accepted[].postStatus on the job, and status on the post itself, follow the app’s processing states:
The job’s credits block is the rollup of that column: reserved counts accepted posts, settled those that reached complete, refunded those that ended in a failure state. Poll GET /content/jobs/{id} at pollAfterMs; each read refreshes those counts from the posts and moves the job from syncing to completed, partial, or failed. Once a job is terminal it never reopens.
For a pipeline, the simplest loop is: POST, then poll the job until it is terminal, then read accepted[].postId with GET /posts/{id}/analysis. If you sync on a schedule instead, the new posts will simply show up in GET /posts?updated_since=… once they are complete, tagged addedVia: api.
Add posts by URL
Up to 100 links per call, on TikTok, Instagram, YouTube, Facebook, LinkedIn, X, Reddit, or Pinterest. The reserve-and-create step runs inside the request, so the per-link result is final when you get the 202; what remains is processing, which you follow on the job.
Collections: by id or by name
collections takes a list of refs. Use { "id": "col_…" } for a collection you already know, or { "name": "…" }:
- If exactly one active collection on the team has that name (case-insensitive), the posts go there.
- If none does, Siftsy creates a campaign with that name and reports
created: trueon the job. - If more than one does, the request fails with
409 collection_name_ambiguousanderror.candidateslisting the ids, before anything is charged. Retry with an id.
To create a nested group instead of a campaign: { "name": "Wave 1", "type": "group", "parentId": "col_launch" }.
Tags
tags are plain strings. They are trimmed, a leading # is dropped, and duplicates are merged case-insensitively. Tags are also merged onto posts that were already on the team (skipped), so re-adding a link with a new tag is a cheap way to tag it.
Credits and capture
If the team cannot fund any of the links, the response is 402 and nothing is created. If it can fund some, the request succeeds and the unfunded links show up in rejected[] with reason: insufficient_credits.
Retries are safe
An identical request (same team, links, collections, tags, credits, capture) within the job retention window returns the existing job with 200 and meta.replayed: true instead of charging twice. Change any of those and it is a new job.
Add a public account
Siftsy discovers the account’s recent public posts, applies your filters, and adds what matches — the same as adding an account in the app. Discovery takes a little while, so this always runs as a job: it starts discovering, moves to syncing once posts are created, and settles like a posts job.
Supported: TikTok, Instagram, YouTube, Facebook pages, LinkedIn, X.
Filters
These are the account filters the app offers. You cannot hand-pick posts beyond them — if you know which posts you want, use POST /content/posts/add.
Price it first with dryRun
Add "dryRun": true and the job completes without adding or charging anything. discovered[] lists the posts that matched and credits.estimated is what adding them would cost:
Refresh posts already on the team
Comments keep arriving after a post is added. Resync in the app re-fetches them and re-scores the post; POST /content/posts/refresh does the same programmatically, for up to 100 posts by id and/or URL.
Two rules make this predictable:
A post is refreshed at most once every 12 hours through the API (and through Siftsy Automations). A post refreshed more recently — by anyone, in the app or by API — is skipped with reason: min_interval and a nextRefreshAt. It is never charged. Schedule refreshes twice a day at most; anything more frequent just returns skips.
It spends the same credits as the app, by the same rules. When the plan’s included refresh cadence (hourly, daily, or weekly depending on plan) allows it, the refresh is free — creditSource: included, creditCost: 0. Otherwise it costs one MAP or Ad Hoc credit (creditSource: credit), or five with capture.fullRead. There is no separate refresh credit, and credits.source picks auto or adhoc as on the other content routes.
Posts that need a credit the team cannot fund are rejected with insufficient_credits; if nothing could be refreshed for that reason the response is 402. Posts still processing are skipped with already_processing. Ids or URLs that are not on the team come back rejected (not_found / not_on_team) — the API never reveals other teams’ posts.
Unlike adding posts, identical refresh requests are not replayed: the 12‑hour floor already makes a repeated call free. Accepted posts reprocess, bump dataVersion, and surface in GET /posts?updated_since=… when done — so a warehouse can refresh its stale posts and pick up the new scores on the next incremental sync.
Follow the job
Poll at meta.pollAfterMs (default 5 s) while the job is discovering, queuing, or syncing. Each read refreshes counts.syncing / complete / failed from the posts’ processing status and settles the job:
credits on the job is an accounting view: reserved for accepted posts, settled for the ones that finished, refunded for accepted posts that could not be processed (Siftsy returns those credits, as in the app).
GET /content/jobs?status=syncing lists recent jobs (summaries, newest first) for the team; jobs are kept for 30 days.
Reasons you will see
Rate limits
Credits, not requests, are the real governor here; these caps only stop a runaway loop. See Rate limits.