This page lists every endpoint of the MetricPeek REST API. It is generated from the same source as the OpenAPI description, which API tools and code generators can read directly, and from the same operations an AI assistant uses over MCP. How to create an API key and how requests, errors and limits work is explained in REST API, and every error code in API errors.
Before you start
- Every path below is under
/api/v1on the address of the app. - Every request sends a team API key in the
Authorizationheader as a Bearer token. Only the team owner creates keys, in Settings → API & MCP, and API & MCP access has to be on for the team. - Each endpoint names the access level the key needs: read, write, publish or spend. A key without it is refused.
- Query parameters are for
GETrequests. Every other request sends its parameters as a JSON object in the body, never in the query string. Idempotency-Keyis a header of your choosing. Repeating a call with the same key returns the first result instead of doing the work again. Endpoints that spend credits require it.- A parameter the endpoint does not take is refused, never ignored.
- A name in braces in a path stands for an identifier the API returned. Paths that differ only in that name, such as
/posts/{id}and/posts/{post_id}, are the same address.
Account
The team this key works for, its capabilities, credit balance and plan limits.
Show the team and access of this API key
GET /me
Returns the person this API key acts for, the team it works in, that person's role in the team, the capabilities the key has, and the team's timezone and content language. The capabilities are the effective ones, after the key's access level, the person's role and the operator's switches, so an operation that needs a listed capability is not refused for lack of it. The credit balance and the plan limits are not part of this answer and come from GET /credits and GET /limits. This call never counts against the team's daily allowance of free calls.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
No parameters.
Get the team's credit balance
GET /credits
Returns the team's credit balance, the credits used today by the whole team and the part of them spent through API keys and MCP connections, both counted in the team's timezone, and the daily credit limits that apply to this key and to the team. Beside them come the credits reserved today, for this key and for all the team's keys and connections together: a profile report whose tracking was switched on through the API or MCP counts one refresh against the daily credit limits until midnight in the team's timezone, although nothing is charged for it. The balance is left out, rather than sent as zero, when the role of the person the key acts for may not see it. This call changes nothing, credits cannot be bought through the API, and it never counts against the team's daily allowance of free calls.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
No parameters.
Get the team's plan limits
GET /limits
Returns the team's current plan and its limits: connected social accounts, posts a month, seats, storage, how long analytics data stays available, how many profiles can be compared and how many free calls a day the plan allows, together with whether the plan includes API and MCP access. A limit of -1 means unlimited, while 0 means none. The answer also says how many free calls the team has left today. This call changes nothing and never counts against that allowance.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
No parameters.
Jobs
Background work started by other endpoints.
Get the status of a background job
GET /jobs/{job_id}
Returns the state of a background job started by another endpoint, named in the Location of its 202 answer and in its data.job_id: data.state is pending, running, succeeded, partially_succeeded or failed, and data.finished turns true once the job has ended. A finished job names what it produced, such as the report, the new media item, the hashtags of a search with the credits it finally charged, the files of a profile media download, each file of an upload batch, or each account's result of a publication as the network confirms it. Long results come a page at a time, and text written by the accounts being analysed is in untrusted_content. A job of another team answers 404 like one that does not exist, and asking about a job never counts against the team's daily allowance of free calls.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
job_id |
path | string | yes | The job id returned by the tool that started the work, in the form "<type>:<uuid>", where type is one of profile_report, hashtag_report, social_post, hashtag_search, media_import or content_fetch. |
cursor |
query | string | no | The cursor returned in page.cursor of a previous call, to read the next page of a finished hashtag search or profile media download. |
limit |
query | integer | no | How many hashtags of a finished search, or items of a finished profile media download, to return, 1 to 50. Defaults to 20. |
Reports
Profile and hashtag analytics reports.
List reports
GET /reports
Lists the analytics reports the team already has, newest first, profile reports and hashtag reports alike. Each entry gives the report's id, kind, subject and status, when its data was last refreshed and until when it stays available, but not the report contents, which GET /reports/{type}/{id} returns. Filters narrow the list by kind, platform, status, tracking, or a part of the profile name or hashtag. Listing reports charges nothing.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
type |
query | string: profile, hashtag |
no | Return only this kind of report. Left out, both kinds are returned. |
platform |
query | string: instagram, tiktok |
no | Return only reports about this platform. |
status |
query | string: pending, ready, error |
no | Return only reports in this state. |
tracked_only |
query | boolean | no | When true, return only reports whose periodic refreshing is on. |
query |
query | string | no | Return only reports whose profile name or hashtag contains this text. |
cursor |
query | string | no | The cursor returned in page.cursor of a previous call, to read the next page. |
limit |
query | integer | no | How many reports to return, 1 to 50. Defaults to 20. |
Get a report
GET /reports/{type}/{id}
Returns one report of the team. By default the answer is a summary: the profile or hashtag analysed, the headline engagement figures, the 0-100 score with its components, the top hashtags and words, the best time to post, and when the data was measured. Larger parts are added only when named in sections, the summary's sections_available lists the ones this report holds, and a long section comes a page at a time. Captions, biographies and other text written by the analysed account are in untrusted_content, and reading a report charges nothing, because it was paid for when it was created.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
type |
path | string: profile, hashtag |
yes | Which kind of report to read: profile or hashtag. |
id |
path | string | yes | The report id, as GET /reports returns it. |
sections |
query | list of strings | no | Names of the larger parts to include, as listed in the summary's sections_available. Left out, only the summary is returned. |
cursor |
query | string | no | The cursor returned in page.cursor of a previous call, to read the next page of a paged section. |
limit |
query | integer | no | How many items of a paged section to return, 1 to 50. Defaults to 20. |
Create a report
POST /reports
Creates a new analytics report and answers 202 with a Location pointing at the job, which names the report once it is ready to read with GET /reports/{type}/{id}. A profile report analyses one public Instagram or TikTok profile, given as a username, an @handle or a profile link, and a hashtag report analyses one Instagram hashtag. A report the team already has, or one still being generated, is returned without a charge, and a report whose generation fails is refunded. An Idempotency-Key is required, and a repeat with the same key returns the same report without a second charge.
- Access level: spend, this endpoint spends the team's credits
- Also needs: write. Always: the new report becomes an item the team keeps.
- Idempotency-Key: required
- Success:
202 Accepted, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
type |
body | string: profile, hashtag |
yes | "profile" for an Instagram or TikTok profile, "hashtag" for an Instagram hashtag. |
platform |
body | string: instagram, tiktok |
no | instagram or tiktok. Hashtag reports exist for instagram only. Defaults to instagram. |
subject |
body | string | yes | The username (profile) or the hashtag (hashtag). |
Refresh a report
POST /reports/{type}/{id}/refresh
Fetches current data for a report the team already has and moves its data window forward, answering 202 with a Location pointing at the job, which says when the refresh has finished. A refresh that could not fetch anything is refunded. The same person can refresh a report again only after a short interval, shared with the app, and an earlier call is refused with 429 and Retry-After. An Idempotency-Key is required, and a repeat with the same key returns the same refresh without a second charge.
- Access level: spend, this endpoint spends the team's credits
- Idempotency-Key: required
- Success:
202 Accepted, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
type |
path | string: profile, hashtag |
yes | Which kind of report the id names. |
id |
path | string | yes | The report id, as GET /reports returns it. |
Turn report tracking on or off
PUT /reports/{type}/{id}/tracking
Turns the automatic refreshing of a report on or off, or changes how often it runs, and answers at once with the report's tracking state, including the next automatic refresh and what each one charges. While tracking is on, every automatic refresh is charged to the team with no further call, until tracking is turned off. Switching tracking on charges a hashtag report at once and a profile report nothing at once, while switching it off or changing the frequency of a tracked report charges nothing. Switching tracking on through the API counts one automatic refresh against the key's and the team's daily credit limits until midnight in the team's timezone. For a hashtag report that is the charge made at once, and for a profile report, where nothing is charged at once, it is counted all the same and switching tracking off again gives it back. The team's plan limits how many reports can be tracked at the same time, and an Idempotency-Key is required, although a repeated switch is recognised as the same switch and charged once.
- Access level: spend, this endpoint spends the team's credits
- Repeated cost: this can also commit the team to credits charged again later, until it is turned off. The OpenAPI description gives the amount in
x-metricpeek-recurring-price. - Idempotency-Key: required
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
type |
path | string: profile, hashtag |
yes | Which kind of report the id names. |
id |
path | string | yes | The report id, as GET /reports returns it. |
enabled |
body | boolean | yes | true to track the report, false to stop tracking it. |
frequency_days |
body | integer: 1, 3, 7 |
no | How often the automatic refresh runs, in days: 1, 3 or 7. Defaults to 3. |
Social accounts
The social accounts connected to the team.
List connected social accounts
GET /social-accounts
Lists the social accounts connected to the team, with their id, platform, username, whether the account is active and the state of its connection. By default only accounts with a live connection are listed, and active_only=false adds the others, which is where an account that needs reconnecting shows up. The ids are the ones posts, account analytics and GET /connect-link take. Nothing is connected or disconnected here.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
platform |
query | string | no | Return only accounts on this platform, for example instagram, tiktok, facebook, linkedin, pinterest, bluesky, threads, youtube or x. |
active_only |
query | boolean | no | When true (the default) only accounts whose connection is live are returned. |
cursor |
query | string | no | The cursor returned in page.cursor of a previous call, to read the next page. |
limit |
query | integer | no | How many accounts to return, 1 to 50. Defaults to 20. |
Get the analytics of a connected account
GET /social-accounts/{account_id}/analytics
Returns how the posts of one connected account performed over a period of days: reach, engagement, the best performing post, the publishing cadence and the breakdown by content format. The figures are the ones the analytics screen of the app shows, computed from the account's posts as they were collected from the network in the background, so this call reaches no network and covers connected accounts only. An empty answer says why in notes, for example that nothing has been collected yet or that no post falls inside the period. A team whose plan does not include social analytics is refused.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
account_id |
path | integer | yes | The id of a connected account, as GET /social-accounts returns it. |
period_days |
query | integer | no | How many days back to measure. Defaults to 30. |
Get a link to connect or reconnect a social account
GET /connect-link
Returns a link to a page in MetricPeek where the person the key acts for connects a new social account to the team, when platform is given, or reconnects an account whose connection stopped working, when account_id is given. Nothing is connected until that person confirms in a signed-in browser, and the page asks before it switches the browser to this team. For a new connection the answer also gives slots_left: how many more accounts the plan allows, 0 when none is free and null when the plan sets no limit. An account whose connection still works is refused, because reconnecting it would disconnect it first.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
platform |
query | string: instagram, facebook, tiktok, tiktok_business, youtube, linkedin, x, threads, pinterest, bluesky |
no | The platform to connect a new account on. Give this or account_id, not both. |
account_id |
query | integer | no | The id of a connected account whose connection stopped working (GET /social-accounts shows it inactive, with connection_probe refused, or with account_status needs_reauth), to reconnect it. Give this or platform, not both. |
Posts
Drafts, scheduled posts and published posts.
List posts
GET /posts
Lists the team's posts, newest first, with their status, their scheduled or published time in the team's timezone and in UTC, the accounts they target and the start of their text. Filters narrow the list by status, by account and by a time range, and a time without a UTC offset is read in the team's timezone. The full post, with its media, its per-account results and its per-account overrides, comes from GET /posts/{id}.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
status |
query | string: draft, scheduled, processing, published, partially_published, failed |
no | Return only posts in this status. |
account_id |
query | integer | no | Return only posts targeting this connected account. |
from |
query | string | no | ISO 8601 lower bound on the scheduled or published time. Without a UTC offset it is read in the team's timezone. |
to |
query | string | no | ISO 8601 upper bound on the scheduled or published time. Without a UTC offset it is read in the team's timezone. |
cursor |
query | string | no | The cursor returned in page.cursor of a previous call, to read the next page. |
limit |
query | integer | no | How many posts to return, 1 to 50. Defaults to 20. |
Get a post
GET /posts/{id}
Returns one post of the team in full: its whole text, its media, every account it targets with that account's own result, and the per-account overrides of text or media that show what goes out on each network. Post text written by the team is returned as data. A failed account carries an error_code and a short explanation, while the network's own error text is in untrusted_content.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | The uuid of one post. Given, the response carries that post in full instead of the index. |
Check a post against the platform rules
GET /posts/{post_id}/validation
Checks a post against the rules of every network it targets and returns the problems found, each with a code and whether it blocks publishing. The answer also says whether the post is waiting for the client's approval and how much of the team's monthly post allowance is left, and account_id limits the check to one of the post's accounts. Nothing is changed and nothing is sent to any network.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
post_id |
path | string | yes | The uuid of the post, as GET /posts returns it. |
account_id |
query | integer | no | Check the post only for this one of its accounts. |
Create a post draft
POST /posts
Creates a draft post for one or more of the team's connected accounts, with its text and, optionally, media from the Media Library, and answers 201 with the post and a Location pointing at it. Nothing is sent to any network until the post is scheduled or published. A text longer than MetricPeek accepts is refused rather than shortened. A retry with the same Idempotency-Key returns the draft the first call created instead of making a second one.
- Access level: write
- Idempotency-Key: optional
- Success:
201 Created, withLocationpointing atGET /posts/{id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
caption |
body | string | yes | The text of the post. |
account_ids |
body | list of integers | yes | Ids of the connected accounts the post is for, as GET /social-accounts returns them. |
media_ids |
body | list of integers | no | Ids of media library items to attach, in order. |
Change a draft or scheduled post
PATCH /posts/{id}
Changes the text, publish time, media or target accounts of a draft or scheduled post and returns the post as stored, with the validation problems found after the change. Fields left out stay as they are, while media_ids and account_ids replace the whole set. A change to a scheduled post is passed to the publishing provider at once and goes out as changed, and a change to a post the client approved sends it back to the client for approval. Published, publishing and failed posts cannot be changed, and neither can the template of a recurring series.
- Access level: write
- Also needs: publish. When the post is scheduled: the change is sent to the platform provider and goes out without anyone pressing Publish.
- Idempotency-Key: optional
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | The uuid of the post, as GET /posts returns it. |
caption |
body | string | no | The new text of the post. |
scheduled_at |
body | string | no | The new publish time in ISO 8601, for example 2026-09-29T15:00 or 2026-09-29T13:00:00Z. Without a UTC offset it is read in the team's timezone. |
media_ids |
body | list of integers | no | The complete list of media library item ids the post should carry, in order. |
account_ids |
body | list of integers | no | The complete list of connected account ids the post should go to. |
Cancel a scheduled post
POST /posts/{post_id}/cancel
Takes a scheduled post off the schedule: its scheduled copy is withdrawn from the publishing provider and the post becomes a draft again, with its content kept. A recurring series is paused, not deleted. A post that is already publishing or published cannot be cancelled, and cancelling a draft changes nothing. When the provider does not release the scheduled copy, the post stays scheduled and the call is refused, so a cancelled post never goes out.
- Access level: write
- Idempotency-Key: optional
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
post_id |
path | string | yes | The uuid of the scheduled post, as GET /posts returns it. |
Schedule a post
POST /posts/{post_id}/schedule
Schedules a draft for a given time on every account it targets, and the publishing provider publishes it then with no further confirmation. A time without a UTC offset is read in the team's timezone, a date without a time is refused, and the answer gives the time in the team's timezone and in UTC. A post waiting for the client's approval, a post with blocking validation problems and a team that has used its monthly post allowance are refused. A post that is already scheduled is moved with PATCH /posts/{id} rather than here, and a repeat with the same Idempotency-Key returns the first result.
- Access level: publish
- Idempotency-Key: optional
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
post_id |
path | string | yes | The uuid of the draft, as GET /posts or POST /posts returns it. |
scheduled_at |
body | string | yes | When to publish, in ISO 8601 with a time, for example 2026-09-29T15:00 (team timezone) or 2026-09-29T13:00:00Z. A date without a time is refused. |
Publish a post now
POST /posts/{post_id}/publish
Sends a draft or scheduled post to every account it targets, to be published now, and answers 202 with a Location pointing at the job. Each network confirms its account separately, and the job reports each account's result as it arrives, so the 202 means the post was handed over, not that it was published. A post waiting for the client's approval, a post with blocking validation problems and a team that has used its monthly post allowance are refused, each with its own code. A post that was already sent is never sent again, with or without an Idempotency-Key, and the answer then says already_submitted.
- Access level: publish
- Idempotency-Key: optional
- Success:
202 Accepted, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
post_id |
path | string | yes | The uuid of the post, as GET /posts or POST /posts returns it. |
Retry the failed accounts of a post
POST /posts/{post_id}/retry
Sends a post again to the accounts where publishing failed, or to the listed ones among them, and answers 202 with a Location pointing at the job, which reports each account's new result. Accounts that did not fail, that have used up their retries or that were retried moments ago are skipped and named with the reason. A post waiting for the client's approval is refused. A repeated call never sends an account twice.
- Access level: publish
- Idempotency-Key: optional
- Success:
202 Accepted, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
post_id |
path | string | yes | The uuid of the post, as GET /posts returns it. |
account_ids |
body | list of integers | no | Retry only these of the post's accounts. Left out, every account where publishing failed is retried. |
Media
The team's media library and the ways files get into it.
List media library files
GET /media
Lists the files in the team's Media Library with their id, name, file name, type, size and folder, together with how much of the team's storage is used. Filters narrow the list by folder, with 0 for files in no folder, by images or video, and by a part of the name. The ids are the ones posts take as media_ids, and nothing is uploaded here.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
folder_id |
query | integer | no | Return only files in this folder. Pass 0 for files in no folder at all. |
kind |
query | string: image, video |
no | Return only images, or only video. |
query |
query | string | no | Return only files whose name or file name contains this text. |
cursor |
query | string | no | The cursor returned in page.cursor of a previous call, to read the next page. |
limit |
query | integer | no | How many files to return, 1 to 50. Defaults to 20. |
Import a file from a public link
POST /media/imports
Starts downloading one image or video from a public https address into the team's Media Library and answers 202 with a Location pointing at the job, which gives the new media id once the download has finished. Only public https addresses are fetched, and private, local and numeric addresses are refused before anything is downloaded. The file type is read from the downloaded bytes, not from the address or the name, and the file uses the team's storage, not credits. A file that is already in the library returns the existing media item instead of a second copy.
- Access level: write
- Idempotency-Key: optional
- Success:
202 Accepted, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
url |
body | string | yes | The public https address of the image or video. |
filename |
body | string | no | A name for the file in the library. Only the part before the extension is used; the extension follows the file's real type. |
folder_id |
body | integer | no | The id of one of the team's media library folders, as GET /media returns it. Left out, the file is not put in a folder. |
Create upload links for files
POST /uploads
Opens an upload batch for the team's Media Library and answers 201 with a Location pointing at the batch's job, which reports each file as it arrives or is refused. Every file described in files by its SHA-256 hash and size gets its own PUT link under data.upload.uploads, with a one-time secret that accepts exactly the described bytes, once, before the expiry time in the answer, and data.upload_page_url is a page where a signed-in person can upload the files in a browser instead. The secrets appear in this answer only, and a repeat with the same Idempotency-Key returns the same batch with new secrets while the earlier ones stop working. Uploads use the team's storage, not credits, with the size limits stated in data.limits, and a file already in the library returns the existing media item.
- Access level: write
- Idempotency-Key: optional
- Success:
201 Created, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
files |
body | list of objects | no | Optional. The files that will be uploaded, in the order they should keep, each described by the lowercase hex SHA-256 of its bytes, its size in bytes, its MIME type and its name. |
folder_id |
body | integer | no | The id of one of the team's media library folders, as GET /media returns it. Left out, the files are not put in a folder. |
replaces_batch |
body | string | no | Optional. The job id of an earlier batch from this connection that the new batch replaces. Its files that are still waiting are closed and GET /jobs/{job_id} reports them as replaced by the new batch. Ignored when a file of that batch has already arrived. |
Download media from a public profile
POST /profile-media-downloads
Downloads media of the kind named in type from a public Instagram or TikTok profile and answers 202 with a Location pointing at the job, which returns what was fetched a page at a time, with a cursor for the next page where there is one. By default the files are saved into the Downloads folder of the team's Media Library and use the team's storage, while save_to_library set to false returns only their descriptions, and the networks' own media links are never returned. The charge is per call, whatever the profile turns out to hold, and a fetch that fails because the profile does not exist, is private or could not be read is refunded. An Idempotency-Key is required, and a repeat with the same key returns the same job without a second charge.
- Access level: spend, this endpoint spends the team's credits
- Also needs: write. When save_to_library is true, the default: the files are saved into the team's media library.
- Idempotency-Key: required
- Success:
202 Accepted, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
platform |
body | string: instagram, tiktok |
yes | instagram or tiktok. |
type |
body | string: profile, avatar, posts, reels, stories, highlights, playlists |
yes | What to fetch. Instagram: profile, avatar, posts, reels, stories, highlights. TikTok: profile, avatar, posts, playlists. |
username |
body | string | yes | The account: a username, an @handle or a link to the profile. |
cursor |
body | string | no | The next_cursor of an earlier finished fetch of the same profile, to get its next page. Only profile, posts and reels have further pages. |
limit |
body | integer | no | How many files to take from what was fetched, 1-50 (1 for an avatar). Defaults to 12. |
save_to_library |
body | boolean | no | Save the files into the team's media library. Defaults to true. |
Brand voices
The team's brand voices.
List brand voices
GET /brand-voices
Returns a short entry for every brand voice of the team and the default voice in full: the business description, tone, point of view, audiences, emoji policy, words and phrases to use and to avoid, custom instructions, content language, and one ready-assembled text block that holds all of it. The text block is the same one the app itself uses when it writes captions and carousels. Another voice in full comes from GET /brand-voices/{id}, and the material a voice was researched from is never returned.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
No parameters.
Get a brand voice
GET /brand-voices/{id}
Returns one brand voice of the team in full: the business description, tone, point of view, audiences, emoji policy, words and phrases to use and to avoid, custom instructions and content language, plus the ready-assembled text block the app itself writes with. The material the voice was researched from is never returned.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | integer | yes | The id of one of the team's brand voices. Left out, every voice is returned in short form together with the full text block of the default one. |
Hashtag lists
The team's own hashtag lists and the ready-made ones.
List hashtag lists
GET /hashtag-lists
Lists the hashtag lists the team can use: its own lists and the ready-made lists that come with MetricPeek. Each entry gives the list's name, platform, how many hashtags it holds and whether it is ready-made, and scope picks the team's own lists, the ready-made ones or both. One list with its hashtags comes from GET /hashtag-lists/{list_id}.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
scope |
query | string: own, system, all |
no | Which lists to return: the team's own, the ready-made ones, or both (the default). |
platform |
query | string: instagram, tiktok |
no | Return only lists tied to this platform. |
cursor |
query | string | no | The cursor returned in page.cursor of a previous call, to read the next page. |
limit |
query | integer | no | How many lists to return, 1 to 50. Defaults to 20. |
Get a hashtag list with its hashtags
GET /hashtag-lists/{list_id}
Returns one hashtag list the team can use, one of its own or a ready-made one, with its hashtags and, for each hashtag, its size, tier and score. Each size says whether it was measured on the network or estimated from the hashtags that appear alongside it.
- Access level: read
- Idempotency-Key: not accepted
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
list_id |
path | integer | yes | The id of one visible list. Given, the response carries that list with its hashtags instead of the index. |
Create a hashtag list
POST /hashtag-lists
Creates a hashtag list owned by the team and answers 201 with the new list and a Location pointing at it. Hashtags given in hashtags go in at once, the answer names any that were refused, and a hashtag MetricPeek does not know yet is added and gets its figures later. Each call without an Idempotency-Key creates another list, so a retry sends the same key to get the first list back. Nothing is charged, a team at its plan's limit of saved lists is refused, and ready-made lists cannot be created or changed this way.
- Access level: write
- Idempotency-Key: optional
- Success:
201 Created, withLocationpointing atGET /hashtag-lists/{list_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
name |
body | string | yes | The name of the list, 1-50 characters. |
platform |
body | string: instagram, tiktok |
no | The platform whose metrics the list is about. Left out, the list is not tied to a platform. |
color |
body | string | no | A six-digit hex colour for the list, for example #4F46E5. |
icon |
body | string | no | A Remix icon token for the list, for example ri-folder-fill. |
hashtags |
body | list of strings | no | Hashtag names to put in the new list, with or without a leading #. |
Add hashtags to a list
POST /hashtag-lists/{list_id}/hashtags
Adds hashtags to one of the team's own lists and answers with the hashtags added, the ones already in the list and the ones refused. A hashtag already in the list stays as it is, so repeating the same call changes nothing further. Ready-made lists cannot be changed, and nothing is charged.
- Access level: write
- Idempotency-Key: optional
- Success:
200 OK
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
list_id |
path | integer | yes | The id of one of the team's own lists, as GET /hashtag-lists returns it. |
hashtags |
body | list of strings | yes | Hashtag names to add, with or without a leading #. |
subcategory |
body | string | no | An optional grouping label stored alongside each hashtag in this list. |
Hashtags
Hashtag search.
Search hashtags
POST /hashtags/search
Searches Instagram or TikTok for hashtags matching a keyword or a hashtag and answers 202 with a Location pointing at the job, which returns the hashtags found with their measured or estimated post count, posting rate, tier and 0-100 score, and the hashtags that often appear alongside them. A keyword the team has already paid for on the same platform, wherever it was bought, is served from stored data without a charge while the team's data window on that search is live, and a search that finds nothing is refunded. An Idempotency-Key is required, and a repeat with the same key and the same query returns the same job without a second charge. Nothing is saved to a hashtag list.
- Access level: spend, this endpoint spends the team's credits
- Idempotency-Key: required
- Success:
202 Accepted, withLocationpointing atGET /jobs/{job_id}
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
query |
body | string | yes | A keyword ("bakery kraków") or a hashtag ("#sourdough"), 2-100 characters. A leading # is optional; the order of words in a phrase does not change the price. |
platform |
body | string: instagram, tiktok |
no | Which platform to search. Defaults to instagram. |
What this does not do
- It does not accept an AI assistant's sign-in. Only team API keys work here, and an assistant connects over MCP instead.
- It does not work from a web page. A key in a browser is a leaked key, so cross-origin requests are not allowed.
- It does not delete anything. A cancelled scheduled post becomes a draft again.
- It does not buy credits or change the plan. Both stay in the app.
- It does not connect a social account by itself.
GET /connect-linkreturns a link that a person opens in the app. - It does not take file bytes in a request body. Files arrive through upload links or from a public link.
- It does not call your server when background work finishes. Read the job at its
Locationuntil it has finished.
When something looks wrong
Every request answers 401 with the code unauthorized. The key is missing, mistyped, revoked or expired, or the header is not Authorization: Bearer followed by the key. The answer is the same for each of these on purpose. Check the key in Settings → API & MCP.
A request answers 403 with the code rest_disabled. The REST API is switched off for everyone at the moment. The key itself is fine and needs no change.
A request answers 403 with another code. The code names the reason, for example an access level the key does not have, API & MCP access switched off for the team, or a plan that no longer includes API keys. API errors explains every code.
An endpoint that spends credits answers 422 with the reason idempotency_key_required. Send an Idempotency-Key header: a new key for each new request, and the same key when repeating one.
A request answers 422 with the reason unknown_argument. The endpoint does not take that parameter. Compare its spelling with the tables above.
A request answers 429. Wait the number of seconds in the Retry-After header before repeating it.