API v2 Instagram Channel
Read an agent's Instagram connection and manage its automations and conversation starters.
Use these endpoints to manage what an agent does on Instagram: the four automations (respond to DMs, comment-to-DM, story leads, story mentions), their per-post / per-story instances, and the conversation starters Instagram shows when someone opens a DM for the first time.
Connecting Instagram is not part of the API. The OAuth handshake happens in the dashboard under Publish → Instagram; these endpoints manage an agent that is already connected. Every endpoint requires Authorization: Bearer YOUR_API_KEY, and every response is a bare object without a data envelope.
Requests for an agent with no Instagram connection return 404 INSTAGRAM_NOT_CONNECTED, except GET /channels/instagram, which answers { "connected": false }.
Publishes and clears of the same persistent-menu or conversation-starters resource are serialized per agent. An overlapping PATCH returns 409 INSTAGRAM_SYNC_IN_PROGRESS; retry after the active operation finishes.
Get the Instagram Channel
GET /api/v2/agents/{agentId}/channels/instagram
Returns the connection, all four automations, and the conversation starters in one call.
| Field | Type | Notes |
|---|---|---|
connected | boolean | false when the agent has no Instagram connection; no other field is present. |
id | string | Connection ID. |
instagram_username | string or null | Connected Instagram handle. |
instagram_user_id | string | Instagram account ID. |
facebook_page_name | string or null | Linked Facebook page, when the connection was made with Facebook Login. |
profile_picture_url | string or null | Instagram avatar. |
login_kind | string | instagram_login or facebook_login. |
status | string | Connection health, for example connected. Anything else means the connection needs to be re-established in the dashboard. |
scopes | array of strings or null | Permissions observed through grant readback. null means Meta did not make grant readback available; an empty array means a successful read returned no grants. |
last_error_message | string or null | Last connection error surfaced in the dashboard. |
automations | array | One entry per automation, always all four. |
conversation_starters | object | Same shape as the conversation starters endpoint below. |
{
"connected": true,
"id": "a0d2b8b6-7d0e-4bd5-9f0a-1e2f3a4b5c6d",
"instagram_username": "agentkit.ai",
"instagram_user_id": "17841400000000000",
"facebook_page_name": null,
"profile_picture_url": "https://scontent.cdninstagram.com/...",
"login_kind": "instagram_login",
"status": "connected",
"scopes": [
"instagram_business_basic",
"instagram_business_manage_messages",
"instagram_business_manage_comments"
],
"last_error_message": null,
"automations": [
{ "key": "respond_to_dms", "status": "live", "config": { "mode": "all" } }
],
"conversation_starters": {
"status": "draft",
"draft_prompts": [],
"live_prompts": null,
"synced_at": null,
"last_sync_error": null,
"last_sync_error_code": null
}
}
Refresh connection permissions
POST /api/v2/agents/{agentId}/channels/instagram/permissions/refresh
The response includes a machine-readable outcome, optional code, lastVerifiedAt, and loginKind. grant_readback_unavailable is the normal steady-state result for Instagram Login: the token identity check passed and webhook repair was submitted, but Meta does not expose grant readback for that login method. lastVerifiedAt records only a previous full grant and subscription readback, not the time of this partial check.
Automations
GET /api/v2/agents/{agentId}/channels/instagram/automations/{key}
PATCH /api/v2/agents/{agentId}/channels/instagram/automations/{key}
key is one of respond_to_dms, comment_to_dm, story_leads, story_mentions. Any other value returns 404 RESOURCE_NOT_FOUND.
Both methods return { "key", "status", "config" }. An automation the agent has never configured reports its default: respond_to_dms is live (replying to every DM with the agent's AI is the baseline behaviour), the other three are draft.
| Status | Meaning |
|---|---|
live | Running on the connected account. |
draft | Saved but not running; incomplete configurations are allowed. |
off | Explicitly disabled. |
The PATCH body takes config, status, or both — at least one is required.
curl -X PATCH 'https://your-domain.com/api/v2/agents/955f28f1-8515-40bb-802c-f3f730bf0343/channels/instagram/automations/respond_to_dms' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"status": "off"}'
config is the automation's full configuration object, not a patch: send the object you read back from GET, with your edits applied. Each key has its own shape (respond_to_dms takes mode, keywords, message, links, intent and email-capture fields; comment_to_dm takes post scope, keyword filter, public replies, and DM copy — the opening message with its buttonLabel, then the link message and links sent when the button is tapped; and so on). Read the current object first rather than composing one by hand.
Behaviour worth knowing before you automate against this endpoint:
- A
statusalone never rewrites your configuration. It flips the stored config on or off. - A
configalone saves a draft, unless the automation is already live and the new configuration still satisfies the go-live rules — then it stays live. An incomplete edit to a live automation drops it todraftinstead of silently changing what the connected account is running. - Going live validates.
status: "live"with an incomplete configuration returns400 VALIDATION_INVALID_BODY;detailsnames the missing fields. The one exception isrespond_to_dms, which falls back to replying to every DM rather than refusing to turn back on. - Comment automations need the comments permission. Setting
comment_to_dmlive on a connection granted before that permission existed returns409 INSTAGRAM_RECONNECT_REQUIRED; reconnect the account in the dashboard. Turning it off always works. - Email capture is plan-gated. On a plan without the Instagram add-ons,
askEmailEnabledis saved asfalserather than rejected. comment_to_dmandstory_leadscan have several rows. This route always addresses the oldest one (the row that existed before instances were introduced) and still returns a single object; the channel response still lists exactly four automations. Use the instance routes below to work with the others. The deprecatedpostScope: "next"is still accepted on input and stored as"any".
Automation instances
GET /api/v2/agents/{agentId}/channels/instagram/automations/{key}/instances
POST /api/v2/agents/{agentId}/channels/instagram/automations/{key}/instances
GET /api/v2/agents/{agentId}/channels/instagram/automations/{key}/instances/{id}
PATCH /api/v2/agents/{agentId}/channels/instagram/automations/{key}/instances/{id}
key is comment_to_dm or story_leads — the two automations whose trigger names a target (a post or reel id, a story id). Any other key returns 404 RESOURCE_NOT_FOUND. Every instance is { "id", "key", "status", "config" }; the list is ordered oldest first.
An agent may run one live instance per target plus one live catch-all per kind (postScope: "any" / storyScope: "any"). Drafts and off rows are unlimited. When a comment or story reply arrives, the live instance that targets that exact post or story handles it — its keyword filter is the only one evaluated — and the catch-all runs only when no instance targets it.
curl -X POST 'https://your-domain.com/api/v2/agents/955f28f1-8515-40bb-802c-f3f730bf0343/channels/instagram/automations/comment_to_dm/instances' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"config": { "postScope": "specific", "mediaId": "17900000000000001", ... }, "status": "live"}'
POST requires a complete config (read one back from GET and edit it); without status the instance is saved as a draft. PATCH takes config, status, or both, with the same rules as the key route. There is no delete: set status to "off" to retire an instance.
Going live is where cardinality is enforced:
409 INSTAGRAM_AUTOMATION_TARGET_TAKEN— another live instance already targets the same post or story. Turn that one off first, or target a different post.409 INSTAGRAM_AUTOMATION_CATCH_ALL_EXISTS— a live catch-all for this kind already exists.
Regression guarantees for existing clients: GET /automations/comment_to_dm keeps returning one object (the oldest instance), and GET /channels/instagram keeps returning exactly four automations entries, one per key.
Conversation Starters
GET /api/v2/agents/{agentId}/channels/instagram/conversation-starters
PATCH /api/v2/agents/{agentId}/channels/instagram/conversation-starters
Conversation starters are the tappable questions Instagram shows the first time someone opens a DM with the account (Meta calls them ice breakers). Each starter carries the scripted answer your agent sends when it is tapped.
| Field | Type | Notes |
|---|---|---|
status | string | draft, live, or off. |
draft_prompts | array | Up to 4 objects: question (≤80 characters), message (the scripted reply), links (up to 3 { label, url } buttons). |
live_prompts | array or null | The prompts Meta confirmed are set. null unless the status is live. |
synced_at | string or null | ISO 8601 timestamp of the last confirmed sync with Meta. |
last_sync_error | string or null | Fixed copy for the last sync failure. Meta's message text is never returned. |
last_sync_error_code | string or null | Allow-listed machine code for the last sync failure. |
Internal synchronization states are not exposed. While a publish or clear is in flight, status continues to report the last confirmed state. After a failed sync it still reports that confirmed state. last_sync_error carries fixed copy derived from last_sync_error_code and may include numeric HTTP or Meta codes.
The PATCH body takes prompts, status, or both — at least one is required.
curl -X PATCH 'https://your-domain.com/api/v2/agents/955f28f1-8515-40bb-802c-f3f730bf0343/channels/instagram/conversation-starters' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"prompts": [
{
"question": "What does it cost?",
"message": "Plans start at $19/month — here is the full breakdown.",
"links": [{ "label": "Pricing", "url": "https://agentkit.ai/pricing" }]
}
],
"status": "live"
}'
status: "live" publishes to Instagram, so it is the one call here with an external side effect:
- Every starter needs a non-empty
questionandmessage, and every link URL must be absolutehttp(s). Otherwise the call returns400 VALIDATION_INVALID_BODYand nothing is published. - Omitting
promptspublishes the saved draft. - The draft is always saved before Meta is called, so a failed publish never loses your edits.
statusbecomesliveonly after Meta confirms the prompts and the server reads them back. If the sync fails, the call returns502 INSTAGRAM_SYNC_FAILED, and the stored status andlive_promptskeep describing what is actually on Instagram.- A connection that is not
connectedreturns409 INSTAGRAM_RECONNECT_REQUIREDbefore any Meta call.
status: "off" clears the starters on Instagram first and only then records off locally, with the same 502 behaviour if the clear fails. Because live means "confirmed on Meta", you cannot move a live set straight back to draft — set it off first, which returns 400 VALIDATION_INVALID_BODY with that instruction if you try.
Sync failures use allow-listed codes and numeric metadata. Meta's message text is never persisted or returned.
{
"error": {
"code": "INSTAGRAM_SYNC_FAILED",
"message": "Instagram rejected the update. (HTTP 400, Meta code 100)",
"details": {
"code": "meta_set_failed",
"http_status": 400,
"meta_code": 100
}
}
}
details.code is one of meta_set_failed, meta_set_unconfirmed, meta_read_failed, meta_clear_failed, meta_clear_unconfirmed, meta_readback_mismatch, meta_subscription_unverified, meta_compensation_unconfirmed, reconnect_required, local_state_write_failed, or unknown_error. The numeric fields are null when unavailable. meta_set_unconfirmed and meta_clear_unconfirmed mean the request never produced a response (timeout or dropped connection); meta_compensation_unconfirmed means the rollback after a failed update could not be verified. In all three the change may or may not have been applied — retry the sync.