IG DMer API
IG DMer automates Instagram comment→DM flows and AI DM replies, publishes reels/photos/carousels, and stores Instagram insights. This API lets agents read all of it and, with a full-access key, do everything the dashboard does: post and schedule content, DM people, build automations.
- Base URL:
https://igdmer.thejonbroadbent.com - Auth: every request needs
Authorization: Bearer <API_KEY>(keys start withigdm_; the owner creates them on the Accounts page).X-API-Key: <API_KEY>also works.GET /api/metells you your key's name andscope. - Scopes:
readkeys can onlyGET(anything else →403).write(full access) keys can call every endpoint below, except key and team management, the webhook log, the dev simulator and disconnecting an account. A key belongs to one workspace and only sees that workspace's Instagram accounts (anything else answers 404). - Format: JSON in and out (except uploads and
contacts.csv). Timestamps are unix milliseconds. Days areYYYY-MM-DD(UTC). - Errors:
{"error": "<message>"}with status400(bad input; validation errors also carryissues[]),401(missing/invalid key),403(not allowed for this key),404,502(Instagram refused; hascode). - Freshness: comments/DMs are live (seconds). Instagram metrics sync daily (posts + account) and every 30 min (stories); Instagram itself can lag up to 48 h.
curl -H "Authorization: Bearer $IGDM_API_KEY" https://igdmer.thejonbroadbent.com/api/accounts
Post a reel
BASE=https://igdmer.thejonbroadbent.com
AUTH="Authorization: Bearer $IGDM_API_KEY"
# 1. Account id
curl -s -H "$AUTH" $BASE/api/accounts # → [{ "id": "…", "username": "the_truth_of_power", … }]
# 2a. Post a file on your disk (upload, then post)
KEY=$(curl -s -H "$AUTH" -H "Content-Type: video/mp4" --data-binary @reel.mp4 $BASE/api/uploads/direct | jq -r .key)
curl -s -H "$AUTH" -H "Content-Type: application/json" -H "Idempotency-Key: reel-2026-11-02-a" $BASE/api/posts \
-d "{\"account_id\":\"$ACCOUNT\",\"r2_key\":\"$KEY\",\"caption\":\"Caption #tag\"}"
# 2b. …or a file that's already online (direct download link)
curl -s -H "$AUTH" -H "Content-Type: application/json" $BASE/api/posts \
-d '{"account_id":"…","media_url":"https://example.com/reel.mp4","caption":"…","scheduled_at":1798000000000}'
# → 201 { "id": "<post id>", "kind": "reel", "status": "queued" | "scheduled", "scheduled_at": null | ms }
# 3. Poll until published (reels take 1–5 min for Instagram to process)
curl -s -H "$AUTH" $BASE/api/posts/<post id> # → status "published" + media_id + permalink, or "failed" + error
Typical agent workflows
- Publish:
GET /api/accounts→ optionalGET /api/posts/best-hours→ upload →POST /api/posts(withscheduled_at) → pollGET /api/posts/:id. - Comment → DM on a new reel: pass
keywords+template_automation_id(an existing automation whose DM to reuse) onPOST /api/posts; the keyword automation is created for that reel when it goes live. - Performance:
GET /api/analytics,GET /api/account-insights,GET /api/content(sort bymetrics.views,rates.engagement_rate…). - Inbox:
GET /api/needs-you→GET /api/contacts/:id/chat→POST /api/contacts/:id/send. - Live activity: poll
GET /api/events?account_id=…&after_id=<last id seen>&limit=1000.
Write endpoints (full-access keys)
Uploads
Files for posts are stored first, then referenced by key (up-<uuid>.mp4|mov|jpg). Unused uploads are deleted after 80 days;
uploads of published posts are deleted once Instagram has them.
POST /api/uploads/direct — the raw file as the request body, up to 100 MB (Cloudflare's request limit).
- Header Content-Type: video/mp4, video/quicktime, image/jpeg or image/png. Or pass ?name=file.mp4 / ?type=video|image.
- Photos: JPEG or PNG (PNG is converted to JPEG on a white background, max 1440 px wide). Aspect must be 4:5 to 1.91:1 or you get 400.
- → 201 { key, type: "video"|"image", size, width?, height? }
POST /api/uploads/from-url { url, type?: "video"|"image" } — the server downloads a public file (up to 1 GB). Must be a
direct file link (a web page returns 400). Same response as above. You can skip this and pass media_url to POST /api/posts.
Big files from disk (over 100 MB): multipart in 20 MB parts (all parts except the last exactly 20 MB):
1. POST /api/uploads { name, type: "video/mp4", size } → { key, upload_id }
2. PUT /api/uploads/<key>/<part 1..n>?upload_id=… with the raw bytes of each part → { part_number, etag }
3. POST /api/uploads/<key>/complete { upload_id, parts: [{ part_number, etag }] } → { key, size }
POST /api/posts
Publish or schedule a post. Header Idempotency-Key: <any string> is strongly recommended: retrying with the same key returns
the first post (200 { id, status, duplicate: true }) instead of posting twice.
| field | type | notes |
|---|---|---|
account_id |
string | required |
kind |
reel | image | carousel |
optional: one video → reel, one photo → image, items → carousel |
r2_key |
string | an upload key (reel / image) |
media_url |
url | instead of r2_key: the server downloads it |
items |
[{ key }] or [{ url }], 2–10 |
carousel slides, photos and/or videos (type optional) |
caption |
string ≤ 2200 | max 30 hashtags |
scheduled_at |
ms | null | null/past = post now. Up to 75 days ahead |
share_to_feed |
bool, default true | reels only |
thumb_offset_ms |
int | reels: cover frame |
trial_strategy |
MANUAL | SS_PERFORMANCE | null |
reels: post as a trial reel (non-followers first). MANUAL = you graduate it in the app |
keywords |
string[] ≤ 10 | comment keywords that trigger a DM on this post |
template_automation_id |
string | required with keywords: whose DM flow to send |
file_name |
string | label only |
→ 201 { id, kind, status: "queued"|"scheduled", scheduled_at }. Statuses: scheduled → queued → processing →
published | failed | cancelled.
Not possible through Instagram's API: stories (not built here), music from Instagram's library, stickers, collabs, editing anything but the caption after posting.
GET /api/posts/:id · GET /api/posts?account_id=
One post / the latest 50. Fields: id, kind, status, caption, scheduled_at, published_at, media_id, permalink,
error, keywords, automation_id (the keyword automation made for it), trial_strategy, created_via (dashboard | api:<key name>).
PATCH /api/posts/:id
Only while scheduled/queued. Any of: scheduled_at, post_now: true, caption, keywords, template_automation_id,
trial_strategy, share_to_feed. 400 once publishing has started.
DELETE /api/posts/:id
Cancel a scheduled/queued post (deletes its upload).
GET /api/calendar?account_id=&from=&to=
What went out (media[], with views) and what's planned (posts[]) in a time range (ms).
GET /api/posts/best-hours?account_id=
Median reel views by hour posted (Europe/Madrid), hours with 2+ reels: { time_zone, reels, hours: [{ hour, reels, median_views }] }.
GET /api/accounts/:id/publishing
{ allowed }: false means the account must be reconnected in the dashboard with the publish permission.
Automations
POST /api/automations → 201 { id } · PUT /api/automations/:id (full replace, same body) · PATCH /api/automations/:id
{ active } · DELETE /api/automations/:id (analytics history is kept).
Body (comments are explanations, send plain JSON):
{
"account_id": "…",
"name": "BOOK → link",
"trigger_type": "comment", // comment | dm_keyword | story_reply | story_mention | live_comment
"media_scope": "any", // any | specific (then media_ids: [Instagram media ids])
"media_ids": [],
"keywords": ["book"],
"match_mode": "contains", // contains (whole word) | exact | any
"exclude_keywords": [],
"public_replies": ["Sent you a DM 👀"], // public reply under the comment, one picked at random
"once_per_user": true,
"priority": 0, // higher wins when several match
"active": true,
"flow": {
"start": "s1",
"steps": {
"s1": { "type": "message", "text": "Here you go 👇", "buttons": [{ "type": "url", "title": "Open", "url": "https://…" }] }
}
}
}
Step types: message (text ≤1000, variants[], image_url, buttons[] ≤3: {type:"url",title≤20,url} or
{type:"postback",title,next}, quick_replies[] {title,next}, next, track_links), follow_gate (pass, fail?, fail_text,
retry_button), collect_email (prompt, invalid_text, next), ai_reply (brief, link, max_chars, daily_limit,
hard_limit, react_to_acks, handoff_text, extra_links[], model, next), tag (add[], next), delay (seconds ≤ 43200, next).
Every next/pass/fail must point at an existing step id. Instagram rules the engine enforces: a comment allows exactly one
private reply; follow-ups only after the person replies or taps a button (24 h window).
POST /api/automations/:id/backfill { dry: true, spacing_min: 60, spacing_max: 90, since?, until? } — DM people who commented a
matching keyword in the last 7 days before the automation existed. Always call with dry: true first (returns counts), then
dry: false. DELETE /api/automations/:id/backfill stops queued backfill DMs.
POST /api/accounts/:id/copy-automations { from_account_id, ids[] } — copy automations from another account (switched off).
DMs and contacts
POST /api/contacts/:id/send { text ≤ 1000 } — DM a contact. Only inside Instagram's 24 h window (the person messaged in
the last 24 h; GET /api/contacts/:id/chat returns window_ends_at), else 400. Counts as a human reply: the AI pauses 12 h for
this person. Logged as human_reply with meta.via = "api:<key name>".
POST /api/contacts/:id/send-voice { clip_id } — send a voice note (same rules).
GET /api/contacts/:id/chat — last 16 messages both ways + window_ends_at.
GET /api/needs-you?account_id= — chats the AI handed off. POST /api/needs-you/:contactId/done clears one. POST /api/contacts/:id/ai-catchup — let the AI answer a stranded chat now (inside the window).
Voice notes
- POST /api/voice/clips/tts
{ account_id, text ≤ 1500, voice_id? }— ElevenLabs text-to-speech with the account's connected key → clip{ id, url, … } - POST /api/voice/clips/upload?account_id=&name= — raw audio body (
audio/mp4/m4a,audio/aac,audio/wav; not mp3), ≤ 25 MB - GET /api/voice/clips?account_id= (saved library) · PATCH /api/voice/clips/:id
{ name }(save to library) · DELETE /api/voice/clips/:id
Accounts and settings
- PATCH /api/accounts/:id
{ bot_disclosure?, ai_persona?, ai_hidden_names?[], dm_cap_per_hour? }(dm_cap_per_hour 1–300 or null = 300; GET /api/accounts also returns dm_cap_effective and dms_last_hour) - POST /api/accounts/:id/sync — refresh all Instagram insights now
- POST /api/accounts/:id/subscribe — re-subscribe Instagram webhooks
- GET /api/accounts/:id/media — the account's posts (id, caption, permalink, type, posted_at): use for
media_ids - GET|PUT /api/accounts/:id/links
{ track_links?, link_domain? }— tracked-link settings; POST …/links/check re-tests the domain - GET|PUT|DELETE /api/accounts/:id/elevenlabs
{ api_key?, voice_id?, voice_name?, model? }; POST …/elevenlabs/options lists voices/models
Retention analysis
- POST /api/content/:mediaId/uploads — raw PNG/JPEG/WebP screenshot of the reel's retention graph (body,
Content-Typeset). Analysis runs in the background. - GET /api/content/:mediaId/uploads — uploads with
analysis(JSON: summary, fixes, holds, cliffs). POST /api/uploads/:id/analyse re-runs it. - GET /api/content/:mediaId/transcript —
{ text, words: [[start_s, end_s, word]], duration_s }
Read endpoints (any key)
GET /api/accounts
Connected Instagram accounts.
Fields: id (use as account_id), ig_user_id, username, name, profile_pic_url, status (active | token_expired |
disconnected), last_error, insights_synced_at, insights_error, token_expires_at, created_at.
GET /api/automations?account_id=
Automations (comment/DM triggers) with 30-day stats.
Fields: id, name, trigger_type (comment | dm_keyword | story_reply | story_mention | live_comment),
media_scope (any | specific), media_ids[], keywords[], match_mode (contains whole-word | exact | any),
exclude_keywords[], public_replies[] (public comment replies, one picked at random), once_per_user, priority, active,
triggered_30d, dms_30d, clicks_30d, created_at, updated_at.
GET /api/automations/:id
One automation plus its flow: { start: stepId, steps: { [stepId]: Step } }. Step types:
- message: text, variants[] (random alternative wordings), buttons[] (url or postback→next), quick_replies[], next, track_links
- follow_gate: checks if the person follows; pass / fail step ids
- ai_reply: LLM reply; brief, link, max_chars, daily_limit, model
- collect_email, tag (add[]), delay (seconds, next)
GET /api/analytics?account_id=&automation_id=&days=30
DM funnel. days 1–365. automation_id optional.
totals/unique(people):comment_received,trigger_matched,dm_sent,dm_read,postback(button taps),link_click(tracked links only),follow_check_pass,email_capturednew_contactsdaily[]:{ day, …same keys }automations[]: per automation{ automation_id, name, trigger_matched(_uniq), dm_sent(_uniq), dm_read(_uniq) }
GET /api/content?account_id=&type=all&days=0
Every post/reel/story with Instagram metrics and the DMs it drove. type: all | FEED | REELS | STORY.
days: only posts from the last N days (0 = all time).
Per item:
- id (Instagram media id), media_type, media_product_type (FEED | REELS | STORY), caption, permalink,
thumbnail_url, media_url, posted_at, metrics_at, insights_error
- metrics (lifetime totals from Instagram; keys present depend on type): views, reach, likes, comments, shares,
saved, total_interactions, follows, profile_visits, ig_reels_avg_watch_time (ms), ig_reels_video_view_total_time (ms),
reels_skip_rate, posts/stories also profile_activity + pa_bio_link_clicked, pa_email, pa_call, pa_direction, pa_text, pa_other,
stories also replies, link_clicks, nav_*. *Instagram does not report follows/profile visits for reels.
- rates (0–1): like_rate (likes/views), comment_rate, share_rate, save_rate, engagement_rate (interactions/reach),
follow_rate, profile_visit_rate (null when not available)
- dm: dm_triggered (people who commented a keyword), dm_sent, dm_tapped, dm_clicked, dm_emails, dm_follow_ok, comment_webhooks
GET /api/content/:mediaId
Same as one content item plus snapshots[]: one row per day { day, …metrics } (growth curve of the lifetime totals).
GET /api/account-insights?account_id=&days=30
Account-level growth. days 1–90 (Instagram keeps 90 days).
daily[]:{ day, reach, views, accounts_engaged, total_interactions, likes, comments, shares, saves, replies, reposts, follows, unfollows, net_follows, profile_views, website_clicks (bio link taps), profile_links_taps (contact buttons), followers_count?, views_follow_follower, views_follow_non_follower, reach_follow_follower, reach_follow_non_follower, views_type_reel|post|story, reach_type_*, total_interactions_type_*, likes_type_*, comments_type_*, shares_type_*, saves_type_* }totals: sums of the main keys plusfollowers_now,followers_changelink_taps_by_type: bio link / button taps by typeaudience[]:{ kind (follower_demographics | engaged_audience_demographics), breakdown (age | gender | country | city), data, captured_at }, plus{ kind: "online_followers", breakdown: "hour_pt", data: { "0".."23": followers online } }(hours in US Pacific time)
GET /api/contacts?account_id=&q=&limit=50&offset=0
People who commented on a trigger or messaged. limit ≤ 200. q searches username/email/tags.
Response: { total, contacts: [{ id, igsid, username, name, follows_business (1/0/null), email, tags (JSON array string),
last_inbound_at, first_seen_at, updated_at }] }
GET /api/contacts.csv?account_id=
Same contacts as CSV.
GET /api/events?account_id=&type=&after_id=&since=&limit=50
Raw activity log. limit ≤ 1000. Default order newest first. With after_id the order is oldest first after that id:
use it to sync incrementally. since = unix ms lower bound. type filters one event type.
Fields: id (increasing), ts, type, account_id, automation_id, automation_name, contact_id, username, igsid,
media_id, ref, meta (JSON string).
Event types:
| type | meaning | useful meta |
|---|---|---|
| comment_received | someone commented | text |
| trigger_matched | comment/DM matched an automation | text |
| public_reply_sent | public reply posted under the comment | reply_id |
| dm_sent | we sent a DM | step, kind (buttons/text/ai/fallback_text), text, private_reply, variant |
| dm_read | they read it | |
| postback | they tapped a button | step |
| link_click | tracked link clicked | url |
| follow_check_pass / follow_check_fail | follow gate result | |
| message_received | they sent us a DM | text |
| human_reply | the owner replied by hand (AI pauses 12 h for that person) | |
| ai_skipped | AI didn't reply | reason |
| post_published / post_failed | a post made here went live / failed | permalink / error (ref = post id) |
| email_captured, trigger_skipped_repeat, postback_ignored, reply_debounced, backfill_queued | | |
Notes for agents
- Posting is real: it goes live on Instagram. Use
scheduled_atif a human should get a chance to review it in the dashboard (Post page → calendar), and always send anIdempotency-Key. - Instagram caps DMs at ~300/hour per account; sends over the cap are queued, not lost. Comment triggers stop at 200/hour.
- All numbers are as reported by Instagram. Instagram gives no per-reel follows/profile visits; use account-level
followsandprofile_views. contacts.tagsandevents.metaare JSON-encoded strings: parse them.- Be polite: poll
eventsat most every 15 s; other endpoints change at most every 30 min.