Commerce Flows API & MCP

Create, copy, publish, send, and analyze Meta WhatsApp Flows with ChatYug’s External API (Bearer ck_…) or MCP tools. Third parties can author full screen designs via flowJson without the portal; operators may still prefer the Flow Design UI.

Prerequisites

  • Active ChatYug API key or MCP connection scoped to the WABA
  • Flows published on Meta before customer send
  • Valid phoneNumberId for send

Base URL

https://chatyug.com/api/external

Manage flows

MethodPathMCP tool
GET/commerce/flowslist_commerce_flows
GET/commerce/flows/:flowIdget_commerce_flow
POST/commerce/flowscreate_commerce_flow
POST/commerce/flows/media—
POST/commerce/flows/:flowId/copycopy_commerce_flow
PUT/commerce/flows/:flowIdupdate_commerce_flow (confirm)
POST/commerce/flows/:flowId/publishpublish_commerce_flow

flowId in manage / copy paths is the local ChatYug flow id. For send, pass Meta’s meta_flow_id as flowId.

Generate a flow design via API

You can create a complete WhatsApp Flow by posting Meta-compatible flowJson. ChatYug normalizes it (routing model, Footer payloads, media picker rules), uploads it to Meta as a DRAFT, and stores it for the portal designer.

Create

curl -X POST https://chatyug.com/api/external/commerce/flows \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Lead capture",
    "category": "LEAD_GENERATION",
    "flowJson": {
      "version": "7.3",
      "routing_model": { "LEAD_SCREEN": [] },
      "screens": [{
        "id": "LEAD_SCREEN",
        "title": "Lead",
        "terminal": true,
        "layout": {
          "type": "SingleColumnLayout",
          "children": [
            { "type": "TextHeading", "text": "Hello ${data.customer_name}" },
            { "type": "TextBody", "text": "Interested in ${data.product_name}?" },
            { "type": "TextInput", "name": "full_name", "label": "Your name", "required": true, "input-type": "text" },
            {
              "type": "Footer",
              "label": "Submit",
              "on-click-action": {
                "name": "complete",
                "payload": { "full_name": "${form.full_name}" }
              }
            }
          ]
        }
      }]
    }
  }'

Update draft

curl -X PUT https://chatyug.com/api/external/commerce/flows/12 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Lead capture v2", "flowJson": { /* full document */ } }'

After JSON upload, a previously published Meta flow returns to DRAFT until you call publish again.

Copy Flow

Clone an existing flow into the same WABA (API key WABA) or another WABA owned by the same ChatYug account. Creates a new Meta DRAFT and a new local id / meta_flow_id. Analytics and publish status are not copied.

curl -X POST https://chatyug.com/api/external/commerce/flows/12/copy \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Lead capture (Copy)", "targetWabaId": "OPTIONAL_OTHER_OWNED_WABA" }'

MCP: copy_commerce_flow with flowId, optional name, optional targetWabaId.

Different ChatYug user: there is no privileged cross-tenant copy. Export with source key (GET /commerce/flows/:id → flow.flow_json), then POST /commerce/flows with the destination key.

flowJson shape

FieldNotes
versionUse 7.3 (ChatYug bumps older versions on save).
routing_modelMap of screen id → array of next screen ids. Built automatically if omitted.
screens[]id (UPPER_SNAKE), title, terminal, optional data model, layout.
layout{ "type": "SingleColumnLayout", "children": [ …components ] }
_chatyug_canvasOptional portal layout sidecar; stripped before Meta upload.
data_api_versionSet when using data_exchange; otherwise ChatYug may strip it.

Node / component JSON (API)

Every portal designer component can be authored as Meta JSON in flowJson.screens[].layout.children. UI guide: Flow Design Guide — components. Below: purpose, fields, and paste-ready snippets for the External API.

Quick index: TextHeading · TextSubheading · TextBody · TextCaption · RichText · TextInput · TextArea · Dropdown · Radio · Checkbox · DatePicker · Calendar · OptIn · Image · ImageCarousel · PhotoPicker · DocumentPicker · EmbeddedLink · NavigationList · Footer

TextHeading

Screen title. Required field: text.

{ "type": "TextHeading", "text": "Register interest" }

TextSubheading

Secondary title under the heading. Field: text.

{ "type": "TextSubheading", "text": "We will call you back" }

TextBody

Instructions or dynamic copy. Field: text (may use ${data.*}).

{ "type": "TextBody", "text": "Order ${data.order_id} — ${data.product_name}" }

TextCaption

Small hint / legal note. Field: text.

{ "type": "TextCaption", "text": "GST invoice will be emailed" }

RichText

Light markdown-style body (**bold**). Field: text.

{ "type": "RichText", "text": "Please review **before** submitting." }

TextInput

Short answer. Fields: name (unique, lowercase/underscore), label, required, input-type (text|number|email|phone|…).

{
  "type": "TextInput",
  "name": "full_name",
  "label": "Your name",
  "required": true,
  "input-type": "text"
}

Pitfall: never reuse the same name on two fields in one flow.

TextArea

Long text. Fields: name, label, required.

{
  "type": "TextArea",
  "name": "notes",
  "label": "Additional notes",
  "required": false
}

Dropdown

Single choice from a compact list. Fields: name, label, required, data-source (≥2 { id, title }).

{
  "type": "Dropdown",
  "name": "payment_method",
  "label": "Payment",
  "required": true,
  "data-source": [
    { "id": "cod", "title": "Cash on delivery" },
    { "id": "upi", "title": "UPI" }
  ]
}

RadioButtonsGroup

Visible single-choice list. Same option shape as Dropdown.

{
  "type": "RadioButtonsGroup",
  "name": "rating",
  "label": "How was your experience?",
  "required": true,
  "data-source": [
    { "id": "good", "title": "Good" },
    { "id": "ok", "title": "OK" },
    { "id": "bad", "title": "Bad" }
  ]
}

CheckboxGroup

Multi-select. Same data-source pattern (≥2 options).

{
  "type": "CheckboxGroup",
  "name": "interests",
  "label": "Topics",
  "required": false,
  "data-source": [
    { "id": "offers", "title": "Offers" },
    { "id": "new", "title": "New products" }
  ]
}

DatePicker

Date field. Fields: name, label, required.

{
  "type": "DatePicker",
  "name": "delivery_date",
  "label": "Preferred delivery date",
  "required": true
}

CalendarPicker

Calendar-style date UI. Fields: name, label, required.

{
  "type": "CalendarPicker",
  "name": "appointment_date",
  "label": "Appointment",
  "required": true
}

OptIn

Consent / terms. Fields: name, label, required.

{
  "type": "OptIn",
  "name": "terms_ok",
  "label": "I agree to the terms",
  "required": true
}

Image

Show a product/brand image. Field: src — public HTTPS URL (Meta must fetch it). Portal paste-URL only; External API can host via POST /commerce/flows/media.

{ "type": "Image", "src": "https://chatyug.com/flow_design_media/62/flow-img-….jpg" }

ImageCarousel

Multiple images. Field: images — array of { "src": "https://…" }.

{
  "type": "ImageCarousel",
  "images": [
    { "src": "https://example.com/a.jpg" },
    { "src": "https://example.com/b.jpg" }
  ]
}

PhotoPicker

Customer uploads a photo. Fields: name, label, max-file-size-kb, min-uploaded-photos / max-uploaded-photos.

{
  "type": "PhotoPicker",
  "name": "id_photo",
  "label": "Upload ID photo",
  "max-file-size-kb": 5120,
  "min-uploaded-photos": 1,
  "max-uploaded-photos": 1
}

Pitfall: never send Meta required on PhotoPicker (even false). Use min-uploaded-photos: 1 for “Require upload”. Submitted files appear in analytics attachments.

DocumentPicker

Customer uploads a document (PDF etc.). Same Meta rule — no required; use min/max uploaded documents.

{
  "type": "DocumentPicker",
  "name": "invoice_pdf",
  "label": "Upload invoice",
  "max-file-size-kb": 10240,
  "min-uploaded-documents": 1,
  "max-uploaded-documents": 1
}

Inline tappable link. Fields: text, on-click-action with name: "open_url" and url (HTTPS).

{
  "type": "EmbeddedLink",
  "text": "Read terms",
  "on-click-action": {
    "name": "open_url",
    "url": "https://example.com/terms"
  }
}

NavigationList

Menu that jumps to other screens. Field: list-items with navigate actions; target screen ids must exist.

{
  "type": "NavigationList",
  "name": "main_menu",
  "label": "Choose a path",
  "list-items": [
    {
      "id": "to_support",
      "main-content": { "title": "Support" },
      "on-click-action": {
        "name": "navigate",
        "next": { "type": "screen", "name": "SUPPORT_SCREEN" },
        "payload": {}
      }
    },
    {
      "id": "to_sales",
      "main-content": { "title": "Sales" },
      "on-click-action": {
        "name": "navigate",
        "next": { "type": "screen", "name": "SALES_SCREEN" },
        "payload": {}
      }
    }
  ]
}

CTA on every interactive / terminal screen. Fields: label, on-click-action.

  • complete — finish flow; payload should include ${form.field} for each input.
  • navigate — go to another screen; put forwarded values in payload (become ${data.*} on the next screen).
  • data_exchange — endpoint/checkout flows (requires Commerce Settings RSA + flow endpoint).
{
  "type": "Footer",
  "label": "Submit",
  "on-click-action": {
    "name": "complete",
    "payload": {
      "full_name": "${form.full_name}",
      "city": "${form.city}"
    }
  }
}

ChatYug auto-syncs complete payloads from form field names on save if you omit them.

Upload images for Image / ImageCarousel

Portal Flow Design keeps paste-HTTPS URL only. Via API, upload a file and use the returned public URL as src:

curl -X POST https://chatyug.com/api/external/commerce/flows/media \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "mediaFile=@./product.jpg"
{
  "success": true,
  "url": "https://chatyug.com/flow_design_media/62/flow-img-….jpg",
  "path": "/flow_design_media/62/flow-img-….jpg"
}

Limits: JPEG / PNG / WebP, max 5MB, multipart field name mediaFile. You may also use any other public HTTPS URL Meta can fetch (same as the portal).

Multi-screen create recipe

Screen DETAILS collects city → Footer navigate → terminal CONFIRM shows ${data.city} and completes.

curl -X POST https://chatyug.com/api/external/commerce/flows \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "City confirm",
    "category": "OTHER",
    "flowJson": {
      "version": "7.3",
      "routing_model": { "DETAILS": ["CONFIRM"], "CONFIRM": [] },
      "screens": [
        {
          "id": "DETAILS",
          "title": "Details",
          "terminal": false,
          "layout": {
            "type": "SingleColumnLayout",
            "children": [
              { "type": "TextHeading", "text": "Where should we deliver?" },
              { "type": "TextInput", "name": "city", "label": "City", "required": true, "input-type": "text" },
              {
                "type": "Footer",
                "label": "Continue",
                "on-click-action": {
                  "name": "navigate",
                  "next": { "type": "screen", "name": "CONFIRM" },
                  "payload": { "city": "${form.city}" }
                }
              }
            ]
          }
        },
        {
          "id": "CONFIRM",
          "title": "Confirm",
          "terminal": true,
          "data": {
            "city": { "type": "string", "__example__": "Bharuch" }
          },
          "layout": {
            "type": "SingleColumnLayout",
            "children": [
              { "type": "TextHeading", "text": "Confirm" },
              { "type": "TextBody", "text": "Deliver to ${data.city}" },
              {
                "type": "Footer",
                "label": "Submit",
                "on-click-action": {
                  "name": "complete",
                  "payload": { "city": "${data.city}" }
                }
              }
            ]
          }
        }
      ]
    }
  }'

Then POST /commerce/flows/:id/publish and POST /messages/flow with the returned Meta meta_flow_id.

How ChatYug handles data

  • ${form.field_name} — value of an input on the same screen (Footer complete / navigate payload).
  • ${data.key} — screen data model: filled by navigate payload from a previous screen, initialScreenData on send, declared screen.data, known init keys (order_id, product_name, order_total, customer_name), or a form field name used elsewhere in the flow.
  • On save, ChatYug runs normalizeFlowJsonForStorage: ensures terminal Footers, syncs complete payloads from form names, sanitizes pickers, builds routing_model when missing, and auto-declares screen.data on navigate targets from inbound payload keys (Meta requires this).

Pass previous-screen values

{
  "type": "Footer",
  "label": "Continue",
  "on-click-action": {
    "name": "navigate",
    "next": { "type": "screen", "name": "CONFIRM" },
    "payload": { "city": "${form.city}" }
  }
}
// On CONFIRM screen:
{ "type": "TextBody", "text": "You selected ${data.city}" }

Product / order info (on send, not create)

Design with placeholders such as ${data.product_name}. At send time, inject real values:

curl -X POST https://chatyug.com/api/external/messages/flow \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "YOUR_PHONE_NUMBER_ID",
    "to": "919876543210",
    "flowId": "META_FLOW_ID",
    "bodyText": "Complete your order",
    "ctaText": "Open",
    "initialScreenId": "CHECKOUT_SCREEN",
    "initialScreenData": {
      "order_id": "42",
      "product_name": "Amul Protein Water × 1",
      "order_total": "INR 251.00",
      "items_count": "1",
      "locale": "en"
    }
  }'

Build product_name / totals from GET /orders/:orderId (items[]) or the cart webhook’s data.chatyug. MCP: same fields on send_commerce_flow_message (ctaLabel maps to API ctaText).

End-to-end

  1. (Optional) POST /commerce/flows/media → put returned url into Image / ImageCarousel
  2. POST /commerce/flows with flowJson (or …/copy)
  3. POST /commerce/flows/:id/publish
  4. POST /messages/flow with Meta flowId + optional initialScreenData
  5. Parse inbound nfm_reply.response_json (below) and/or pull analytics + media

Fetch design + analytics + media (external)

  1. Design: GET /commerce/flows/:flowId → flow.flow_json (screens/components).
  2. Submissions: GET /commerce/flows/analytics?flowId=:id&eventType=submit → each event has payload, attachments[], hasMedia.
  3. Files: for each attachment, GET attachments[].downloadUrl with your API key (or use export?format=json / Excel).
# 1. Full design
curl "https://chatyug.com/api/external/commerce/flows/12" \
  -H "Authorization: Bearer YOUR_API_KEY"

# 2. Customer submissions (includes attachments metadata)
curl "https://chatyug.com/api/external/commerce/flows/analytics?flowId=12&eventType=submit" \
  -H "Authorization: Bearer YOUR_API_KEY"

# 3. Download one PhotoPicker/DocumentPicker file
curl -L -o id_photo.jpg \
  "https://chatyug.com/api/external/commerce/flows/analytics/events/69/media/id_photo" \
  -H "Authorization: Bearer YOUR_API_KEY"

Webhook nfm_reply often has Meta media IDs only — use analytics attachments (or order checkout_submission.attachments) for ChatYug download URLs. Details below under Flow analytics and Access uploaded photos and documents.

Send a flow message

POST /messages/flow · MCP send_commerce_flow_message

curl -X POST https://chatyug.com/api/external/messages/flow \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "YOUR_PHONE_NUMBER_ID",
    "to": "919876543210",
    "flowId": "META_FLOW_ID",
    "bodyText": "Please complete this form",
    "ctaText": "Open"
  }'

Successful sends are logged as analytics send events. Customer completions (nfm_reply / flow endpoint) become submit events. Portal Test Send and API send also write an outbound “Flow sent” bubble in Conversations.

Receive filled data (nfm_reply)

When the customer completes the flow, Meta delivers a normal inbound message webhook with interactive.type = "nfm_reply". ChatYug forwards this on your customer webhook and stores the same fields in Conversations + Flow Analytics.

  1. Parse data.message.interactive.nfm_reply.response_json (it is a JSON string).
  2. Keys in that object match the designer input Name (e.g. customer_name → ${form.customer_name} in the canvas).
  3. Correlate with the token you passed as flowToken on send — it is usually present as flow_token inside response_json.

Cart checkout shortcut: when flowToken equals a commerce order’s flow_token, GET /api/external/orders/:orderId returns checkout_submission with the same submit payload and media attachments[] (download URLs). Prefer that for order fulfilment; use analytics list/media for non-order forms.

{
  "eventType": "message",
  "data": {
    "from": "919876543210",
    "message": {
      "type": "interactive",
      "interactive": {
        "type": "nfm_reply",
        "nfm_reply": {
          "name": "flow",
          "body": "Sent",
          "response_json": "{\"flow_token\":\"flow_1720856400000_abc\",\"customer_name\":\"Rahul\",\"city\":\"Bharuch\"}"
        }
      }
    }
  }
}
// Node — end-to-end correlate
const reply = payload.data.message.interactive.nfm_reply;
const answers = JSON.parse(reply.response_json || '{}');
const token = answers.flow_token; // same value returned from POST /messages/flow
// Load your CRM lead/order by token, then map answers.customer_name, answers.city, …
curl -X POST https://chatyug.com/api/external/messages/flow \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "YOUR_PHONE_NUMBER_ID",
    "to": "917016793529",
    "flowId": "META_FLOW_ID",
    "flowToken": "lead_42_checkout",
    "bodyText": "Please complete this form",
    "ctaText": "Open"
  }'
# Later, on your webhook: JSON.parse(nfm_reply.response_json).flow_token === "lead_42_checkout"

Portal views of the same events: Conversations (Flow sent / Flow response bubbles) and Flow Design → Analytics (structured send/submit table + Excel).

Also see Webhook Settings samples and API docs §4.5.

Pre-publish checklist

  • Terminal screens include a Footer
  • Screen IDs are unique UPPER_SNAKE_CASE
  • Field names unique flow-wide (lowercase / underscore)
  • Dropdown / Radio / Checkbox have ≥2 options
  • Image src is HTTPS when set
  • Footer / NavigationList navigate targets exist
  • If any Footer uses data_exchange, configure Commerce Settings flow endpoint (+ RSA)

ChatYug runs these checks on Approve in the designer and list before calling Meta.

Flow analytics

MethodPathMCP tool
GET/commerce/flows/analyticslist_flow_analytics
GET/commerce/flows/analytics/summaryget_flow_analytics_summary
GET/commerce/flows/analytics/export—
GET/commerce/flows/analytics/events/:eventId/media/:fieldNameget_flow_media

Query params: flowId (local id, recommended), metaFlowId, eventType (send|submit), phone, fromDate, toDate, page, pageSize. Export: format=xlsx (default) or json — export always returns submit rows only (customer answers).

PhotoPicker / DocumentPicker answers are downloaded into ChatYug storage on submit. List responses include a parsed payload object; media items gain chatyug_url and chatyug_media_type (image|document|video from MIME — there is no VideoPicker).

curl "https://chatyug.com/api/external/commerce/flows/analytics?flowId=12&page=1&pageSize=50" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://chatyug.com/api/external/commerce/flows/analytics/summary?flowId=12" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl -OJ "https://chatyug.com/api/external/commerce/flows/analytics/export?flowId=12&format=xlsx" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://chatyug.com/api/external/commerce/flows/analytics/events/EVENT_ID/media/photo_75ke?format=json" \
  -H "Authorization: Bearer YOUR_API_KEY"

Access uploaded photos and documents

Every analytics event (and every row of export?format=json) carries a flat attachments array plus a hasMedia boolean, so you never have to walk nested payload objects to find uploaded files.

{
  "id": 69,
  "event_type": "submit",
  "phone": "919876543210",
  "hasMedia": true,
  "attachments": [
    {
      "field": "photo_75ke",
      "type": "image",
      "fileName": "photo.jpg",
      "mimeType": "image/jpeg",
      "metaMediaId": "123456789012345",
      "url": "https://chatyug.com/received_media/flow/.../image_....jpg",
      "downloadUrl": "https://chatyug.com/api/external/commerce/flows/analytics/events/69/media/photo_75ke",
      "metadataUrl": "https://chatyug.com/api/external/commerce/flows/analytics/events/69/media/photo_75ke?format=json"
    }
  ]
}
FieldUse it for
downloadUrlRecommended. Authenticated download of the real bytes (send your API key). Self-heals from Meta if the local file is missing, so it keeps working when url would 404.
metadataUrlSame endpoint with ?format=json — returns { url, mimeType, fileName, metaMediaId } without transferring the file.
urlPublic /received_media/… link with no auth. Good for embedding in a browser or spreadsheet; can 404 if the file was pruned.
fieldFlow Design field name that produced the upload. Use it as :fieldName on the media endpoint.
typeimage, document, or video (derived from MIME).
metaMediaIdMeta's original media ID, for reconciliation and support tickets.

List submissions, then download every uploaded file:

# 1. List submissions
curl "https://chatyug.com/api/external/commerce/flows/analytics?flowId=12&eventType=submit" \
  -H "Authorization: Bearer YOUR_API_KEY"

# 2. Download each attachment using its downloadUrl
curl -L -o photo.jpg \
  "https://chatyug.com/api/external/commerce/flows/analytics/events/69/media/photo_75ke" \
  -H "Authorization: Bearer YOUR_API_KEY"
// Node.js — save every uploaded file from every submission
const res = await fetch(
  'https://chatyug.com/api/external/commerce/flows/analytics?flowId=12&eventType=submit',
  { headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { events } = await res.json();

for (const ev of events) {
  for (const att of ev.attachments || []) {
    const file = await fetch(att.downloadUrl, {
      headers: { Authorization: `Bearer ${API_KEY}` }
    });
    const buf = Buffer.from(await file.arrayBuffer());
    require('fs').writeFileSync(`${ev.id}-${att.fileName}`, buf);
  }
}
Behaviour notes
  • The list endpoint re-downloads any media whose local file is missing before responding. Pass healMedia=0 to skip that step when you only need text answers.
  • Meta media IDs expire. Once no local copy remains and the Meta ID is dead, the media endpoint returns 404 along with metaMediaId.
  • MCP equivalent: list_flow_analytics then get_flow_media with eventId + fieldName.

Portal

Operators open analytics from each flow card in Flow Design (Excel export included). See the Flow Design Guide — Analytics.