Plain markdown for agents

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.

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


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

Accounts and settings

Retention analysis


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.

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).

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