For partner developers

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.

Base URL
https://api.mirsad.si/v1
Contract (OpenAPI 3.1)
Auth
Authorization: Bearer mk_…
Rate limit
30 requests / minute / key

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.

AccessScopesAllows
Read onlytracking.readEvery GET
Read and writetracking.read, tracking.writeAlso register and stop posts, create sign-in links, disconnect creators

Send the key on every request. Keys are 43 characters and start with mk_.

Header
Authorization: Bearer mk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Use one key per system (for example one for production, one for staging) and keep keys on your server. Never put a key in a mobile app or in browser code: anyone could read it there.

Conventions

Quick start

Three calls from registration to numbers. The figures are an example. Put your key in an environment variable first:

Shell
export MIRSAD_KEY='mk_…'

1. Register the post when the creator publishes:

Request
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:

Response (shortened)
{
  "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

  1. 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.
  2. First reading within about 10 minutes. Mirsad finds the post among the creator's recent posts and stores its numbers. Status becomes active.
  3. One reading a day at 04:30 Baku time, kept forever as a history (not overwritten). That is what /metrics returns.
  4. 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.

The creator must have a Creator or Business account. Instagram does not show personal accounts to Business Discovery. A post by a personal account (or a wrong username) ends as not_found after two daily checks; until then 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.

FieldRequiredNotes
urlyesInstagram post or Reel link
usernameyesThe creator's Instagram username, with or without @. Must match the username in the link if the link has one
external_campaign_idyesYour campaign id
external_creator_idnoYour creator id
campaign_ends_onyesYYYY-MM-DD, today or later, at most 365 days ahead
AnswerWhen
201Registered; returns the post with status pending
200The same post was already registered for the same campaign: the existing record. Retries are safe
409The post is already tracked under another campaign (the answer includes its id and external_campaign_id)
422Invalid 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.

Request
curl -s "https://api.mirsad.si/v1/tracked-posts?external_campaign_id=spring-sale&limit=50" \
  -H "Authorization: Bearer $MIRSAD_KEY"
Response
{ "data": [ /* posts, as in GET /tracked-posts/{id} */ ], "next_cursor": null }

GET/tracked-posts/{id} tracking.read

One post with its latest reading.

Response
{
  "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.

ParameterNotes
from, toOptional dates (Baku), both inclusive
sourcebusiness_discovery (default): public readings. ig_login: the creator's own insights, when they have signed in
Response
{
  "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.

Response (shape)
{
  "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.

Availability: sign-in is switched on per organisation once your organisation's Meta app is set up with Mirsad. Until then 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.
  1. Your server asks Mirsad for a one-time sign-in link for a creator.
  2. The creator opens it in their phone's browser (where Instagram is logged in), sees a short Mirsad page, then Instagram's consent screen.
  3. Mirsad sends them back to your return_url with the result in the query string, and sends your server a notification.
  4. From then on latest_insights is filled on that creator's tracked posts, and stories appear under the connection.

Back on your site: return_url parameters

ParameterValues
mirsad_connectconnected or error
connection_id, usernameOn success
external_creator_idAlways
reasonOn error, see below
reasonWhat happenedWhat to tell the creator
deniedThey declined on Instagram's screenAsk again if they agree
account_mismatchThey signed in with another account than usernameSign in with the right account (new link)
account_in_useThat Instagram account is already connected for another of your creatorsCheck your creator records
not_professionalPersonal accountSwitch to a Creator account, then use a new link
permissions_missingThey unticked a required permissionUse a new link and keep both permissions
browser_mismatchFinished in another browser than the one that opened the linkOpen the same link again in one browser (the link is still valid)
failedInstagram or Mirsad errorTry 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.

A connection
{
  "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.

Body
{
  "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.

Node.js
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

StatusMeaningRead again?
pendingRegistered, not read yet. status_note may say "not found yet: …"Yes, every 10 minutes during the first day, then daily
activeRead dailyYes
endedFinal reading after the campaign's end date takenNo
stoppedYou stopped itNo
deletedGone from the creator's posts on two daily checks in a rowNo
not_foundInstagram has no Creator/Business account with that username (unknown, renamed or personal), or the post was not among their recent posts, on two daily checksNo; register again with the right username
privateThe account is no longer publicNo

Connection

StatusMeaning
activeInsights read daily, stories hourly
expiredInstagram no longer accepts the token. Readings kept; send a new sign-in link
replacedThe creator signed in again; the new connection takes over
revokedDisconnected; 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.

StatusMeaningWhat to do
400Bad query, cursor or JSONFix the request
401Missing, malformed, unknown or revoked keyCheck the key; create a new one in the portal if it was revoked
403The key lacks the scope, or post tracking is not enabled for your organisationUse a read-and-write key; contact Mirsad
404No such post or connection (or not yours)Check the id
409Post tracked under another campaign; sign-in not set up yetSee the message
413 / 415Body over 10 KB / not JSONSend JSON with Content-Type: application/json
422Validation failed (details lists the fields)Fix the fields
429More than 30 requests in a minute for this key, or many failed keys from your addressWait for Retry-After seconds
5xxMirsad errorRetry with backoff; registration is safe to retry

Integration checklist

  1. Create a read-and-write key in the portal and store it in your server's secret store.
  2. When a creator's approved content goes live, call POST /tracked-posts with your campaign and creator ids and the campaign's last day. Retry on network errors; a repeat is answered with the same record.
  3. Check that the creator's account is a Creator or Business account before the campaign starts; watch for status_note "not found yet".
  4. Refresh your dashboard from /campaigns/{id}/summary (and /metrics for charts) once a day after 04:30 Baku time. 30 requests a minute is plenty for that; page with cursor instead of many small calls.
  5. Show null as "not shown by Instagram", never as 0.
  6. For sign-in (when enabled): create links server-side, handle every reason, verify notifications, and offer creators a disconnect button that calls DELETE /connections/{id}.