curl --request POST \
--url https://api.keebai.com/v1/flows \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "reserva-mesa",
"flow_data": {
"version": "7.0",
"screens": [
{
"id": "WELCOME",
"title": "Reserva",
"terminal": true,
"layout": {
"type": "SingleColumnLayout",
"children": []
}
}
]
},
"channel_id": "665f1a2b3c4d5e6f70819234",
"phone_number_id": "100000000000001",
"description": "<string>",
"categories": [
"OTHER"
],
"metadata": {}
}
'import requests
url = "https://api.keebai.com/v1/flows"
payload = {
"name": "reserva-mesa",
"flow_data": {
"version": "7.0",
"screens": [
{
"id": "WELCOME",
"title": "Reserva",
"terminal": True,
"layout": {
"type": "SingleColumnLayout",
"children": []
}
}
]
},
"channel_id": "665f1a2b3c4d5e6f70819234",
"phone_number_id": "100000000000001",
"description": "<string>",
"categories": ["OTHER"],
"metadata": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'reserva-mesa',
flow_data: {
version: '7.0',
screens: [
{
id: 'WELCOME',
title: 'Reserva',
terminal: true,
layout: {type: 'SingleColumnLayout', children: []}
}
]
},
channel_id: '665f1a2b3c4d5e6f70819234',
phone_number_id: '100000000000001',
description: '<string>',
categories: ['OTHER'],
metadata: {}
})
};
fetch('https://api.keebai.com/v1/flows', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.keebai.com/v1/flows",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'reserva-mesa',
'flow_data' => [
'version' => '7.0',
'screens' => [
[
'id' => 'WELCOME',
'title' => 'Reserva',
'terminal' => true,
'layout' => [
'type' => 'SingleColumnLayout',
'children' => [
]
]
]
]
],
'channel_id' => '665f1a2b3c4d5e6f70819234',
'phone_number_id' => '100000000000001',
'description' => '<string>',
'categories' => [
'OTHER'
],
'metadata' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.keebai.com/v1/flows"
payload := strings.NewReader("{\n \"name\": \"reserva-mesa\",\n \"flow_data\": {\n \"version\": \"7.0\",\n \"screens\": [\n {\n \"id\": \"WELCOME\",\n \"title\": \"Reserva\",\n \"terminal\": true,\n \"layout\": {\n \"type\": \"SingleColumnLayout\",\n \"children\": []\n }\n }\n ]\n },\n \"channel_id\": \"665f1a2b3c4d5e6f70819234\",\n \"phone_number_id\": \"100000000000001\",\n \"description\": \"<string>\",\n \"categories\": [\n \"OTHER\"\n ],\n \"metadata\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.keebai.com/v1/flows")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"reserva-mesa\",\n \"flow_data\": {\n \"version\": \"7.0\",\n \"screens\": [\n {\n \"id\": \"WELCOME\",\n \"title\": \"Reserva\",\n \"terminal\": true,\n \"layout\": {\n \"type\": \"SingleColumnLayout\",\n \"children\": []\n }\n }\n ]\n },\n \"channel_id\": \"665f1a2b3c4d5e6f70819234\",\n \"phone_number_id\": \"100000000000001\",\n \"description\": \"<string>\",\n \"categories\": [\n \"OTHER\"\n ],\n \"metadata\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.keebai.com/v1/flows")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"reserva-mesa\",\n \"flow_data\": {\n \"version\": \"7.0\",\n \"screens\": [\n {\n \"id\": \"WELCOME\",\n \"title\": \"Reserva\",\n \"terminal\": true,\n \"layout\": {\n \"type\": \"SingleColumnLayout\",\n \"children\": []\n }\n }\n ]\n },\n \"channel_id\": \"665f1a2b3c4d5e6f70819234\",\n \"phone_number_id\": \"100000000000001\",\n \"description\": \"<string>\",\n \"categories\": [\n \"OTHER\"\n ],\n \"metadata\": {}\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"channel_id": "<string>",
"name": "<string>",
"categories": [],
"description": "<string>",
"meta_flow_id": "<string>",
"flow_data": {},
"metadata": {},
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
}POST /v1/flows
Create a WhatsApp Flow: registers it at Meta and uploads its Flow JSON. The Flow starts as a draft.
curl --request POST \
--url https://api.keebai.com/v1/flows \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "reserva-mesa",
"flow_data": {
"version": "7.0",
"screens": [
{
"id": "WELCOME",
"title": "Reserva",
"terminal": true,
"layout": {
"type": "SingleColumnLayout",
"children": []
}
}
]
},
"channel_id": "665f1a2b3c4d5e6f70819234",
"phone_number_id": "100000000000001",
"description": "<string>",
"categories": [
"OTHER"
],
"metadata": {}
}
'import requests
url = "https://api.keebai.com/v1/flows"
payload = {
"name": "reserva-mesa",
"flow_data": {
"version": "7.0",
"screens": [
{
"id": "WELCOME",
"title": "Reserva",
"terminal": True,
"layout": {
"type": "SingleColumnLayout",
"children": []
}
}
]
},
"channel_id": "665f1a2b3c4d5e6f70819234",
"phone_number_id": "100000000000001",
"description": "<string>",
"categories": ["OTHER"],
"metadata": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'reserva-mesa',
flow_data: {
version: '7.0',
screens: [
{
id: 'WELCOME',
title: 'Reserva',
terminal: true,
layout: {type: 'SingleColumnLayout', children: []}
}
]
},
channel_id: '665f1a2b3c4d5e6f70819234',
phone_number_id: '100000000000001',
description: '<string>',
categories: ['OTHER'],
metadata: {}
})
};
fetch('https://api.keebai.com/v1/flows', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.keebai.com/v1/flows",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'reserva-mesa',
'flow_data' => [
'version' => '7.0',
'screens' => [
[
'id' => 'WELCOME',
'title' => 'Reserva',
'terminal' => true,
'layout' => [
'type' => 'SingleColumnLayout',
'children' => [
]
]
]
]
],
'channel_id' => '665f1a2b3c4d5e6f70819234',
'phone_number_id' => '100000000000001',
'description' => '<string>',
'categories' => [
'OTHER'
],
'metadata' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.keebai.com/v1/flows"
payload := strings.NewReader("{\n \"name\": \"reserva-mesa\",\n \"flow_data\": {\n \"version\": \"7.0\",\n \"screens\": [\n {\n \"id\": \"WELCOME\",\n \"title\": \"Reserva\",\n \"terminal\": true,\n \"layout\": {\n \"type\": \"SingleColumnLayout\",\n \"children\": []\n }\n }\n ]\n },\n \"channel_id\": \"665f1a2b3c4d5e6f70819234\",\n \"phone_number_id\": \"100000000000001\",\n \"description\": \"<string>\",\n \"categories\": [\n \"OTHER\"\n ],\n \"metadata\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.keebai.com/v1/flows")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"reserva-mesa\",\n \"flow_data\": {\n \"version\": \"7.0\",\n \"screens\": [\n {\n \"id\": \"WELCOME\",\n \"title\": \"Reserva\",\n \"terminal\": true,\n \"layout\": {\n \"type\": \"SingleColumnLayout\",\n \"children\": []\n }\n }\n ]\n },\n \"channel_id\": \"665f1a2b3c4d5e6f70819234\",\n \"phone_number_id\": \"100000000000001\",\n \"description\": \"<string>\",\n \"categories\": [\n \"OTHER\"\n ],\n \"metadata\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.keebai.com/v1/flows")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"reserva-mesa\",\n \"flow_data\": {\n \"version\": \"7.0\",\n \"screens\": [\n {\n \"id\": \"WELCOME\",\n \"title\": \"Reserva\",\n \"terminal\": true,\n \"layout\": {\n \"type\": \"SingleColumnLayout\",\n \"children\": []\n }\n }\n ]\n },\n \"channel_id\": \"665f1a2b3c4d5e6f70819234\",\n \"phone_number_id\": \"100000000000001\",\n \"description\": \"<string>\",\n \"categories\": [\n \"OTHER\"\n ],\n \"metadata\": {}\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"channel_id": "<string>",
"name": "<string>",
"categories": [],
"description": "<string>",
"meta_flow_id": "<string>",
"flow_data": {},
"metadata": {},
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
}draft — publish it before sending it to real recipients.
Endpoint
POST https://api.keebai.com/v1/flows
Required scope
flows:write
Body
| Field | Type | Required | Description |
|---|---|---|---|
channel_id | string | No | Keebai channel id of the WhatsApp channel that owns the Flow. Only needed when the project has more than one active WhatsApp channel. |
phone_number_id | string | No | Alias for channel_id: the numeric Meta phone_number_id. |
name | string | Yes | Flow name, unique per channel. Up to 100 characters. |
description | string | No | Up to 500 characters. |
categories | string[] | No | Any of OTHER, SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY. Defaults to ["OTHER"]. |
flow_data | object | Yes | Flow JSON as defined by Meta: version, screens, routing_model. |
metadata | object | No | Free-form metadata stored alongside the Flow in Keebai. |
Example request
curl -X POST https://api.keebai.com/v1/flows \
-H "Authorization: Bearer kbai_pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "reserva-mesa",
"description": "Reserva de mesa en dos pasos",
"categories": ["APPOINTMENT_BOOKING"],
"flow_data": {
"version": "7.0",
"screens": [
{
"id": "WELCOME",
"title": "Reservar mesa",
"terminal": true,
"data": {},
"layout": {
"type": "SingleColumnLayout",
"children": [
{
"type": "Form",
"name": "reserva",
"children": [
{ "type": "TextInput", "name": "nombre", "label": "Nombre", "required": true },
{ "type": "DatePicker", "name": "fecha", "label": "Fecha", "required": true },
{
"type": "Footer",
"label": "Reservar",
"on-click-action": {
"name": "complete",
"payload": { "nombre": "${form.nombre}", "fecha": "${form.fecha}" }
}
}
]
}
]
}
}
]
}
}'
Response
201 Created
{
"id": "66a1f2b3c4d5e6f708192a3b",
"channel_id": "665f1a2b3c4d5e6f70819234",
"name": "reserva-mesa",
"description": "Reserva de mesa en dos pasos",
"meta_flow_id": "1234567890123456",
"categories": ["APPOINTMENT_BOOKING"],
"status": "draft",
"flow_data": { "version": "7.0", "screens": [] },
"metadata": {},
"created_at": "2026-07-27T14:31:07.221Z",
"updated_at": "2026-07-27T14:31:07.221Z"
}
meta_flow_id is what you pass as flow_id when sending a message of type flow through POST /v1/messages.
400 Bad Request
The channel is not WhatsApp, has no Meta credentials, or the Flow JSON was rejected by Meta.409 Conflict
A Flow with that name already exists on the channel.Authorizations
Personal Access Token con prefijo kbai_pk_. Generar desde el portal con permiso developer.manage_tokens.
Body
100"reserva-mesa"
Flow JSON as defined by Meta (version, screens, routing_model). Uploaded as the Flow asset on create.
{
"version": "7.0",
"screens": [
{
"id": "WELCOME",
"title": "Reserva",
"terminal": true,
"layout": {
"type": "SingleColumnLayout",
"children": []
}
}
]
}
Keebai channel id. Optional when the tenant has exactly one active channel of the given type; required to disambiguate otherwise.
"665f1a2b3c4d5e6f70819234"
WhatsApp-only alias for channel_id: the Meta phone_number_id of the sending number. Ignored on other channels.
"100000000000001"
500OTHER, SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY Free-form metadata stored alongside the Flow in Keebai.
Response
Keebai flow id.
Keebai channel this Flow belongs to.
OTHER, SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY draft, published, archived Meta Flow id. Use it as flow_id when sending a message of type flow. Null until Meta accepts the Flow.
Flow JSON currently stored in Keebai.
Was this page helpful?