Skip to content

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:

  1. Render the advertiser's text exactly. Channel formatting, such as numbering choices or mapping them to chips, is allowed. New sentences aren't.
  2. Show metadata.disclosureText with the first message.
  3. Start at entryNodeId, and stop at an action that ends the flow.
  4. When the person types free text at a choice set, match it against the intents by their description and examples. On a match, report nlu_match and go to the intent's target. With no match, go to noMatchTarget if the node has one. Otherwise, offer the choices once more, and then end the flow with flow_abandon and the reason no_match.
  5. Report events as you go, without waiting for responses.
  6. If the person disengages or objects, end the flow at once, with flow_abandon and the reason user_exit. The conversation matters more than the ad.
  7. 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.