DM automation requests
Create a draft DM automation, activate only on request, target tracked posts, and diagnose runs without duplicate sends.
Compact reference for agents making REST requests. All endpoint paths below are relative to https://backend.blotato.com/v2. For task instructions, read the workflow guide. For tool calls, use the MCP reference.
Manage dm automations
Inspect existing automations before creating a flow. Confirm the account, Page, trigger scope, message, and requested activation state. A draft request does not authorize activation. Configure gates and webhooks only when requested, using the user's approved destination for exported contact data.
A DM automation sends a direct message when someone comments on your post or sends
your account a message. Text plus up to 3 link buttons. An automation holds up to 2
triggers: one comment-received and one message-received. Any one firing starts a
single run.
Optional steps run in a FIXED order around that message:
followGate (instagram only) -> emailGate -> dmMessage -> webhook
Supported platforms: instagram, facebook
Notes:
- A comment trigger answers with a PRIVATE REPLY to the comment (one per comment,
within 7 days). A message trigger answers with a standard DM (24-hour window).
- Your own comments and the messages your account sends never start a run.
- Link buttons are optional. Set buttons to [] for a text-only message or an
email-to-webhook flow. A button title is the text the recipient sees, and its
url is the page the button opens.
- A button url and webhook.url serve different purposes. webhook.url receives
captured email data after the DM sends.
- No cold outreach. An automation answers people who contact you first.
- An automation is created as a draft unless isActive is true.
- While a run waits on a contact's reply, their next DM RESUMES that run instead of
starting a new one. A button tap never starts a run.
- Automated sends use the monthly active-contacts limit. Multiple sends to the same
active contact do not add another contact that month. GET /users/me/usage returns
the current contacts and contactsLimit, including account-specific limits.
- Every live automation whose trigger matches starts its OWN run. For best results, keep one live automation per keyword per account.
- Each matching comment starts a new run, so a repeat commenter gets a reply per comment.
- Messages without text (photos, stickers, voice notes, reactions) never start a run or
advance a gate.
- Disconnecting an account ARCHIVES its automations. Reconnecting keeps them. Removing a
Facebook Page leaves its automations live, but they stop receiving events until the
Page is added back.
List automations, optionally with selected analytics:
GET /dm-automations?limit=20&cursor=OPTIONAL_CURSOR&metrics=triggered&metrics=issues
Response: { "items": [ { ...<DmAutomation>, "analytics": { "triggered": 412, "issues": 3 } }, ... ], "cursor": "..." }
Metric values: triggered | completed | failed | issues. issues counts failures from the
last 7 days and is a subset of failed. Unrequested metrics are absent.
Create a draft automation:
POST /dm-automations
{
"accountId": "required-account-id",
"platform": "instagram", // "instagram" | "facebook"
"target": { "targetType": "instagram" }, // facebook also needs "pageId"
"name": "Reply to price questions", // 1-60 characters
"triggers": [ // 0-2 triggers, at most one per type
{
"type": "comment-received", // or "message-received"
"keywords": ["price", "link"], // [] matches every comment/text message
"postId": null // comment triggers only, see below
}
],
"dmMessage": "Which product are you asking about?", // replace with the approved reply
"buttons": [], // REQUIRED field, pass [] for none
"isActive": false // draft, not listening yet
}
Response: 201 { "flow": <DmAutomation> }
Save flow.id and inspect flow.isActive and flow.triggers. Creation is not proof a
trigger fired or a message was sent. Replace illustrative account IDs and wording.
Optional fields, only when requested:
buttons: [{ "type": "url", "title": "View link", "url": "https://example.com" }]
followGate: { "message": "Follow me, then reply FOLLOWING.", "buttonTitle": "I'm following" }
emailGate: { "message": "Reply with your email." }
webhook: { "method": "POST", "url": "https://example.com/hooks/leads" }
Webhook methods: GET|POST|PUT|PATCH|DELETE. Optional headers hold destination-specific
credentials. Do not publish credential values in docs, logs, or conversation text.
Follow/email gate messages: 1-640 characters. Follow button title: at most 20 characters.
Use the user's approved webhook URL, not the illustrative example.com address.
Activate only when requested:
PATCH /dm-automations/:id
{ "patch": { "isActive": true } }
Inspect response.flow.isActive. Read back with GET /dm-automations/:id if needed.
GATE AND WEBHOOK RULES:
- followGate: INSTAGRAM ONLY. Sends the gate message with a confirm button, waits up
to 30 DAYS for a reply, then checks follower status. Not following -> gate message
again. Following or UNKNOWN status -> proceed. UNKNOWN means the contact never
granted profile access, and Blotato proceeds rather than blocking them. A confirmed
follow is cached 30 days, and a negative result is never cached. Every run still sends
the gate message. Passing followGate on a facebook automation -> 422 (20303).
ANY text reply advances the gate - the follower check runs on the contact's next
text DM whatever it says, so the button tap is a shortcut, not a requirement.
SETUP: the account must be subscribed to button-tap events, which happens at
CONNECT time. An account connected before DM automations and follow gating landed
does not reliably receive taps and runs reach "expired" - the user must reconnect it in
Accounts.
DESKTOP: Instagram renders the confirm button in the mobile app only. Write the
gate message to ask for a typed reply (e.g. "Reply FOLLOWING once you have") so a
desktop audience advances without a button.
- emailGate: sends the gate message, waits up to 72 HOURS, reads the FIRST email
address out of the reply. No address found -> gate message again. Found -> saved to
the contact, then proceed. Shape is validated, deliverability is not. The parsed
address has no dedicated field in GET /conversations or GET /messages.
The person's original reply remains message text. Use a webhook for the stored
contact email captured by the gate.
SKIP: on automations published after September 14, 2026, a contact who already
replied with an email to ANY email gate on the same connected account skips the
gate. Older automations ask every time until saved again (PATCH with their
current triggers, or Save and Publish in the web app).
- webhook: called AFTER dmMessage sends. Every method except GET carries a JSON body
with Content-Type: application/json, with or without a gate on the automation:
{ "email": "them@example.com", "isFollower": true }
GET carries no body. The host must resolve to a PUBLIC address - private, loopback,
link-local and cloud metadata ranges are rejected. Redirects are NOT followed.
Timeout 15s. A timeout logs a warning (code 20305) and the run STILL COMPLETES,
with no retry. Response body read is capped at 16 KB. A non-2xx response is logged
and the run STILL COMPLETES. A blocked address, DNS failure or connection error
fails the run (code 20304). The URL and body appear in run logs, headers do not.
Update an automation (patch is NESTED under "patch"):
PATCH /dm-automations/:id
{ "patch": { "dmMessage": "New text", "buttons": [] } }
Patchable: name, triggers, dmMessage, buttons, followGate, emailGate, webhook, isActive.
triggers replaces the WHOLE trigger set ([] clears it). Pass null for followGate,
emailGate or webhook to REMOVE it.
accountId, platform, and target are fixed at creation.
Content changes to a LIVE automation publish immediately. Changes to a draft stay
saved until isActive is true. A live automation needs at least one ACTIVE trigger - to
clear every trigger, pass an empty triggers array and set isActive false in the same
request. To drop one trigger, send triggers holding the one you keep. A run already in
progress keeps the version it started with.
Omitting isActive preserves the current state, including immediate publication of
content changes to an already live flow. Confirm live changes before sending them.
Update a single trigger, e.g. turn it on or off (immediate on a live
automation, no republish):
PATCH /dm-automations/:flowId/triggers/:triggerId
{ "patch": { "isActive": false } }
Response: 200 { "trigger": <Trigger> }
- triggerId comes from the automation's triggers[].id. IDs CHANGE whenever the
automation's content or triggers are updated - re-read the automation first.
- Turning off the last active trigger on a live automation is rejected with 422
(code 20303).
Archive an automation:
DELETE /dm-automations/:id
Response: 200 { "flow": <DmAutomation> } // flow.isActive is false
Archiving stops new runs. Existing runs, including waiting gates, remain able to
continue. Archiving does not cancel messages already queued. Archive only when requested.
<DmAutomation>:
{
"id": "flow_abc123",
"accountId": "98434",
"name": "Auto-DM links from comments",
"platform": "instagram",
"target": { "targetType": "instagram" },
"triggers": [
{ "id": "trg_abc123", "type": "comment-received", "keywords": ["price"], "postId": null, "isActive": true },
{ "id": "trg_def456", "type": "message-received", "keywords": ["price"], "isActive": true }
],
"dmMessage": "Tap below for the link.",
"buttons": [ { "type": "url", "title": "View link", "url": "https://example.com" } ],
"followGate": { "message": "Follow me, then tap below.", "buttonTitle": "I'm following" },
"emailGate": { "message": "Reply with your email." },
"webhook": { "method": "POST", "url": "https://example.com/hooks/leads" },
"isActive": true,
"publishedVersionId": "ver_001", // null for a draft
"createdAt": "2026-08-01T12:00:00Z",
"updatedAt": "2026-08-02T09:30:00Z"
}
TRIGGER RULES:
- Up to 2 triggers per automation, at most one per type. One matching event starts
ONE run. Each trigger has its own keywords and, for comment triggers, its own postId.
- id: read-only, present on every trigger Blotato returns. Omit it on create/update.
- isActive: false pauses one trigger while the automation stays live. A live automation
needs at least one active trigger.
- type "comment-received": fires on a comment on your post or reel.
- type "message-received": fires on a DM someone sends your account.
- keywords: case-insensitive substring match, tolerant of line breaks inside a
multi-word keyword. Any one keyword matching fires the automation. [] matches all
text.
- postId (comment triggers only): the BLOTATO post id from GET /published-posts.
That listing and the web picker include Blotato-published posts only. For an Instagram
post published outside Blotato, an incoming comment creates a tracked Blotato post
record. Use that comment's non-null postId after verifying the account and native
platformPostId. The trigger matches this tracked ID, not the native platform ID.
If the required postId is unavailable, stop and explain the missing ID. Do not widen
a single-post request to all posts. postId null watches every post and reel on the
account and requires the user's approval for that scope.
- Buttons in an automation are type "url" only. For postback buttons or quick
replies, send the message yourself with POST /messages.
- The followGate confirm button is a postback button Blotato manages - you set
its label only.
- INSTAGRAM: automation buttons show in the Instagram mobile app only. Put a button
OR a URL inside dmMessage, never both - a message carrying both leaves the URL
unclickable on desktop. For a desktop audience, pass buttons: [] and put the URL
in dmMessage.
INSPECT ACTIVITY:
GET /dm-automations/:id/runs?limit=20&cursor=OPTIONAL_CURSOR&status=failed&since=ISO_8601
Response: { "items": [ <DmAutomationRun>, ... ], "cursor": "..." }
status accepts multiple values: running | waiting | completed | expired | superseded | failed.
since returns runs with activity at or after the ISO 8601 timestamp.
<DmAutomationRun>:
{
"id": "run_abc123",
"contactId": "1784000000", // platform id of the person
"platform": "instagram",
"status": "failed", // running | waiting | completed | expired | superseded | failed
"error": {
"code": 20102,
"message": "Messaging window has expired",
"action": "Ask the contact to send a new message, then retry.",
"details": { "retryable": false }
},
"createdAt": "2026-08-02T09:30:00Z",
"updatedAt": "2026-08-02T09:30:04Z"
}
GET /dm-automations/:id/logs?flowRunId=OPTIONAL_RUN_ID&limit=20&cursor=OPTIONAL_CURSOR
Response: { "items": [ <AutomationLog>, ... ], "cursor": "..." }
<AutomationLog>:
{
"id": "log_abc123",
"flowRunId": "run_abc123",
"nodeId": "nd_9fJ2", // null for run-level entries
"level": "info", // info | warning | error
"message": "Queued message for send",
"context": { "messageId": "msg_abc123" },
"createdAt": "2026-08-02T09:30:01Z"
}
GET /dm-automations/:id/analytics
Response: { "analytics": { "triggered": 412, "completed": 398, "failed": 9, "issues": 3 } }
A run stays open until its message settles, so completed + failed is sometimes
lower than triggered. failed counts all-time runs ending on an error. issues counts
failures from the last 7 days and is a subset of failed. Expired and superseded runs
count toward triggered only.
Match the flow, run, and message IDs before diagnosing a failure. A completed run
does not prove webhook delivery: timeout and non-2xx results require inspecting logs.
Do not replay the whole flow to retry a webhook if the DM was already sent.
No run-retry operation is documented. Do not recreate or reactivate an automation
as an implicit retry. Preserve IDs and report the observed result.
ERRORS:
- 404: automation or trigger not found
- 422: invalid for publishing (code 20303: no trigger, two triggers of one type, no
active trigger, or a bad message/button/gate/webhook), or missing connected account
(code 5000)Return to the agent reference index.
Last updated