RelayAPI
GuidesAutomations

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 runtime attachments. Default and welcome bindings provide fallback inbound behavior; Get Started, persistent menus, and ice breakers synchronize provider profile surfaces and route their postbacks into a flow.

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 14 kinds

KindIGFBWATG
dm_received
comment_created
story_reply
story_mention
live_comment
ad_click
ref_link_click
share_to_dm
schedule
field_changed
tag_applied
tag_removed
conversion_event
webhook_inbound

Keyword matching — there is no dedicated keyword kind. Use dm_received with a config.keywords array (and optional match_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 legacy keyword kind.

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,
  "daily_cap": 500,
  "priority": 100,
  "specificity": 30
}

Fields:

  • kind — one of the 14 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.
  • daily_cap (default unlimited) — maximum atomic admissions for this entrypoint per UTC day.
  • 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", "case_sensitive": false, "first_message_only": false }

// comment_created
{ "post_ids": ["17895..."], "keywords": ["link"], "match_mode": "contains", "case_sensitive": false, "include_replies": false }

// story_reply / story_mention
{ "story_ids": ["17895..."], "keywords": ["promo"], "match_mode": "contains", "case_sensitive": false }

// live_comment
{ "keywords": ["question"], "match_mode": "contains", "case_sensitive": false }

// schedule — supported forms: daily M H, hourly, or every N minutes
{ "cron": "0 9 * * *", "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"] }

// share_to_dm
{}

// webhook_inbound — see dedicated section below

An active schedule entrypoint requires at least one enumerable filter in all or any: contact.tags or contact.segments with op: "contains". You may save a paused schedule while configuring it, but activation rejects none groups and arbitrary contact fields instead of creating a job that can only fail at runtime.

Conflict resolution

Multiple entrypoints can match the same event. The matcher picks one with this sort:

ORDER BY specificity DESC, priority ASC, created_at ASC

Specificity is derived automatically:

ConfigSpecificity
dm_received with keywords + match_mode: exact | regex30
webhook_inbound (unique slug)30
Asset-filtered (e.g. comment_created with post_ids)25
Filtered with tag/segment/field predicate20
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:

  1. Does this contact have an active or waiting run for this (automation_id, entrypoint_id)? → skip.
  2. Does this contact have a completed run within reentry_cooldown_min of now? → skip.
  3. Is allow_reentry = false and the contact has ever been enrolled? → skip.

The cooldown clock starts at the prior run's completed_at.

Admission is transactional: active-run, re-entry, cooldown, daily-cap, and run creation checks execute under one contact/automation lock, so concurrent deliveries cannot bypass them.

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

MethodPathPurpose
GET/v1/automations/{id}/entrypointsList entrypoints on an automation
POST/v1/automations/{id}/entrypointsCreate
GET/v1/automation-entrypoints/{id}Retrieve
PATCH/v1/automation-entrypoints/{id}Update
DELETE/v1/automation-entrypoints/{id}Remove
POST/v1/automation-entrypoints/{id}/rotate-secretRotate the webhook secret (for webhook_inbound only)

Bindings

Bindings attach a flow to a runtime or provider-owned surface on a specific social account. There are five supported types.

TypeChannelsAvailability
default_replyIG, FB, WA, TGFires when an inbound event matches no entrypoint
welcome_messageIG, FB, WA, TGFires on the contact's first-ever inbound on the channel
get_startedFBSynchronizes the Messenger Get Started payload; its postback starts the flow
main_menuIG, FBSynchronizes the provider persistent menu; postback items start the flow
ice_breakerIGSynchronizes up to four Instagram ice-breaker questions; postbacks start the flow

Shape

{
  "id": "abnd_...",
  "social_account_id": "acc_...",
  "channel": "instagram",
  "binding_type": "default_reply",
  "automation_id": "auto_...",
  "config": {},
  "status": "active",
  "desired_active": true,
  "sync_revision": 3,
  "last_synced_revision": 3,
  "sync_attempts": 1,
  "last_synced_at": "2026-07-18T10:00:00.000Z",
  "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

// default_reply / welcome_message
{}

// get_started (Facebook)
{ "payload": "GET_STARTED" }

// main_menu (Instagram/Facebook)
{
  "items": [
    { "label": "Help", "action": "postback", "payload": "HELP" },
    { "label": "Website", "action": "url", "url": "https://example.com" }
  ],
  "composer_input_disabled": false
}

// ice_breaker (Instagram; maximum four)
{
  "questions": [
    { "question": "What are your hours?", "payload": "HOURS" }
  ]
}

Provider-synchronized bindings move through pending_sync before becoming active or paused. Writes increment sync_revision; queue workers apply only that exact revision, and a reconciler re-enqueues revisions that have not been acknowledged. sync_failed retains the provider error for repair. Deleting a provider binding first removes its provider profile field and only then removes the local row.

Facebook requires an active, synchronized get_started binding before a main_menu can synchronize. Facebook menus support at most three top-level items; Instagram menus support up to 20.

Endpoints

MethodPathPurpose
GET/v1/automation-bindingsList. Filters: social_account_id, binding_type, automation_id
POST/v1/automation-bindingsCreate
GET/v1/automation-bindings/{id}Retrieve
PATCH/v1/automation-bindings/{id}Update
DELETE/v1/automation-bindings/{id}Remove

Create and update requests accept only status = "active" | "paused".

Runtime match algorithm

On every inbound channel event, the matcher:

  1. Loads candidate entrypoints where channel = X, kind = Y, status = active, and (social_account_id IS NULL OR social_account_id = event.account_id).
  2. Applies the kind-specific config matcher (keyword match, post-id match, cron window, etc.).
  3. Applies filter predicates against the contact.
  4. Skips any entrypoint blocked by re-entry rules or contact-pause.
  5. Sorts by (specificity DESC, priority ASC, created_at ASC). Takes the first.
  6. Enrolls — creates an automation_runs row, 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 is active.

Interactive provider postbacks are matched against the synchronized Get Started, main-menu, or ice-breaker payload on that exact account. A matching active binding enrolls its automation with the same atomic admission protections as an entrypoint.

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 plus top-level webhook_secret_plaintext exactly once. Store the plaintext—subsequent reads return a masked config value. Rotation returns a new webhook_secret_plaintext:

POST /v1/automation-entrypoints/{id}/rotate-secret

Contact lookup modes

byEffect
emailExtract via field_path, match on contacts.email
phoneExtract via field_path, match on contacts.phone
platform_idMatch on a platform identifier from the channel
custom_fieldExtract via field_path, match on contacts.custom_fields.<custom_field_key>
contact_idExtract 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: set X-Relay-Timestamp to the current Unix time in seconds, then compute HMAC-SHA256 over <timestamp>.<raw-request-body> using the plaintext secret. Send both headers:

X-Relay-Signature: sha256=<hex>
X-Relay-Timestamp: <unix-seconds>

Timestamps more than five minutes from the API server's current time are rejected. Sign the exact body bytes sent on the wire; reformatting the JSON after signing invalidates the signature.

Example

BODY='{"order":{"id":"1234","total":42.00},"customer":{"email":"alice@example.com"}}'
TIMESTAMP=$(date +%s)
SIG=$(printf '%s.%s' "$TIMESTAMP" "$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" \
  -H "X-Relay-Timestamp: $TIMESTAMP" \
  -d "$BODY"

Responses

StatusMeaning
202 { run_id }Enrolled successfully
401Missing or invalid signature
404Unknown webhook_slug
409Enrollment was blocked by the entrypoint's active-run, re-entry, cooldown, daily-cap, pause, or filter policy
422Contact lookup failed (auto_create_contact is false or required fields are missing)

The webhook endpoint is account-agnostic for email, phone, contact-ID, and custom-field lookup — the slug alone identifies the entrypoint, so those modes do not require a social_account_id. A platform_id is only unique within a connected account, so that lookup mode requires the entrypoint to be attached to the matching social account.

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.

On this page