Automations
Condition operators, payload templating, and the six action types.
An automation is a rule stored on your panel: when an event of a given type arrives and every condition matches, run an action. No infrastructure needed on your side.
curl -X POST "$BASE/api/crm/$CRM/automations" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Thank big tippers on Discord",
"trigger_event": "new_tip",
"conditions": [
{ "field": "payload.amount", "op": "gt", "value": 20 }
],
"action_type": "discord",
"action_params": {
"url": "https://discord.com/api/webhooks/…",
"message": "💸 {payload.fan.username} tipped ${payload.amount}"
},
"is_active": true
}'Conditions
A list, joined with AND. Every condition must match for the action to run. An empty list matches every event of that type.
{
"conditions": [
{ "field": "payload.amount", "op": "gt", "value": 5 },
{ "field": "payload.fan.username", "op": "neq", "value": "banned_user" }
]
}field is a dotted path into the event envelope, so
event_type, of_user_id, occurred_at and anything under payload. are all
addressable.
Operators
op | Meaning |
|---|---|
eq | equal |
neq | not equal |
gt / gte | greater than / or equal |
lt / lte | less than / or equal |
contains | substring |
startswith | prefix |
in | the field's value appears in the list you supply |
Actions
action_type | action_params |
|---|---|
webhook | { "url", "method"?, "headers"?, "body"? } — unsigned and un-retried, unlike a first-party webhook |
discord | { "url", "message", "username"? } |
slack | { "url", "message" } |
telegram | { "bot_token", "chat_id", "message" } |
send_dm | { "message", "to_fan_id"? } — defaults to payload.fan.id |
tag_fan | { "tag": "vip" } |
Templating
Any message supports {dotted.path} interpolation against the event, resolved at
dispatch:
"💸 {payload.fan.username} tipped ${payload.amount} on {occurred_at}"Templating applies to every string in action_params — url, tag,
to_fan_id, nested objects and arrays — not just message. Credential keys
(bot_token, secret, token, api_key, password) are deliberately excluded
and passed through byte-for-byte.
An unresolvable path renders as an empty string, not as literal {…}. A typo
silently leaves a hole in the message ("💸 tipped $") rather than announcing
itself — check the first delivery.
send_dm needs write actions enabled
send_dm is an OnlyFans write and is gated per account by
allow_of_write_actions. Enable it via
PATCH /accounts/{of_user_id}/polling. Otherwise the action is logged as a
failed run in GET /automations/{id}/runs with
error_snippet: "send_dm blocked: account has allow_of_write_actions=false".
There is no HTTP error to observe — automations run in the event fan-out, not in a
request. See Connect an account.
Managing automations
GET /automations # list
POST /automations # create
GET /automations/{id} # read one
PATCH /automations/{id} # update (send {"is_active": false} to pause)
DELETE /automations/{id}
POST /automations/{id}/run-now # fire the action immediately, for testing
GET /automations/{id}/runs # run historyrun-now validates an action without waiting for a real event — but it builds a
synthetic event from the sample_payload you supply, so send one. Without it
the payload is empty, every payload.* condition fails, and the run is recorded as
skipped.
curl -X POST "$BASE/api/crm/$CRM/automations/$ID/run-now" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"sample_payload": {"amount": 25, "fan": {"id": "987", "username": "somefan"}}}'The response is {"status": "success" | "skipped" | "failed"}. GET /runs
(default 50, max 200) has the history.
POST /automations also accepts of_user_id to scope a rule to one connected
account — the per-account routing that webhooks cannot do.
Condition operators are not validated on create
An unknown or misspelled op is accepted, then fails every comparison at dispatch,
so the automation silently never fires. Check /runs after creating a rule.
Worked examples
{
"name": "VIP tagging",
"trigger_event": "new_purchase",
"conditions": [{ "field": "payload.amount", "op": "gte", "value": 100 }],
"action_type": "tag_fan",
"action_params": { "tag": "vip" }
}The tag then shows up in GET /fans, so you can segment on it later — including
as a mass-DM audience filter. See Messaging.
{
"name": "Account needs reconnecting",
"trigger_event": "polling_paused",
"conditions": [],
"action_type": "telegram",
"action_params": {
"bot_token": "123456:ABC…",
"chat_id": "-1001234567890",
"message": "⚠️ Polling paused for {of_user_id} — {payload.reason} ({payload.failures} failures)"
}
}This is the single most useful automation to set up first. It is the earliest reliable signal that a session has died — see Sessions.
{
"name": "Welcome DM",
"trigger_event": "new_subscriber",
"conditions": [],
"action_type": "send_dm",
"action_params": {
"message": "Hey {payload.fan.display_name}, thanks for subscribing 💕",
"to_fan_id": "{payload.fan.id}"
}
}Note to_fan_id is templated from the event, so one rule covers every new
subscriber.
Automations, webhooks, or your own code?
Automations are the right tool when the reaction is a notification or a simple one-step side effect. Once you need branching logic, external data, or state, use a webhook and do the work in your own service.
Both run from the same event emission, so you can use them together — an automation for the Discord ping, a webhook for your database.