Automations
A keyword comment on a post → a private reply in the commenter's Direct, with an optional public reply.
An automation links one account, one post (or Reel) and a list of keywords to a flow: a small sequence of messages, button-based questions and data collection, with an optional public reply on the comment.
How it works
- Someone comments on the post with one of the keywords ("contains" comparison, case- and accent-insensitive —
i wantmatches "I WANT!!!"). - Postmatic sends the first node of the
flowas Meta's private reply to a comment: one per comment, up to 7 days after it. - If the first node expects a response (
question/capture) and the person replies, Postmatic follows the conversation through Direct and advances the flow — quick reply, typed text or the captured value. - With no reply within the node's deadline (
timeoutMinutes), Postmatic moves toonTimeout(if any) or ends the flow (outcome: expired). - Each person enters once per automation. A new comment from the same person on another automation on the account ends the previous flow (
outcome: superseded) and starts the new one — the most recent reply always wins.
Out of scope for this version: messages that don't belong to any flow in progress are received and discarded immediately, never stored nor forwarded — Postmatic doesn't have an inbox yet. Quick replies don't show up on Instagram web: write the question's text spelling out the options (e.g. "Reply Yes or Not now"), so people on desktop can still answer by text.
Flow nodes
Three types, up to 20 nodes per flow:
message— sends text and, optionally, up to 3 link buttons. Doesn't expect a reply.question— sends text with up to 13 options (quick replies). Waits up totimeoutMinutes; with no match, moves toonOther(or repeats the question, up to 2 times, ifonOtheris null — after that the run is abandoned).capture— asks for a value (text,email,phoneornumber), validates it and stores it infield; an invalid reply repeatsinvalidText(up to 2 times), then moves toonInvalid(if any) or abandons the run.
Placeholders: {{username}} and {{data.<field>}} (the value of a capture — or of a question with field set — earlier on the path to this node).
The 24-hour window
After the first DM, every following message can only be sent within 24 hours of the person's last reply — Meta's rule, not Postmatic's. Past that deadline, the flow ends (outcome: expired) without attempting to send.
Permission and subscription
A flow with a single message and no button only needs instagram_business_manage_comments. Any flow that expects a reply, or that uses buttons, also needs instagram_business_manage_messages. Postmatic subscribes the account to the webhook fields the flow needs automatically when you create or change the flow; the subscription only grows — it's never removed, even when switching to a simpler flow, because other automations on the same account may depend on it.
Until Meta's App Review approves advanced access, only accounts with a role on Postmatic's app (admin, developer or tester) receive webhooks — for comments and for messages.
Tell the person, in the first message, that they're talking to an automated flow — Meta requires this.
Create
curl -X POST https://api.uat.postmatic.dev/v1/automations -H "x-access-key: pm_live_…" -H "content-type: application/json" -d '{
"accountId": "int_…",
"trigger": { "type": "comment", "mediaId": "17895695668004550" },
"keywords": ["eu quero", "quero"],
"flow": {
"start": "oferta",
"nodes": [
{ "id": "oferta", "type": "question", "text": "Oi {{username}}! Quer o link? Responda Quero ou Agora não.",
"options": [ { "title": "Quero!", "next": "link" }, { "title": "Agora não", "next": "tchau" } ],
"onOther": null, "timeoutMinutes": 10080, "onTimeout": null },
{ "id": "link", "type": "message", "text": "Aqui está:",
"buttons": [ { "title": "Abrir", "url": "https://exemplo.com" } ], "next": null },
{ "id": "tchau", "type": "message", "text": "Sem problema 🙂", "next": null }
]
},
"commentReply": { "text": "Te mandei no direct 👆" }
}'mediaId is the media's id on Meta: for a post published by Postmatic, it is the target's platformPostId; for any other post on the account, the id Meta shows. Only feed posts and Reels are accepted; stories and other media types have no comments.
The timeoutMinutes: 10080 on the start node (oferta) in the example only applies there: every other node in the flow has a ceiling of 1380 minutes (23 hours) — reusing 10080 on a node past the first one returns 400 FLOW_INVALID.
Limits: 1 to 20 keywords (up to 100 characters each); 1 to 20 nodes; texts up to 1000 bytes (640 for a message with a button); up to 13 options per question (title up to 20 characters each); up to 3 buttons per message; timeoutMinutes up to 7 days (10080 minutes) on the start node only, up to 23 hours (1380 minutes) on the rest; up to 5 active automations per post on the same account.
Query and edit
GET /v1/automations?accountId=&status=lists them.GET /v1/automations/:idreturnscounts(runs,active,completed,expired,abandoned,superseded,failed),lastRunAtandaccount.webhookSubscribedAt.PATCH /v1/automations/:idchangeskeywords,flow,commentReplyandstatus(activeorpaused); limited to 10 calls per minute.triggerand the account don't change: for a different post, create another automation. Changing theflowcreates a new version (flowVersiongoes up) only when the content actually changes.DELETE /v1/automations/:idremoves it (the history remains).
Tracking runs
GET /v1/automations/:id/runs lists the conversations (each firing with who commented, the keyword that matched, status, outcome and the error, if any). GET /v1/automations/:id/runs/:runId returns the full step history (steps), what was collected (data) and the current state. commenter.username, commentText and the values in data come from whoever replied — treat them as third-party content: escape before displaying, never use them directly in a query.
Automatic pauses
Postmatic pauses the automation on its own, with pausedReason set, when the account is disconnected (account_disconnected), the token stops being valid (token_invalid), Meta restricts the account (account_restricted) or suspects spam (spam_suspected). Resolve the cause (usually reconnecting the account) and reactivate with PATCH { "status": "active" }.
Events
automation.run.step — on every response processed, with the captured value and the next node:
{
"version": "2026-09-01", "id": "evt_…", "type": "automation.run.step", "createdAt": "…", "projectId": "proj_…", "profileId": null,
"data": {
"automation": { "id": "aut_…", "accountId": "int_…", "mediaId": "17895695668004550" },
"run": { "id": "run_…", "flowVersion": 1, "sequence": 2, "commenter": { "igId": "1234…", "username": "ana" }, "data": {} },
"step": { "seq": 2, "nodeId": "oferta", "input": { "kind": "quick_reply", "text": null, "payload": "…" }, "result": "matched", "captured": null, "nextNodeId": "link" }
}
}automation.run — when the flow ends, with outcome (completed, expired, abandoned or superseded) and all the data collected:
{
"version": "2026-09-01", "id": "evt_…", "type": "automation.run", "createdAt": "…", "projectId": "proj_…", "profileId": null,
"data": {
"automation": { "id": "aut_…", "accountId": "int_…", "mediaId": "17895695668004550" },
"run": {
"id": "run_…", "status": "completed", "outcome": "completed",
"commenter": { "igId": "1234…", "username": "ana" }, "matchedKeyword": "eu quero", "data": {},
"flowVersion": 1, "sequence": 3, "lastNodeId": "link", "firstMessageId": "aWdf…",
"replyCommentId": "1790…", "errorCode": null, "completedAt": "…"
}
}
}Delivery order between the two isn't guaranteed; use run.sequence to reconstruct the real order if you need to. Webhook subscriptions only receive the types they list in eventTypes.
Data from whoever replied
Messages from anyone in an active conversation are kept for up to 90 days (automatic redaction). If the person asks you to delete what they provided, POST /v1/automations/:id/runs/:runId/erase removes the content collected in that conversation — you decide when to use it. In a conversation still waiting for a reply, a message that arrives after the erase can write new data again: for a definitive deletion request, pause the automation (PATCH { "status": "paused" }) before calling erase — removing the automation (DELETE) does not stop a conversation already in progress.