Skip to content

Serving conversational flow ads

A flow ad is a small, bounded, advertiser-authored conversation: message nodes, tappable choices, optional natural-language intent edges, and explicit ending actions. Instead of a card, your fetch returns the whole conversation graph — your surface (an AdKit-guided LLM, your bot runtime, or the web embed) executes it locally and reports engagement as it goes. No server round trips mid-flow, and no generated copy: every word the user sees was written by the advertiser.

Flow ads are opt-in. Integrations that never ask for them never receive one.

Requesting a flow

Add adFormat=Flow to your fetch (render format direqt, the default):

curl -s "https://ads.advertising.chat/v1/fetch/v2?key=YOUR_API_KEY&adUnit=YOUR_AD_UNIT&adFormat=Flow&subscriber=anon-123" \
  -X POST -H "Content-Type: application/json" \
  -d '{"context": "User just finished planning their morning routine and asked about coffee."}'

A 204 means no fill (a normal outcome — show nothing). A 200 returns the flow payload.

The payload shape

{
  "messageId": "direqt-…",     // your fetchEventId for ALL flow events
  "format": "direqt",
  "payload": {
    "specVersion": "1",        // reject unknown majors (see version gate)
    "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-wrapped…",
                    "label": "Shop beans" } },
      { "id": "bye", "type": "action", "action": { "kind": "end" } }
    ],
    "creativeId": "…"
  },
  "properties": {}
}

Three node types:

Type Renders as Then
message One message (text, optional imageUrl) follow next
choice-set A prompt + up to 4 quick replies (choices); optional intents for free-text matching jump to the tapped choice's target
action Optional closing text + the action affordance link_out / handoff / end are terminal; conversion may continue via next

link_out and handoff URLs arrive already click-wrapped (shortened redirect URLs that record the click before forwarding). Use them verbatim.

The full JSON Schema is published at /schemas/flow-creative-1.json. Flows are hard-bounded: at most 12 nodes, 4 choices per set, 6 user-visible turns deep, 16 KB serialized.

Execution rules

The normative executor contract is the Ad Skill's Flow Execution addendum. The short version:

  1. Render advertiser copy verbatim — channel furniture (numbering, chip mapping) is fine; new sentences are not.
  2. Present metadata.disclosureText with the first message, always.
  3. Walk the graph from entryNodeId; stop at terminal actions.
  4. Free text on a choice-set: match against intents by their description/examples; report nlu_match and jump. No match → use noMatchTarget, else re-offer once, then exit with flow_abandon (no_match).
  5. Report events as you go (below) — fire-and-forget.
  6. If the user disengages or objects, exit immediately (flow_abandon, user_exit). The conversation outranks the ad.
  7. One flow at a time; never re-enter a finished flow.

Version gate: if specVersion has a major you don't know, don't run the flow — report flow_abandon with reason unsupported_spec. Ignore unknown fields added by minor versions.

Reporting engagement

All flow events go to the existing event endpoint with version: 2 and the fetch's messageId as fetchEventId:

curl -s "https://ads.advertising.chat/v1/event?key=YOUR_API_KEY" \
  -X POST -H "Content-Type: application/json" \
  -d '{
    "version": 2,
    "eventType": "choice_tap",
    "eventData": { "nodeId": "pick", "choiceId": "esp" },
    "fetchEventId": "direqt-…"
  }'

201 on success; 422 if the eventType or eventData is invalid.

eventType When eventData
node_visit A node is presented { "nodeId" }
choice_tap User taps / numbers a choice { "nodeId", "choiceId" }
nlu_match Free text matched an intent { "nodeId", "intent" }
flow_complete A terminal action reached { "terminalNodeId", "turns" }
flow_abandon Executor exits early { "lastNodeId", "turns", "reason" } — reason: user_exit, no_match, timeout, or unsupported_spec
handoff_click Handoff affordance activated { "nodeId", "handoffKind": "url" }

Privacy rule: flow events never carry the user's words. nlu_match reports the intent id, not the utterance; unexpected eventData keys are dropped at ingress. Clicks on the wrapped URLs are tracked automatically even if you send no events at all.

Webchat execution example

A minimal LLM-side loop (the AdKit MCP server + Ad Skill handle this for you on the MCP path):

fetch_ad(ad_format: "Flow") → payload

You: Sponsored · Acme Coffee
     Craving better coffee? We roast to order.
     [record_event node_visit {nodeId: "hook"}]
You: What do you brew at home?   [Espresso] [No thanks]
     [record_event node_visit {nodeId: "pick"}]

User: how much is a bag?
     → matches intent "price_question" (description: "asks about price…")
     [record_event nlu_match {nodeId: "pick", intent: "price_question"}]

You: Fresh-roasted beans, shipped within 48 hours.  [Shop beans]
     [record_event node_visit {nodeId: "detail"}]
     [record_event flow_complete {terminalNodeId: "detail", turns: 3}]

User: (taps Shop beans) → the wrapped URL records the click and redirects.

On SMS-class channels, choices degrade to numbered options (1 Espresso 2 No thanks — reply with a number), smsText/smsPrompt variants are preferred when present, and intent edges degrade to keyword matching against their examples.