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
phoneNumberIdfor send
Base URL
https://chatyug.com/api/external
Manage flows
| Method | Path | MCP tool |
|---|---|---|
| GET | /commerce/flows | list_commerce_flows |
| GET | /commerce/flows/:flowId | get_commerce_flow |
| POST | /commerce/flows | create_commerce_flow |
| POST | /commerce/flows/media | — |
| POST | /commerce/flows/:flowId/copy | copy_commerce_flow |
| PUT | /commerce/flows/:flowId | update_commerce_flow (confirm) |
| POST | /commerce/flows/:flowId/publish | publish_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
| Field | Notes |
|---|---|
version | Use 7.3 (ChatYug bumps older versions on save). |
routing_model | Map 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_canvas | Optional portal layout sidecar; stripped before Meta upload. |
data_api_version | Set 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
}
EmbeddedLink
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": {}
}
}
]
}
Footer
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 inpayload(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, declaredscreen.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, buildsrouting_modelwhen missing, and auto-declaresscreen.dataon 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
- (Optional)
POST /commerce/flows/media→ put returnedurlinto Image / ImageCarousel POST /commerce/flowswithflowJson(or…/copy)POST /commerce/flows/:id/publishPOST /messages/flowwith MetaflowId+ optionalinitialScreenData- Parse inbound
nfm_reply.response_json(below) and/or pull analytics + media
Fetch design + analytics + media (external)
- Design:
GET /commerce/flows/:flowId→flow.flow_json(screens/components). - Submissions:
GET /commerce/flows/analytics?flowId=:id&eventType=submit→ each event haspayload,attachments[],hasMedia. - Files: for each attachment,
GETattachments[].downloadUrlwith your API key (or useexport?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.
- Parse
data.message.interactive.nfm_reply.response_json(it is a JSON string). - Keys in that object match the designer input Name (e.g.
customer_name→${form.customer_name}in the canvas). - Correlate with the token you passed as
flowTokenon send — it is usually present asflow_tokeninsideresponse_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
srcis 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
| Method | Path | MCP tool |
|---|---|---|
| GET | /commerce/flows/analytics | list_flow_analytics |
| GET | /commerce/flows/analytics/summary | get_flow_analytics_summary |
| GET | /commerce/flows/analytics/export | — |
| GET | /commerce/flows/analytics/events/:eventId/media/:fieldName | get_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"
}
]
}
| Field | Use it for |
|---|---|
downloadUrl | Recommended. 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. |
metadataUrl | Same endpoint with ?format=json — returns { url, mimeType, fileName, metaMediaId } without transferring the file. |
url | Public /received_media/… link with no auth. Good for embedding in a browser or spreadsheet; can 404 if the file was pruned. |
field | Flow Design field name that produced the upload. Use it as :fieldName on the media endpoint. |
type | image, document, or video (derived from MIME). |
metaMediaId | Meta'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);
}
}
- The list endpoint re-downloads any media whose local file is missing before responding. Pass
healMedia=0to 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
404along withmetaMediaId. - MCP equivalent:
list_flow_analyticsthenget_flow_mediawitheventId+fieldName.
Portal
Operators open analytics from each flow card in Flow Design (Excel export included). See the Flow Design Guide — Analytics.