Skip to main content
Beta. The Automations API is in beta and we are actively collecting feedback. Endpoints, payloads, and limits may change as we iterate. Please send feedback and bug reports to support so we can prioritize the right improvements.
The Automations endpoints let you define rules that automatically react to incoming Instagram engagement. Each automation pairs one or more triggers (the event that fires the rule) with one or more actions (what happens when it fires). A single rule can listen for multiple triggers and dispatch multiple actions — fire a webhook into your analytics pipeline AND send a DM from the same engagement. The engine only reacts to engagement a user directs at you (a comment, story reply, DM, or reaction) — it never sends follow-triggered DMs, first-message DMs to strangers, or bulk outbound — and it inherits Ayrshare’s per-account rate limits, per-recipient deduplication, and idempotent webhook ingestion.
sent means Meta accepted the message, not that the recipient received it. Delivery is ultimately dictated by the recipient’s Instagram Message requests setting: if they don’t allow message requests from everyone, Meta returns a success response and silently drops the message, and this is invisible at every API layer. Even a delivered comment-triggered DM arrives as a message request the recipient must accept (unless a conversation already exists between the two accounts). See Automation DM Sent but Not Delivered for the full explanation.

How it works

1

Create an automation

POST /automations with the triggers and actions you want. The automation activates immediately.
2

An end user engages

Someone comments on your post, replies to your story, sends a DM, or reacts to a DM. Meta delivers the webhook to Ayrshare.
3

Ayrshare matches and dispatches

A 20–60 second jitter is applied to the engagement to stay inside Instagram’s anti-spam heuristics. The engine then looks up every rule matching the event, checks per-action deduplication and your daily DM cap, and runs each action.
4

Inspect what fired

GET /automations/:id/activity returns the audit log — every dispatch attempt, the per-action results, and any errors.

Triggers

You can attach up to 50 triggers to one automation. Each trigger is a discriminated union on the type field; type-specific fields live at the same level. All triggers are Instagram-only in v1. Keyword matching is case-insensitive and whole-word. An event satisfies a keyword-filtered trigger if it contains any one of the configured keywords.

Evergreen rules with "all"

Set postId (or storyId) to the literal lowercase string "all" to fire on every post, reel, or story on the connected account instead of one specific piece of content. This is what you want for a rule that should keep working as new content is published, with no API call per post:
The value must be exactly "all". A variant such as "ALL" or " all" is rejected with a validation error rather than saved as a post ID that would never match. "all" never widens beyond the connected account — a rule on one profile cannot fire on another profile’s content. Your daily DM cap and the engagement-anchored send rules apply exactly as they do to a single-post rule.
Deduplication is counted per automation and recipient, not per post. On an "all" rule, a user who comments a keyword on two different posts within the dedup window (7 days by default) receives the DM only once; the second activity is recorded as deduplicated. A smaller dedupWindowMinutes only shortens that window; it does not make deduplication per post. Setting it to 0 turns deduplication off, so every matching comment sends a DM, including repeat comments on the same post.
On the story triggers, omitting storyId behaves the same as "all". Prefer "all" — it states the intent explicitly, and omission will be retired in a future release with advance notice on Upcoming API Changes.

Actions

You can attach up to 50 actions to one automation. They run sequentially; each result is recorded on the activity row.

Per-action dedup window

Every action — regardless of type — additionally accepts an optional top-level dedupWindowMinutes field that overrides the default 7-day per-recipient dedup window for that action only.
  • Set to 0 to disable dedup entirely for that action (typical for fire_webhook / send_email where the receiver expects every event).
  • Capped at 525600 (one year).
Action with a 24h dedup override

Dynamic message content

By default, send_dm sends the same message to everyone who triggers the rule. Placeholders such as {{recipient_username}} are the only part that changes from person to person. Set messageUrl on the action and your own server writes the DM for each person instead: every time the rule matches, we ask your endpoint what to say, then send its answer as the DM. Example. Your rule replies to anyone who comments “LINK” on a post, and messageUrl points at your server. When Ava comments “LINK please!”:
  1. We POST the details to your messageUrl: who commented ("recipientUsername": "ava"), what they wrote, and which post it was on.
  2. Your server writes a reply for Ava, for example with a tracking link made just for her, and answers { "message": "Hi ava, here's your link: https://example.com/r/12345" }.
  3. We send that text to Ava as the DM, exactly as you wrote it.
If your server does not answer within timeoutMs, returns an error, or returns something we cannot use, we send Ava the action’s regular message instead, so she still gets a reply. Add it to a send_dm action like this:
send_dm with messageUrl
messageUrl is where we ask for the text. message is still required and is the fallback: it is never sent to your server, and we DM it instead whenever your server does not answer in time, returns an error, or returns something we cannot use (see the warning below). timeoutMs is optional (default 3000, maximum 5000). What we send your endpoint. A POST with this JSON body:
Callback request body
If you have registered a webhook secret, the body is signed with the same secret as your automations webhooks, in the same way: X-Authorization-Content-SHA256 is the HMAC-SHA256 of the raw body, and X-Authorization-Timestamp carries the body’s timeStamp. The verification code you already use for our webhooks works unchanged. If a secret is registered but we cannot read it at that moment, we do not send the callback unsigned; we send your message template instead. Where it can point. messageUrl must be a public HTTPS address. A URL that resolves to a private, loopback, link-local or cloud-metadata address is refused, and redirects are not followed — a 3xx answer counts as a failure. Either way the template is sent instead. What your endpoint sends back. Return 200 with a JSON body, at most 16 KB, containing a message string of 1 to 1000 characters:
Your text is sent verbatim. {{placeholder}} substitution is not applied to it, so a literal {{ in your generated text reaches the recipient as written.
message stays required, and it is not decoration. If your endpoint times out, returns a non-2xx, returns a body we cannot use, or has been temporarily circuit-broken after repeated failures, we send the rendered message template instead. That send can still fail, for example if the template renders empty (see below) or Instagram rejects it. We never retry the callback — a retry risks sending the recipient two messages.
timeoutMs defaults to 3000. The maximum is 5000: a larger value is rejected with a validation error when you create or update the automation. It covers the whole exchange, including reading your response body. Budget for it: the DM is sent after your endpoint answers, so a slow endpoint delays the message. Check which path a fire took on GET /automations/:id/activity. A send_dm entry in actionResults[] carries messageSource once the DM text has been worked out, including when the send then failed. It is absent when the action stopped before that, for example a skipped DM to your own account or a deduplicated action: messageSource says which text the DM was built from, not whether it was delivered. Read the entry’s status for that. A fallback on its own does not fail the action: when the template DM is delivered, the entry is sent, with the reason the callback was not used on errorDetails. If the template DM then fails too, the entry is failed and errorDetails gives the send error followed by the callback reason. Watch for it: a callback failing on every fire otherwise looks exactly like one that is working. Keep your message sendable on its own. If it renders empty for a recipient, for example {{recipient_username}} alone on a DM trigger that carries no username, and your callback text is not used, no DM is sent and the action is failed with the reason on errorDetails.

Webhook events

Two events are delivered to the automations webhook action you register with POST /hook/webhook. Both carry a type field — switch on it before reading anything else.
automation.completed is off by default. Turn it on for an automation by setting "sendCompletedEvent": true on its fire_webhook action. It stays off otherwise, so a receiver built for one payload per fire keeps getting exactly one. automation.triggered is not a record of every match: it is sent only when fire_webhook itself runs, so a match that ends rate_limited or deduplicated produces automation.completed alone. Neither event is sent for an automation that was deactivated or deleted before the dispatch ran.

automation.triggered

activityId is the row GET /automations/:id/activity returns, so you can join the two exactly rather than matching on recipientId + triggerId + keyword — which is ambiguous when several people comment the same keyword at once. actionId is the fire_webhook action that sent the event, and matches actionResults[].actionId; it tells two fire_webhook actions on the same rule apart. Fields a trigger doesn’t populate are null rather than omitted, so the shape is stable. recipientUsername is null for dm_keyword, dm_reaction and story_reply, because Meta’s payload doesn’t include one. commentId is set only for comment_keyword and story_comment, postId only for comment_keyword, and storyId only for the story triggers. story_reply has no keyword. For dm_keyword, commentText is the DM text; for dm_reaction, commentText and keyword are both the reaction emoji.

automation.completed

Sent only when the automation opts in on its fire_webhook action:
fire_webhook action with the outcome event on
status and each actionResults[].status use the same values as the activity log — sent, failed, rate_limited, deduplicated, skipped, auth_error — so in the normal case you learn how a dispatch ended without polling. Delivery of this event is best-effort, though: if the process stops between recording the outcome and queuing the event, it is not sent. The activity log is the record of truth, so if you need every outcome, reconcile against GET /automations/:id/activity for any activityId that has not reported one. A send_dm result also carries messageSource, as on the activity log: template, callback or fallback, so you can spot a messageUrl that keeps falling back without polling. Match results to your configured actions by actionId, not by position — two actions of the same type are only distinguishable that way. actionResults is empty in three cases, each with the reason on the top-level errorDetails where there is one: rate_limited (your daily cap was reached), deduplicated (every action was inside its dedup window), and failed after the dispatch was retried the maximum number of times without finishing. In that last case some actions may already have run, so a DM may have gone out and automation.triggered may already have been sent. Webhook delivery is at-least-once, so the same event can occasionally arrive twice, and a dispatch that is retried can send automation.triggered again with a new hookId. Never deduplicate on hookId. Key each event type separately, so that processing one never suppresses the other: automation.completed on activityId (each activity has one outcome), and automation.triggered on activityId plus actionId (one per fire_webhook action). The two events are delivered independently, so automation.completed can occasionally arrive before automation.triggered.

If no webhook is registered

fire_webhook posts to the automations action registered on the profile, falling back to your Primary Profile. There is no fallback to another action such as messages. If neither registration exists, automation.triggered is not silently dropped: the action is recorded on the activity row as failed, with the reason in actionResults[].errorDetails. (automation.completed has nowhere to go either, and is not recorded.) The dispatch’s overall status then follows the usual precedence: auth_error if any action hit an auth error, otherwise sent if any action was accepted, otherwise failed. So the missing webhook counts toward the automation’s failure stats only when nothing else on the rule sent; when another action did send (and none hit an auth error), the dispatch ends sent and the failed webhook shows only in actionResults[]. A fire_webhook result of sent means the event was queued for delivery to your URL, not that your endpoint accepted it. Delivery is then retried on its own schedule.

Template variables

send_dm.message, send_email.subject, and send_email.message support {{placeholder}} substitution. Unknown placeholders are rejected at create/update time (as a 473 validation error) so a typo never silently leaks the literal {{foo}} into a customer-facing message.
No sender_email / recipient_email. These are deliberately not exposed — your billing email has no legitimate place in a DM to a stranger, and Meta does not provide the recipient’s email on any IG webhook. Avoiding the placeholders prevents accidental disclosure.
Example template:

Rate limits and caps

The active-automation cap is counted per User Profile, not per parent account. Each profile under your account gets its own Launch/Business 10 or Enterprise 50, so an account with many profiles can run that many automations on each. It counts active automations and is enforced on both POST (create) and PUT re-activation (active: false → true), each surfacing error code 470. Need a higher per-profile limit? Contact support to have it raised for your account. The daily DM cap applies per parent Ayrshare account, shared across all your profiles, with a per-profile sub-cap so one busy profile cannot drain the whole account’s quota. When a DM cap is hit, the activity row records status rate_limited and no DM is sent. The messaging conversation limit also applies. send_dm needs the Messaging add-on on the profile, and each DM it sends counts toward that profile’s monthly conversation limit, the same as a DM you send yourself. When the limit is reached, a send_dm to a new recipient is not sent and the activity row records status failed, with the limit message in actionResults[].errorDetails. DMs triggered by a comment (comment_keyword and story_comment) are the exception: they are still sent at the limit, and still counted. Structural caps on a single automation: 1–50 triggers, 1–50 actions. Instagram itself caps DMs at roughly 200/hour per account. The engine paces dispatch with a 20–60 second jitter to stay safely under this.

Activity statuses

A row in GET /automations/:id/activity carries a top-level status plus a per-action status inside actionResults[]: pending and in_flight are transient; everything else is terminal. When an automation has more than one action, the top-level status is derived from the per-action results in this order: auth_error if any action hit an auth error, then sent if any action was accepted, then deduplicated if every action was deduplicated, then skipped if every action was deliberately suppressed (skipped or deduplicated), otherwise failed. Read actionResults[] for the per-action detail.
sent is a platform-acceptance receipt, not a delivery receipt. Instagram does not expose message delivery to any API. A send_dm action is marked sent the moment Instagram accepts the message; whether the recipient actually receives it depends on their Instagram Message requests setting, which Ayrshare cannot read or influence. See Automation DM Sent but Not Delivered.

Error codes

The API returns two shapes of error:
  • Business-rule errors carry a numbered automation code (e.g. { "action": "automation", "code": 469, ... }).
  • Validation errors — any malformed request body (missing or invalid fields, unknown template variables, unrecognized keys) — are returned as a single 473 response with a details object that lists the offending fields. details is the validator’s output (formErrors plus fieldErrors). Branch on details, not on a per-condition code. In fieldErrors, keys are the top-level request fields (triggers, actions): a problem inside a specific entry, such as a trigger missing its keywords, is reported under that field (e.g. triggers), while formErrors holds object-level issues such as unrecognized keys.
Launch plans are included. Business Launch resolves to the Business tier, so Automations are available on Launch, Business, and Enterprise. 468 is only returned to plans that do not include Automations.

What Meta does NOT allow

A few commonly-requested capabilities are not supported because Meta doesn’t permit them on the public Instagram API:
  • Auto-DM on new followers. Instagram does not publish a follow webhook.
  • First-message DMs to strangers. Meta requires the recipient to have engaged first (comment, reply, DM, reaction) before a business account can message them. Every supported trigger is anchored to such an engagement — but note that being allowed to send is not the same as the message being delivered: the recipient’s Instagram Message requests setting can still cause Meta to accept and then silently drop the message (see Automation DM Sent but Not Delivered).
  • Bulk outbound campaigns. Hourly DM caps and anti-abuse heuristics apply at the platform level.

Multi-profile usage

The endpoints respect the profileKey header. Pass a child profile’s key and the automation is created/managed under that profile. Rate limits split across profiles via a per-profile sub-cap so one chatty profile doesn’t drain the parent account’s quota.

FAQ

No. Instagram does not publish a follow webhook, and Meta does not allow third-party apps to send a DM to a user who has not engaged first. Every supported trigger (comment_keyword, story_reply, dm_reaction, dm_keyword) is anchored to such an engagement, which is what makes the send permissible.
sent means Instagram accepted the message, not that it was delivered. Delivery depends on the recipient’s Instagram Message requests setting — if they don’t allow message requests from everyone, Meta returns a success response and silently drops the message, with no error, webhook, or other signal on any API surface. A successful comment-triggered DM also arrives as a message request the recipient must accept (unless a conversation already exists). This is a permanent Instagram platform limitation. See Automation DM Sent but Not Delivered.
If the account’s Instagram credentials or messaging permissions genuinely fail — for example an expired or revoked access token, a missing Facebook Page selection, or Messaging not activated for the profile — the activity row records status auth_error and the DM is not retried. Resolving the account state (relinking, selecting a Page, or activating Messaging) lets the next matching engagement fire normally. This only applies to genuine credential and permission failures: if a specific DM could not be delivered for another reason — for example the recipient could not be found, or the triggering engagement came from the account that would send the DM — the row is recorded as failed or skipped with the reason on actionResults[].errorDetails, and relinking will not change the outcome.
Each dispatch is scheduled 20–60 seconds after the engagement, so DM sends look organic to Instagram’s anti-spam systems. The delay is applied to the engagement before any rule is matched, so it applies to every action on the rule, fire_webhook and send_email included — expect your webhook to arrive 20–60 seconds after the comment, not immediately. The activity row’s created timestamp is when the trigger matched; completedAt is when dispatch finished.
Activity rows are retained indefinitely for trace and analytics. The GET /automations/:id/activity endpoint returns rows from the last 30 days for performance. (The dedup guard uses its own per-action window — defaulting to 7 days — which is unrelated to the activity lookback.)
No. Delete is a soft-delete: the master row is flagged deleted, no new dispatches occur, but historical activity rows remain readable via the activity endpoint.

Endpoints