Mirsad Partner API
Send Mirsad the Instagram posts your creators publish for a campaign. Mirsad reads each post's numbers every day until the campaign ends, and you pull them into your dashboard. Creators can also sign in with Instagram, so you get their private insights and stories too.
Keys and authentication
Create keys yourself: sign in to portal.mirsad.si as an owner or admin of your organisation and open API keys. A key is shown once; Mirsad stores only a fingerprint of it, so copy it straight into your secret store. You can revoke a key on the same page at any time.
| Access | Scopes | Allows |
|---|---|---|
| Read only | tracking.read | Every GET |
| Read and write | tracking.read, tracking.write | Also register and stop posts, create sign-in links, disconnect creators |
Send the key on every request. Keys are 43 characters and start with mk_.
Authorization: Bearer mk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Conventions
- JSON only. Send
Content-Type: application/jsonwith request bodies (at most 10 KB). Responses carryCache-Control: no-store. - Your own ids. Every post and sign-in carries your
external_campaign_idandexternal_creator_id(1–100 printable characters, no spaces), so you never need to map Mirsad's ids back. - Missing is
null, never 0. If Instagram does not show a number (likes hidden by the creator, views on a photo), the field isnull. Treatnullas "unknown", not as zero. - Times are ISO 8601 in UTC (
2026-10-20T19:57:10.226Z). Dates (campaign_ends_on,from,to) are calendar dates in Baku time (UTC+4). - Lists are newest first and paged with
limit(1–200, default 50) and an opaquecursor: pass backnext_cursoruntil it isnull. - Only your data. Every answer contains only your organisation's own posts and creators. An id from another organisation is a 404.
Quick start
Three calls from registration to numbers. The figures are an example. Put your key in an environment variable first:
export MIRSAD_KEY='mk_…'
1. Register the post when the creator publishes:
curl -s -X POST https://api.mirsad.si/v1/tracked-posts \
-H "Authorization: Bearer $MIRSAD_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://www.instagram.com/reel/XXXXXXXXXXX/","username":"creator.username","external_campaign_id":"spring-sale","campaign_ends_on":"2026-10-31"}'2. About 10 minutes later, read it back. The status is now active and latest holds the first reading:
{
"id": "cc995e67-2873-4e7a-9e74-3847305b8132",
"status": "active",
"media_type": "reel",
"posted_at": "2026-10-06T12:28:33.000Z",
"latest": { "captured_at": "2026-10-20T19:57:10.226Z", "views": 268, "likes": 7, "comments": 0, "followers": 666 }
}3. Show the campaign with one call: GET /v1/campaigns/spring-sale/summary.
How tracking works
- You register a post with its link, the creator's Instagram username, your campaign id and the campaign's last day. It starts as pending.
- First reading within about 10 minutes. Mirsad finds the post among the creator's recent posts and stores its numbers. Status becomes active.
- One reading a day at 04:30 Baku time, kept forever as a history (not overwritten). That is what
/metricsreturns. - A final reading on the first run after
campaign_ends_on; then status becomes ended and reading stops.
Public readings come from Instagram's official Business Discovery API: views (Reels only), likes, comments and the creator's follower count. Shares, saves, reach and stories need the creator to sign in.
status_note says "not found yet: …" so you can fix it early. Ask the creator to switch to a Creator account (free, in Instagram's settings), then register the post again.Links that work: https://www.instagram.com/p/<code>/, /reel/<code>/, /reels/<code>/, /tv/<code>/, with or without the username in the path; tracking parameters such as ?igsh= are ignored. Stories and profile links are refused.
POST/tracked-posts tracking.write
Register a post for tracking until the campaign ends.
| Field | Required | Notes |
|---|---|---|
url | yes | Instagram post or Reel link |
username | yes | The creator's Instagram username, with or without @. Must match the username in the link if the link has one |
external_campaign_id | yes | Your campaign id |
external_creator_id | no | Your creator id |
campaign_ends_on | yes | YYYY-MM-DD, today or later, at most 365 days ahead |
| Answer | When |
|---|---|
| 201 | Registered; returns the post with status pending |
| 200 | The same post was already registered for the same campaign: the existing record. Retries are safe |
| 409 | The post is already tracked under another campaign (the answer includes its id and external_campaign_id) |
| 422 | Invalid body, unsupported link, past or too-far end date, username mismatch |
A post that ended not_found without any reading can simply be registered again with the corrected username: it starts fresh.
GET/tracked-posts tracking.read
Your tracked posts, newest first. Filters: external_campaign_id, status; paging: limit, cursor.
curl -s "https://api.mirsad.si/v1/tracked-posts?external_campaign_id=spring-sale&limit=50" \ -H "Authorization: Bearer $MIRSAD_KEY"
{ "data": [ /* posts, as in GET /tracked-posts/{id} */ ], "next_cursor": null }GET/tracked-posts/{id} tracking.read
One post with its latest reading.
{
"id": "cc995e67-2873-4e7a-9e74-3847305b8132",
"platform": "instagram",
"url": "https://www.instagram.com/reel/XXXXXXXXXXX/",
"username": "creator.username",
"external_campaign_id": "spring-sale",
"external_creator_id": null,
"campaign_ends_on": "2026-10-31",
"status": "active",
"status_note": null,
"media_type": "reel",
"posted_at": "2026-10-06T12:28:33.000Z",
"created_at": "2026-10-20T19:56:51.116Z",
"last_checked_at": "2026-10-20T19:57:10.226Z",
"latest": { "captured_at": "2026-10-20T19:57:10.226Z", "views": 268, "likes": 7, "comments": 0, "followers": 666 },
"latest_insights": null // filled when the creator has signed in
}GET/tracked-posts/{id}/metrics tracking.read
Every reading of one post, oldest first: the daily history for a chart.
| Parameter | Notes |
|---|---|
from, to | Optional dates (Baku), both inclusive |
source | business_discovery (default): public readings. ig_login: the creator's own insights, when they have signed in |
{
"post": { /* the post */ },
"source": "business_discovery",
"data": [
{ "captured_at": "2026-10-20T19:57:10.226Z", "views": 268, "likes": 7, "comments": 0, "followers": 666 },
{ "captured_at": "2026-10-21T00:30:17.609Z", "views": 268, "likes": 7, "comments": 0, "followers": 666 }
]
}With source=ig_login each reading has views, reach, likes, comments, saves, shares, total_interactions and, for Reels, avg_watch_time_ms and total_watch_time_ms. Instagram can lag up to 48 hours on these.
GET/campaigns/{external_campaign_id}/summary tracking.read
Totals for a campaign over each post's latest reading, plus the posts themselves. Totals always cover every post; data lists the first 500 (oldest first) and data_complete says whether that is all of them.
{
"external_campaign_id": "spring-sale",
"posts": 2, "posts_with_readings": 2,
"by_status": { "active": 2 },
"totals": { "views": 290, "likes": 20, "comments": 5 },
"posts_with_hidden_likes": 1, // their likes are not in totals.likes
"last_captured_at": "2026-10-21T00:30:17.609Z",
"insights": { "posts_with_insights": 0, "totals": { "views": 0, "reach": 0, "saves": 0, "shares": 0, "total_interactions": 0 }, "last_captured_at": null },
"data": [ /* posts */ ],
"data_complete": true
}The numbers above are an illustration of the shape. When you show totals, say how many posts have hidden likes rather than presenting totals.likes as complete.
DELETE/tracked-posts/{id} tracking.write
Stops tracking a pending or active post (status stopped). Its readings are kept and still readable. A post that already ended is returned unchanged.
How Instagram sign-in works
Public numbers have no reach, saves, shares or stories. When a creator signs in with Instagram, Mirsad also reads their own insights for their tracked posts once a day, and their stories every hour while they are live. The creator sees Instagram's consent screen for your organisation's app and can disconnect at any time.
POST /connect-links answers 409 "Instagram sign-in is not set up for this organisation yet". Meta must also approve the app (App Review) before creators outside the test list can sign in.- Your server asks Mirsad for a one-time sign-in link for a creator.
- The creator opens it in their phone's browser (where Instagram is logged in), sees a short Mirsad page, then Instagram's consent screen.
- Mirsad sends them back to your
return_urlwith the result in the query string, and sends your server a notification. - From then on
latest_insightsis filled on that creator's tracked posts, and stories appear under the connection.
POST/connect-links tracking.write
| Field | Notes |
|---|---|
external_creator_id | Your creator id (required) |
username | The creator's Instagram username (required). Only this account can complete the sign-in, so a forwarded link cannot connect someone else |
return_url | Where the creator lands afterwards (required, https). Must start with an address your organisation registered with Mirsad |
curl -s -X POST https://api.mirsad.si/v1/connect-links \
-H "Authorization: Bearer $MIRSAD_KEY" -H "Content-Type: application/json" \
-d '{"external_creator_id":"cr-1042","username":"creator.username","return_url":"https://partner.example/creators/connected"}'{
"id": "…",
"url": "https://portal.mirsad.si/connect/instagram?t=…",
"external_creator_id": "cr-1042",
"username": "creator.username",
"expires_at": "2026-10-21T12:00:00.000Z" // valid one hour, one use
}Send the creator the url (in your app, by message or by email). Add &lang=az or &lang=ru for the page's language. The creator must finish in the same browser where they opened the link.
Back on your site: return_url parameters
| Parameter | Values |
|---|---|
mirsad_connect | connected or error |
connection_id, username | On success |
external_creator_id | Always |
reason | On error, see below |
| reason | What happened | What to tell the creator |
|---|---|---|
denied | They declined on Instagram's screen | Ask again if they agree |
account_mismatch | They signed in with another account than username | Sign in with the right account (new link) |
account_in_use | That Instagram account is already connected for another of your creators | Check your creator records |
not_professional | Personal account | Switch to a Creator account, then use a new link |
permissions_missing | They unticked a required permission | Use a new link and keep both permissions |
browser_mismatch | Finished in another browser than the one that opened the link | Open the same link again in one browser (the link is still valid) |
failed | Instagram or Mirsad error | Try again with a new link |
Treat the redirect as a hint for your UI only. Confirm the result from your server with GET /connections?external_creator_id=… or the notification.
GET/connections · /connections/{id} tracking.read
Sign-ins, newest first. Filters: external_creator_id, status; paging as usual.
{
"id": "…", "platform": "instagram", "external_creator_id": "cr-1042",
"status": "active", "end_reason": null,
"ig_user_id": "…", "username": "creator.username", "account_type": "creator",
"scopes": ["instagram_business_basic", "instagram_business_manage_insights"],
"connected_at": "…", "ended_at": null, "token_expires_at": "…",
"last_insights_at": "…", "last_stories_at": "…"
}Mirsad renews the creator's Instagram token automatically. A connection becomes expired only if Instagram stops accepting it (for example the creator changed their password); then send a new sign-in link.
GET/connections/{id}/stories tracking.read
Stories read while they were live, newest first (at most 200; optional from/to posting dates). Instagram shows story insights only for 24 hours, so stories published before the creator connected are not available. Each story has media_id, media_type, permalink, posted_at, latest and all hourly readings with reach, views, replies, link_clicks, shares, total_interactions, follows, profile_visits. A story with fewer than 5 viewers has no numbers (Instagram withholds them).
DELETE/connections/{id} tracking.write
Disconnects the creator: their token, account details and every reading that came from the sign-in (insights, stories) are deleted. Public readings stay. The same happens when the creator removes the app in Instagram or asks Meta to delete their data.
Notifications (connection.event)
Give Mirsad an https address and Mirsad POSTs sign-in changes to it: connection.connected, connection.expired (send a new link), connection.revoked (sign-in data deleted). Mirsad gives you a notify secret to verify them.
{
"id": "evt_…",
"type": "connection.connected",
"created_at": "…",
"data": { "connection": { /* as in GET /connections/{id} */ }, "replaces": ["…"] }
}Headers: Mirsad-Event-Id and Mirsad-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">. Verify the signature over the raw body, reject a t older than 5 minutes, and de-duplicate on the event id: delivery is at least once, retried with backoff for about a day until you answer 2xx.
import { createHmac, timingSafeEqual } from "node:crypto"; // rawBody: the request body exactly as received (a Buffer or string), before JSON.parse export function verifyMirsad(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? ""); return a.length === b.length && timingSafeEqual(a, b); }
Statuses
Tracked post
| Status | Meaning | Read again? |
|---|---|---|
pending | Registered, not read yet. status_note may say "not found yet: …" | Yes, every 10 minutes during the first day, then daily |
active | Read daily | Yes |
ended | Final reading after the campaign's end date taken | No |
stopped | You stopped it | No |
deleted | Gone from the creator's posts on two daily checks in a row | No |
not_found | Instagram has no Creator/Business account with that username (unknown, renamed or personal), or the post was not among their recent posts, on two daily checks | No; register again with the right username |
private | The account is no longer public | No |
Connection
| Status | Meaning |
|---|---|
active | Insights read daily, stories hourly |
expired | Instagram no longer accepts the token. Readings kept; send a new sign-in link |
replaced | The creator signed in again; the new connection takes over |
revoked | Disconnected; the sign-in's data was deleted (end_reason says why) |
Errors
Errors are JSON: {"error": "…"}, sometimes with details (a list of field problems) or extra fields.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Bad query, cursor or JSON | Fix the request |
| 401 | Missing, malformed, unknown or revoked key | Check the key; create a new one in the portal if it was revoked |
| 403 | The key lacks the scope, or post tracking is not enabled for your organisation | Use a read-and-write key; contact Mirsad |
| 404 | No such post or connection (or not yours) | Check the id |
| 409 | Post tracked under another campaign; sign-in not set up yet | See the message |
| 413 / 415 | Body over 10 KB / not JSON | Send JSON with Content-Type: application/json |
| 422 | Validation failed (details lists the fields) | Fix the fields |
| 429 | More than 30 requests in a minute for this key, or many failed keys from your address | Wait for Retry-After seconds |
| 5xx | Mirsad error | Retry with backoff; registration is safe to retry |
Integration checklist
- Create a read-and-write key in the portal and store it in your server's secret store.
- When a creator's approved content goes live, call
POST /tracked-postswith your campaign and creator ids and the campaign's last day. Retry on network errors; a repeat is answered with the same record. - Check that the creator's account is a Creator or Business account before the campaign starts; watch for
status_note"not found yet". - Refresh your dashboard from
/campaigns/{id}/summary(and/metricsfor charts) once a day after 04:30 Baku time. 30 requests a minute is plenty for that; page withcursorinstead of many small calls. - Show
nullas "not shown by Instagram", never as 0. - For sign-in (when enabled): create links server-side, handle every
reason, verify notifications, and offer creators a disconnect button that callsDELETE /connections/{id}.