Messaging

AI auto-reply automations

Automatically reply to every inbound WhatsApp DM on a number with your AI agent. The automation owns the inbound → agent → send graph; the messages API delivers the reply with the same service-window, opt-out, and billing rules as any other send.

1. Create an AI agent

POST/v1/ai-agents

Create the agent persona the automation will invoke, and keep its id. Agent creation is API/SDK-only today — the dashboard does not yet have an AI agent creation screen.

curl https://api.tyxter.com/v1/ai-agents \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "support_bot",
    "system_prompt": "You are a friendly support agent. Answer briefly."
  }'

2. Create, version, and publish the automation

POST/v1/automations
POST/v1/automations/{automation_id}/versions
POST/v1/automations/{automation_id}/publish

Create the automation, add a version whose graph is the canonical inbound → ai_agent.invokemessage.send shape, then publish that version. Bind the inbound_message trigger to the WhatsApp number that should auto-reply via its phone_number_id. Published versions are immutable; a run always points at the exact version it executes.

3. The contact messages the number

When a contact sends an inbound WhatsApp text, Tyxter receives it, honors the contact's opt-out state, and creates a single automation run. Runs are deduplicated per inbound message, so provider retries never produce a double reply.

4. The agent replies automatically

The run invokes your AI agent with the contact's message and sends the agent's reply back through the messages API. If the agent yields no usable reply, the builder-configured fallback message is sent instead.

One auto-reply per number

At most one enabled inbound_message automation may be bound to a given phone_number_id per environment. Publishing or enabling a second inbound automation for the same number is rejected with 409 automation_inbound_number_conflict.

Only WhatsApp text inbound triggers an auto-reply in this release. Instagram and non-text inbound are not yet supported.

Beyond auto-reply: the workflow engine

An automation graph is a directed, acyclic set of nodes connected by edges. A trigger node starts a run; the runtime walks the graph, persists each node's output, and resumes across waits. The full node config for every type is published as a per-type oneOf in /openapi.json so you (or an agent) can author a valid graph from the spec alone.

Node catalog

Referencing run data with expressions

String config fields are interpolated against the run context by a sandboxed engine — never eval. Three namespaces: trigger.* (the run input), nodes.<id>.output.* (any upstream node's output, surviving delay / time_gate), and run.*.

{ "type": "message.send",
  "config": {
    "to": "{{ trigger.contact_phone }}",
    "from": "{{ trigger.business_number }}",
    "body": "AI says: {{ nodes.agent.output | default:'(no reply)' }}"
  } }

Filters: default, lower, upper, json, date. Boolean expressions (for condition nodes and edge guards) support comparisons, &&/||/!, contains, and an anchored, ReDoS-safe matches. Invalid expressions are rejected when you create the version.

Reliability: retries, timeouts, cancellation

Side-effect nodes carry an optional config.retry = { max_attempts, backoff_ms } (http.request and ai_agent.invoke retry transient failures by default) and a config.timeout_ms budget. A wedged call that exhausts its retries lands the run in timed_out. Cancel a run with the endpoint below; it stops at the next step boundary rather than aborting an in-flight node.

POST/v1/automation-runs/{run_id}/cancel

A successful cancellation emits an automation.run_cancelled webhook.

Authenticated HTTP (credentials vault)

An http.request node authenticates via config.auth = { credential_id }, referencing a stored credential whose secret is encrypted at rest and injected only at send time — it never appears in graph JSON, run summaries, logs, or the OpenAPI spec.

Receiving third-party webhooks

A webhook.trigger can verify a third party's native signature instead of a relay. Set config.verification.mode to stripe ( verifies Stripe-Signature), github (verifies X-Hub-Signature-256), or shared_secret_header; the default tyxter_hmacverifies Tyxter's own signed envelope. A forged or unsigned request is rejected with a 404.

Automation lifecycle API

All API-key routes are scoped to the caller's project and environment. Cross-project identifiers return 404.

List automations

GET/v1/automations

Query

FieldTypeNotes
limitrequiredinteger
Maximum number of items to return. Defaults to 20; maximum 100.
range 1..100
starting_afterstring
Opaque cursor from the previous response next_cursor.
status"draft" | "active" | "paused" | "archived"

Response

FieldTypeNotes
objectrequired"list"
datarequiredobject[]
has_morerequiredboolean
next_cursorrequiredstring | null

Retrieve an automation

GET/v1/automations/{automation_id}

Response

FieldTypeNotes
idrequiredstring
objectrequired"automation"
namerequiredstring
descriptionrequiredstring | null
statusrequired"draft" | "active" | "paused" | "archived"
active_version_idrequiredstring | null
trace_idrequiredstring | null
archived_atrequiredstring<ISO-8601> | null
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

Update an automation

PATCH/v1/automations/{automation_id}

Updates the mutable automation record, not a published version. Send at least one field.

Request body

FieldTypeNotes
namestring
len 1..120
descriptionstring | null
status"draft" | "active" | "paused" | "archived"

Response

FieldTypeNotes
idrequiredstring
objectrequired"automation"
namerequiredstring
descriptionrequiredstring | null
statusrequired"draft" | "active" | "paused" | "archived"
active_version_idrequiredstring | null
trace_idrequiredstring | null
archived_atrequiredstring<ISO-8601> | null
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

Delete an automation

DELETE/v1/automations/{automation_id}

Response

FieldTypeNotes
idrequiredstring
deletedrequired"true"

List immutable versions

GET/v1/automations/{automation_id}/versions

Query

FieldTypeNotes
limitrequiredinteger
Maximum number of items to return. Defaults to 20; maximum 100.
range 1..100
starting_afterstring
Opaque cursor from the previous response next_cursor.

Response

FieldTypeNotes
objectrequired"list"
datarequiredobject[]
has_morerequiredboolean
next_cursorrequiredstring | null

Pause an automation

POST/v1/automations/{automation_id}/pause

Stops new trigger runs while preserving authored versions and existing run history. The request has no JSON body and supports Idempotency-Key.

Response

FieldTypeNotes
idrequiredstring
objectrequired"automation"
namerequiredstring
descriptionrequiredstring | null
statusrequired"draft" | "active" | "paused" | "archived"
active_version_idrequiredstring | null
trace_idrequiredstring | null
archived_atrequiredstring<ISO-8601> | null
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

Resume an automation

POST/v1/automations/{automation_id}/resume

Re-enables a paused automation after validating its active trigger constraints. The request has no JSON body and supports Idempotency-Key.

Response

FieldTypeNotes
idrequiredstring
objectrequired"automation"
namerequiredstring
descriptionrequiredstring | null
statusrequired"draft" | "active" | "paused" | "archived"
active_version_idrequiredstring | null
trace_idrequiredstring | null
archived_atrequiredstring<ISO-8601> | null
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

Rotate a webhook trigger secret

POST/v1/automations/{automation_id}/webhook-triggers/{slug}/rotate-secret

Returns the new plaintext signing secret once. The previous secret remains valid for a ten-minute grace period, so update the sender before discarding this response.

Response

FieldTypeNotes
objectrequired"automation_webhook_secret"
automation_idrequiredstring
automation_version_idrequiredstring
trigger_idrequiredstring
slugrequiredstring
signing_secretrequiredstring

Run API

Create a manual run

POST/v1/automations/{automation_id}/runs

Queues a run against the automation's active version. Requires automations:run and supports the Idempotency-Key header.

Request body

FieldTypeNotes
inputobject
idempotency_keystring
len 1..255
trace_idstring

Response

FieldTypeNotes
idrequiredstring
objectrequired"automation_run"
automation_idrequiredstring
automation_version_idrequiredstring
trigger_idrequiredstring | null
trigger_kindrequired"manual" | "webhook" | "inbound_message" | "flow_completed" | "schedule"
statusrequired"queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "timed_out"
input_summaryrequiredobject | null
output_summaryrequiredobject | null
error_coderequiredstring | null
error_messagerequiredstring | null
current_node_idrequiredstring | null
started_atrequiredstring<ISO-8601> | null
completed_atrequiredstring<ISO-8601> | null
cancelled_atrequiredstring<ISO-8601> | null
trace_idrequiredstring
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

List runs

GET/v1/automations/{automation_id}/runs

Query

FieldTypeNotes
limitrequiredinteger
Maximum number of items to return. Defaults to 20; maximum 100.
range 1..100
starting_afterstring
Opaque cursor from the previous response next_cursor.
status"queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "timed_out"

Response

FieldTypeNotes
objectrequired"list"
datarequiredobject[]
has_morerequiredboolean
next_cursorrequiredstring | null

Retrieve a run

GET/v1/automation-runs/{run_id}

Response

FieldTypeNotes
idrequiredstring
objectrequired"automation_run"
automation_idrequiredstring
automation_version_idrequiredstring
trigger_idrequiredstring | null
trigger_kindrequired"manual" | "webhook" | "inbound_message" | "flow_completed" | "schedule"
statusrequired"queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "timed_out"
input_summaryrequiredobject | null
output_summaryrequiredobject | null
error_coderequiredstring | null
error_messagerequiredstring | null
current_node_idrequiredstring | null
started_atrequiredstring<ISO-8601> | null
completed_atrequiredstring<ISO-8601> | null
cancelled_atrequiredstring<ISO-8601> | null
trace_idrequiredstring
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

List a run's steps

GET/v1/automation-runs/{run_id}/steps

Query

FieldTypeNotes
limitrequiredinteger
Maximum number of items to return. Defaults to 20; maximum 100.
range 1..100
starting_afterstring
Opaque cursor from the previous response next_cursor.

Response

FieldTypeNotes
objectrequired"list"
datarequiredobject[]
has_morerequiredboolean
next_cursorrequiredstring | null

Invoke a webhook trigger

POST/v1/automation-webhooks/{slug}

Send the raw JSON body with the signature headers required by the trigger's verification mode. The default Tyxter HMAC mode signs ${timestamp}.${raw_body} and sends tyxter-webhook-timestamp plus tyxter-webhook-signature. This capability route does not use an API key; invalid signatures and unknown slugs both return 404. Exact signed-request replays are deduplicated for one hour.

Response

FieldTypeNotes
idrequiredstring
objectrequired"automation_run"
automation_idrequiredstring
automation_version_idrequiredstring
trigger_idrequiredstring | null
trigger_kindrequired"manual" | "webhook" | "inbound_message" | "flow_completed" | "schedule"
statusrequired"queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "timed_out"
input_summaryrequiredobject | null
output_summaryrequiredobject | null
error_coderequiredstring | null
error_messagerequiredstring | null
current_node_idrequiredstring | null
started_atrequiredstring<ISO-8601> | null
completed_atrequiredstring<ISO-8601> | null
cancelled_atrequiredstring<ISO-8601> | null
trace_idrequiredstring
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$