Skip to main content

Connecting to Your Backend

API_CALL is the bridge between a conversation flow and your own backend. When the engine enters a state that contains an API_CALL action, it pauses the conversation, fires an outbound HTTP request, saves the response, then routes to the next state based on whether the request succeeded or failed.

{
"fetch_order": {
"message": "Looking that up for you...",
"actions": [
{
"type": "API_CALL",
"integration": "my_backend",
"path": "/orders/{{variables.order_id}}",
"method": "GET",
"save_to": "order",
"success_transition": "show_status",
"failure_transition": "order_error"
}
]
}
}

Configuration

FieldTypeRequiredDescription
typestringyesMust be "API_CALL".
integrationstringName of a saved integration whose base URL and auth credentials are used. Recommended — keeps secrets out of your flow JSON. Mutually exclusive with url.
pathstringPath appended to the integration's base URL. Supports {{variables.*}} substitution. Required when integration is set.
urlstringFull URL (legacy mode — no saved integration). Supports {{variables.*}} substitution.
methodstringnoHTTP method: GET, POST, PUT, PATCH, or DELETE. Defaults to "GET".
headersobjectnoAdditional request headers merged on top of the integration's headers. Values support {{variables.*}} substitution.
bodyobjectnoExtra fields merged into the auto-injected request body. Only applies to POST, PUT, and PATCH. Values support {{variables.*}} substitution.
save_tostringnoVariable name to store the full response under. For example "order" makes the response accessible as {{variables.order.*}}. When omitted the response is stored at {{variables.response}}.
fire_and_forgetbooleannoWhen true, the request fires but the flow does not wait for a response. No data is saved and the conversation transitions immediately. Defaults to false.
success_transitionstringnoState to route to when the server returns HTTP 2xx.
failure_transitionstringnoState to route to when the server returns HTTP 4xx/5xx or the request times out.

Variable substitution

Any path, url, headers, or body value can embed flow variables using {{variables.name}}. The engine resolves them at call time.

{
"type": "API_CALL",
"integration": "my_backend",
"path": "/users/{{variables.user_id}}/orders/{{variables.order_id}}",
"method": "PATCH",
"headers": { "X-Tenant": "{{variables.tenant_id}}" },
"body": { "status": "{{variables.new_status}}" }
}

HTTP method examples

GET — fetch data

Use GET to read data the flow needs before it can continue. The response is saved to a variable whose fields are available in every subsequent message and condition.

Scenario: User asks for their order status. order_id was collected in an earlier state.

{
"type": "API_CALL",
"integration": "my_backend",
"path": "/orders/{{variables.order_id}}",
"method": "GET",
"save_to": "order",
"success_transition": "show_status",
"failure_transition": "order_not_found"
}

If the server returns { "status": "in_transit", "eta": "2026-06-01", "carrier": "DHL" }:

ExpressionResolves to
{{variables.order.status}}"in_transit"
{{variables.order.eta}}"2026-06-01"
{{variables.order.carrier}}"DHL"

POST — send data

Use POST to create a resource or trigger an action. The auto-injected body already contains the user's last message and all current variables — add body fields for anything extra.

Scenario: Submit a support ticket after collecting issue_type and description.

{
"type": "API_CALL",
"integration": "my_backend",
"path": "/tickets",
"method": "POST",
"body": {
"issue_type": "{{variables.issue_type}}",
"description": "{{variables.description}}",
"priority": "normal"
},
"save_to": "ticket",
"success_transition": "ticket_created",
"failure_transition": "ticket_error"
}

The confirmation state can then reference {{variables.ticket.id}} to show the user their reference number.

PATCH — update a record

Use PATCH for partial updates to an existing resource.

{
"type": "API_CALL",
"integration": "my_backend",
"path": "/users/{{variables.user_id}}/preferences",
"method": "PATCH",
"body": { "notifications": "{{variables.notification_choice}}" },
"success_transition": "preference_saved",
"failure_transition": "preference_error"
}

DELETE — remove a resource

DELETE sends no body. Any context your server needs should come from the URL path or the auto-injected X-Sarufi-* headers.

{
"type": "API_CALL",
"integration": "my_backend",
"path": "/subscriptions/{{variables.subscription_id}}",
"method": "DELETE",
"success_transition": "cancellation_confirmed",
"failure_transition": "cancellation_error"
}

Fire and forget

Set fire_and_forget: true when you want to notify your backend without blocking the conversation — analytics events, webhook pings, non-critical logging.

{
"type": "API_CALL",
"integration": "analytics",
"path": "/events",
"method": "POST",
"body": { "event": "flow_completed", "flow": "onboarding" },
"fire_and_forget": true
}
No failure handling with fire_and_forget

The response is ignored entirely — including errors. Do not use it for operations where failure would affect the user experience.


Response access

The full response body is always available at {{variables.response}}. Use save_to to give it a meaningful name.

{ "save_to": "profile" }

Access nested fields with dot notation:

ExpressionResolves to
{{variables.profile.name}}Top-level name field
{{variables.profile.address.city}}Nested city inside address
{{variables.profile.orders[0].id}}First item of an array
Always name your responses

{{variables.order.status}} is much clearer than {{variables.response.status}} in multi-step flows that make several API calls.


Success and failure routing

success_transition fires on any 2xx response. failure_transition fires on 4xx, 5xx, or a network timeout. Both are optional — omitting them falls back to the state's default transition.

API_CALL fires

├── HTTP 2xx ──► success_transition

├── HTTP 4xx/5xx
│ or timeout ──► failure_transition

└── (neither set) ──► default transition

Design failure states to be actionable: explain what went wrong and offer a path forward (retry, contact support, or exit the flow gracefully).


Auto-injected request headers

Every API_CALL automatically includes these identification headers so your server knows who made the call without parsing the body.

HeaderAlways presentValue
X-Sarufi-User-IdyesUser identifier (phone number on WhatsApp, device-scoped ID on web)
X-Sarufi-Chatbot-IdyesChatbot ULID
X-Sarufi-Channelwhen knownwhatsapp, web, ussd, sms, api
X-Sarufi-Statewhen in a stateCurrent state name
X-Sarufi-Flow-Namewhen availableFlow name
X-Sarufi-User-Namewhen setValue of {{variables.contact_name}}

Auto-injected request body

For POST, PUT, and PATCH the engine always sends a base JSON body. Any body fields you declare are merged on top and take precedence.

{
"message": {
"type": "text",
"text": "<the user's latest message>"
},
"variables": {
"<all current conversation variables>"
}
}

GET and DELETE send no body.


Backend examples

from fastapi import Request, FastAPI

app = FastAPI()

@app.get("/orders/{order_id}")
async def get_order(order_id: str, request: Request):
user_id = request.headers.get("X-Sarufi-User-Id")
chatbot_id = request.headers.get("X-Sarufi-Chatbot-Id")
channel = request.headers.get("X-Sarufi-Channel")

order = db.get_order(order_id, user_id=user_id)
return {"status": order.status, "eta": order.eta, "carrier": order.carrier}


@app.post("/tickets")
async def create_ticket(request: Request):
body = await request.json()
user_id = request.headers.get("X-Sarufi-User-Id")

ticket = db.create_ticket(
user_id=user_id,
issue_type=body["variables"].get("issue_type"),
description=body["variables"].get("description"),
)
return {"id": ticket.id, "reference": ticket.reference}

Integration mode vs legacy mode

Integration modeLegacy mode
Configintegration + pathurl only
Auth credentialsStored encrypted in the platformHardcoded in the flow JSON
Credential rotationUpdate the integration once — all flows update automaticallyRequires editing every flow
RecommendedYesOnly when a saved integration is not possible

Set up integrations in the Sarufi dashboard under Settings → Integrations, or via the Integrations API.