Flow ads¶
A flow ad is a short, bounded conversation written by the advertiser: messages, tappable choices, optional intent matches for free text, and an ending action. Instead of a single card, a fetch returns the whole conversation graph, and your surface runs it locally and reports engagement as it goes. Every word the person sees was written by the advertiser, and the flow needs no server calls while it runs.
Flow ads are opt-in. An integration that doesn't ask for them never receives one. This page describes how to request a flow, the shape of the payload, the rules for running it, and the events it reports.
Request a flow¶
Add adFormat=Flow to a fetch with the default render format, direqt:
curl -X POST "https://ads.advertising.chat/v1/fetch/v2?key=YOUR_FETCH_KEY&adUnit=YOUR_AD_UNIT&adFormat=Flow&subscriber=anon-123" \
-H "Content-Type: application/json" \
-d '{"context": "User just finished planning their morning routine and asked about coffee."}'
A 200 response returns the flow, and a 204 response means no flow fits
the moment. Other render formats return a 422 error for flow requests.
Flows are served only from your own org's rules, and the console's offer editor doesn't author them. To run flow ads, contact the Advertising.chat team. The AdKit MCP server and Node SDK don't request flows, so request them from the Fetch API directly or with the web embed.
The payload¶
{
"messageId": "direqt-…", // the fetchEventId for every flow event
"format": "direqt",
"payload": {
"specVersion": "1",
"entryNodeId": "hook",
"metadata": {
"advertiserName": "Acme Coffee",
"disclosureText": "Sponsored · Acme Coffee", // always render
"teaser": "Better coffee in two taps", // optional
"language": "en"
},
"nodes": [
{ "id": "hook", "type": "message",
"text": "Craving better coffee? We roast to order.",
"next": "pick" },
{ "id": "pick", "type": "choice-set",
"prompt": "What do you brew at home?",
"choices": [
{ "id": "esp", "label": "Espresso", "target": "detail" },
{ "id": "no", "label": "No thanks", "target": "bye" }
],
"intents": [
{ "intent": "price_question",
"description": "The user asks about price, cost, or discounts.",
"examples": ["how much is it?"],
"target": "detail" }
] },
{ "id": "detail", "type": "action",
"text": "Fresh-roasted beans, shipped within 48 hours.",
"action": { "kind": "link_out",
"url": "https://…click-tracking…",
"label": "Shop beans" } },
{ "id": "bye", "type": "action", "action": { "kind": "end" } }
],
"creativeId": "…"
},
"properties": {}
}
A flow has three node types:
| Type | Renders as | Then |
|---|---|---|
message |
One message, with text and an optional imageUrl. |
Follow next. |
choice-set |
A prompt and up to four quick replies in choices, with optional intents for free-text matching. |
Go to the chosen reply's target. |
action |
Optional closing text and the action itself. |
link_out, handoff, and end end the flow. conversion can continue through next. |
The URLs in link_out and handoff actions are click-tracking links. Use
them exactly as returned.
Flows are bounded: at most 12 nodes, four choices and four intents per
choice set, six turns deep, and 16 KB serialized. The JSON Schema is
published at /schemas/flow-creative-1.json.
Running a flow¶
A surface that runs a flow follows these rules:
- Render the advertiser's text exactly. Channel formatting, such as numbering choices or mapping them to chips, is allowed. New sentences aren't.
- Show
metadata.disclosureTextwith the first message. - Start at
entryNodeId, and stop at an action that ends the flow. - When the person types free text at a choice set, match it against the
intentsby theirdescriptionandexamples. On a match, reportnlu_matchand go to the intent's target. With no match, go tonoMatchTargetif the node has one. Otherwise, offer the choices once more, and then end the flow withflow_abandonand the reasonno_match. - Report events as you go, without waiting for responses.
- If the person disengages or objects, end the flow at once, with
flow_abandonand the reasonuser_exit. The conversation matters more than the ad. - Run one flow at a time, and never re-enter a finished flow.
If specVersion has a major version you don't support, don't run the flow.
Report flow_abandon with the reason unsupported_spec. Ignore unknown
fields, which minor versions can add.
On SMS and other text-only channels, choices become numbered options, such
as 1 Espresso 2 No thanks, and the smsText and smsPrompt variants
replace text and prompt when a node has them. Intents fall back to
keyword matching against their examples.
Flow events¶
Flow events go to the Events API with
version: 2 and the fetch's messageId as fetchEventId:
curl -X POST "https://ads.advertising.chat/v1/event?key=YOUR_FETCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"version": 2,
"eventType": "choice_tap",
"eventData": { "nodeId": "pick", "choiceId": "esp" },
"fetchEventId": "direqt-…"
}'
A 201 response confirms the event, and a 422 response means the
eventType or eventData is invalid.
eventType |
When | eventData |
|---|---|---|
node_visit |
A node is shown. | nodeId |
choice_tap |
The person picks a choice. | nodeId, choiceId |
nlu_match |
Free text matched an intent. | nodeId, intent |
flow_complete |
The flow reached an action that ends it. | terminalNodeId, turns |
flow_abandon |
The flow ended early. | lastNodeId, turns, reason: user_exit, no_match, timeout, or unsupported_spec |
handoff_click |
The person opened a handoff. | nodeId, handoffKind: url |
Flow events never carry the person's words. nlu_match reports the intent's
identifier, not the text that matched it, and unexpected eventData keys
are dropped when the event arrives. Taps on the click-tracking links are
recorded even if your surface sends no flow events.