Triggers
Entrypoints that start a run, bindings that attach a flow to a channel surface, and the webhook entrypoint for external systems.
An automation reacts to two kinds of inputs:
- Entrypoints — per-flow triggers. Each flow can have many. An event on the channel is matched against all active entrypoints for that channel and the most specific one enrolls the contact.
- Bindings — per-account attachments. One flow gets bound to a channel surface (default reply, welcome message, main menu, etc.). Bindings fire when no entrypoint matches, filling in the "otherwise" slot.
Entrypoints live under an automation and are managed in the flow builder. Bindings live under a social account and are managed in the account detail page.
Entrypoints
The 15 kinds
| Kind | IG | FB | WA | TG |
|---|---|---|---|---|
dm_received | ✓ | ✓ | ✓ | ✓ |
comment_created | ✓ | ✓ | — | — |
story_reply | ✓ | ✓ | — | — |
story_mention | ✓ | ✓ | — | — |
live_comment | ✓ | ✓ | — | — |
ad_click | ✓ | ✓ | — | — |
ref_link_click | ✓ | ✓ | — | — |
share_to_dm | ✓ | — | — | — |
follow | ✓ | — | — | — |
schedule | ✓ | ✓ | ✓ | ✓ |
field_changed | ✓ | ✓ | ✓ | ✓ |
tag_applied | ✓ | ✓ | ✓ | ✓ |
tag_removed | ✓ | ✓ | ✓ | ✓ |
conversion_event | ✓ | ✓ | ✓ | ✓ |
webhook_inbound | ✓ | ✓ | ✓ | ✓ |
Keyword matching — there is no dedicated
keywordkind. Usedm_receivedwith aconfig.keywordsarray (and optionalmatch_mode/case_sensitive) to react to specific words in an inbound DM. The matcher treats an exact- or regex-mode keyword list as specificity 30, identical to the legacykeywordkind.
Shape
{
"id": "aep_...",
"automation_id": "auto_...",
"channel": "instagram",
"kind": "dm_received",
"status": "active",
"social_account_id": "acc_...", // null = all accounts for the channel
"config": { "keywords": ["pizza"], "match_mode": "exact", "case_sensitive": false },
"filters": { "all": [{ "field": "contact.tags", "op": "contains", "value": "vip" }] },
"allow_reentry": true,
"reentry_cooldown_min": 60,
"priority": 100,
"specificity": 30
}Fields:
- kind — one of the 15 above.
- channel — must match the parent automation's channel.
- social_account_id — scopes the entrypoint to a specific account; leave null for all accounts on the channel.
- config — kind-specific (see below).
- filters — segment / tag / field predicates evaluated against the contact before enrolling.
- allow_reentry (default
true) — whether the same contact can re-enter. - reentry_cooldown_min (default
60) — cooldown after run completion before a re-entry is allowed. - priority (default
100, lower wins in ties). - specificity — auto-derived from the kind + config; higher wins.
Config shapes per kind
// dm_received (optional keyword filtering)
// Omit `keywords` to match any inbound DM on the channel/account. Specify
// `keywords` with `match_mode: "exact"` or `"regex"` for a specificity=30
// match — the canonical replacement for the retired `keyword` kind.
{ "keywords": ["pizza", "order"], "match_mode": "exact | contains | regex", "case_sensitive": false }
// comment_created
{ "post_ids": ["17895..."], "keywords": ["link"], "include_replies": false }
// story_reply
{ "story_ids": ["17895..."], "keywords": ["promo"] }
// schedule
{ "cron": "0 9 * * 1", "timezone": "Europe/Rome" }
// field_changed
{ "field_keys": ["stage"], "from": "new", "to": "qualified" }
// tag_applied / tag_removed
{ "tag_ids": ["trial_expired"] }
// ref_link_click
{ "ref_url_ids": ["ref_..."] }
// conversion_event
{ "event_names": ["purchase"] }
// webhook_inbound — see dedicated section belowConflict resolution
Multiple entrypoints can match the same event. The matcher picks one with this sort:
ORDER BY specificity DESC, priority ASC, created_at ASCSpecificity is derived automatically:
| Config | Specificity |
|---|---|
dm_received with keywords + match_mode: exact | regex | 30 |
webhook_inbound (unique slug) | 30 |
Asset-filtered (e.g. comment_created with post_ids) | 25 |
| Filtered with tag/segment/field predicate | 20 |
Account-scoped broad (e.g. dm_received on account X, no filter) | 10 |
| Catch-all (no account, no filter) | 0 |
Use priority to break ties or force a specific order when two entrypoints have identical specificity.
Re-entry
For every incoming match, the matcher checks:
- Does this contact have an active or waiting run for this
(automation_id, entrypoint_id)? → skip. - Does this contact have a completed run within
reentry_cooldown_minof now? → skip. - Is
allow_reentry = falseand the contact has ever been enrolled? → skip.
The cooldown clock starts at the prior run's completed_at.
Contact pause
A contact can be paused per-automation or globally via automation_contact_controls. If any matching pause row exists (with paused_until IS NULL or still in the future), enrollment is skipped. See Runs for the contact-controls API.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /v1/automations/{id}/entrypoints | List entrypoints on an automation |
POST | /v1/automations/{id}/entrypoints | Create |
GET | /v1/automation-entrypoints/{id} | Retrieve |
PATCH | /v1/automation-entrypoints/{id} | Update |
DELETE | /v1/automation-entrypoints/{id} | Remove |
POST | /v1/automation-entrypoints/{id}/rotate-secret | Rotate the webhook secret (for webhook_inbound only) |
Bindings
Bindings attach a flow to a "channel surface" on a specific social account. There are 5 types.
| Type | Channels | v1 status |
|---|---|---|
default_reply | IG, FB, WA, TG | Wired live — fires when an inbound event matches no entrypoint |
welcome_message | IG, FB, WA, TG | Wired live — fires on the contact's first-ever inbound on the channel |
conversation_starter | FB only | Stubbed — storage + UI only; platform sync via messenger_profile.ice_breakers ships in v1.1 |
main_menu | FB, IG | Stubbed — storage + UI only; platform sync via messenger_profile.persistent_menu ships in v1.1 |
ice_breaker | WA only | Stubbed — storage + UI only; WhatsApp platform sync ships in v1.1 |
Shape
{
"id": "abnd_...",
"social_account_id": "acc_...",
"channel": "instagram",
"binding_type": "default_reply",
"automation_id": "auto_...",
"config": {},
"status": "active",
"last_synced_at": null,
"sync_error": null
}Constraint: one binding per (social_account_id, binding_type). Binding the same flow to multiple surfaces means creating multiple binding rows.
Config per type
// default_reply / welcome_message
{}
// automation_id alone is sufficient
// conversation_starter
{ "starters": [{ "label": "Get started", "payload": "start" }] } // max 4
// main_menu
{
"items": [
{ "label": "Shop", "action": "url", "payload": "https://…" },
{ "label": "Contact", "action": "postback", "payload": "contact" }
]
}
// max 3 levels deep, supports sub_items
// ice_breaker
{ "questions": [{ "question": "How do I order?", "payload": "faq_order" }] }Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /v1/automation-bindings | List. Filters: social_account_id, binding_type, automation_id |
POST | /v1/automation-bindings | Create |
GET | /v1/automation-bindings/{id} | Retrieve |
PATCH | /v1/automation-bindings/{id} | Update |
DELETE | /v1/automation-bindings/{id} | Remove |
Stubbed binding types default to status = "pending_sync" at create time — they're stored but not yet pushed to the platform.
Runtime match algorithm
On every inbound channel event, the matcher:
- Loads candidate entrypoints where
channel = X,kind = Y,status = active, and(social_account_id IS NULL OR social_account_id = event.account_id). - Applies the kind-specific config matcher (keyword match, post-id match, cron window, etc.).
- Applies filter predicates against the contact.
- Skips any entrypoint blocked by re-entry rules or contact-pause.
- Sorts by
(specificity DESC, priority ASC, created_at ASC). Takes the first. - Enrolls — creates an
automation_runsrow, dispatches to the runner.
If step 5 yielded nothing, the matcher falls through to bindings:
- welcome_message — fires only on the contact's first-ever inbound on this channel (checked against
inbox_messages). - default_reply — fires otherwise, if a binding exists on
(social_account_id, "default_reply")and isactive.
Webhook entrypoints
webhook_inbound entrypoints let external systems (CRMs, e-commerce stores, etc.) enroll contacts into a flow by POSTing to a unique signed URL.
Creating a webhook entrypoint
POST /v1/automations/{id}/entrypoints
{
"kind": "webhook_inbound",
"channel": "instagram",
"config": {
"contact_lookup": {
"by": "email",
"field_path": "$.customer.email",
"auto_create_contact": true
},
"payload_mapping": {
"order_id": "$.order.id",
"order_total": "$.order.total"
}
}
}The create response includes config.webhook_slug and the plaintext config.webhook_secret exactly once. Store it — subsequent reads return only the slug. Rotate with:
POST /v1/automation-entrypoints/{id}/rotate-secretContact lookup modes
by | Effect |
|---|---|
email | Extract via field_path, match on contacts.email |
phone | Extract via field_path, match on contacts.phone |
platform_id | Match on a platform identifier from the channel |
custom_field | Extract via field_path, match on contacts.custom_fields.<custom_field_key> |
contact_id | Extract via field_path, match on contacts.id |
Set auto_create_contact: true to create a new contact when the lookup misses. Otherwise the request returns 422.
payload_mapping extracts fields from the body into run.context so downstream nodes can reference {{context.order_id}} etc.
Receiving webhooks
Public endpoint: POST /v1/webhooks/automation-trigger/{webhook_slug}
Signature: the caller must compute HMAC-SHA256 over the raw request body using the plaintext secret and send it as:
X-Relay-Signature: sha256=<hex>Example
BODY='{"order":{"id":"1234","total":42.00},"customer":{"email":"alice@example.com"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | awk '{print $2}')
curl -X POST https://api.relayapi.dev/v1/webhooks/automation-trigger/auto-xyz123 \
-H "Content-Type: application/json" \
-H "X-Relay-Signature: sha256=$SIG" \
-d "$BODY"Responses
| Status | Meaning |
|---|---|
202 { run_id } | Enrolled successfully |
401 | Missing or invalid signature |
404 | Unknown webhook_slug |
422 | Contact lookup failed (auto_create_contact is false or required fields are missing) |
The webhook endpoint is account-agnostic — the slug alone identifies the entrypoint, so you can use webhook_inbound without attaching a social_account_id.
Filters
All entrypoints support the same filter shape, evaluated before enrollment:
{
"all": [
{ "field": "contact.tags", "op": "contains", "value": "vip" },
{ "field": "contact.custom_fields.country", "op": "eq", "value": "IT" }
],
"any": [ /* OR group */ ],
"none": [ /* NOT group */ ]
}Field paths: contact.<column>, contact.custom_fields.<slug>, contact.tags, contact.segments. Operators: eq, neq, gt, gte, lt, lte, contains, not_contains, in, not_in, exists, not_exists.
Found something wrong? Help us improve this page.