Create or clone Flow
Creates a Flow in DRAFT, or clones an existing one. Answers 201 with {id, metaFlowId} — the two fields only, not the whole Flow. Accepts exactly name, categories and cloneFromFlowId: any other key is REFUSED with 400 FLOW_UNKNOWN_FIELDS, never ignored, so {"status":"PUBLISHED"} cannot answer 201 and let you conclude you published. The number and its WABA come from the key — sending whatsappNumberId, wabaId or metaWabaId is 400 FLOW_NUMBER_FROM_KEY. Permission flows:manage (ADMIN and above).
Requires a number-scoped key. A tenant-scoped key must name the number with the x-whatsapp-number-id header, or it gets 403 TENANT_SCOPE_NOT_ALLOWED.
curl --request POST \
--url https://pilotstatus.com.br/v1/flows \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"name": "Agendamento v2",
"categories": [
"APPOINTMENT_BOOKING"
],
"cloneFromFlowId": "cmf1a2b3c4d5e6f7g8h9i0j1"
}
'import requests
url = "https://pilotstatus.com.br/v1/flows"
payload = {
"name": "Agendamento v2",
"categories": ["APPOINTMENT_BOOKING"],
"cloneFromFlowId": "cmf1a2b3c4d5e6f7g8h9i0j1"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Agendamento v2',
categories: ['APPOINTMENT_BOOKING'],
cloneFromFlowId: 'cmf1a2b3c4d5e6f7g8h9i0j1'
})
};
fetch('https://pilotstatus.com.br/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://pilotstatus.com.br/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' => 'Agendamento v2',
'categories' => [
'APPOINTMENT_BOOKING'
],
'cloneFromFlowId' => 'cmf1a2b3c4d5e6f7g8h9i0j1'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$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://pilotstatus.com.br/v1/flows"
payload := strings.NewReader("{\n \"name\": \"Agendamento v2\",\n \"categories\": [\n \"APPOINTMENT_BOOKING\"\n ],\n \"cloneFromFlowId\": \"cmf1a2b3c4d5e6f7g8h9i0j1\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
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://pilotstatus.com.br/v1/flows")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Agendamento v2\",\n \"categories\": [\n \"APPOINTMENT_BOOKING\"\n ],\n \"cloneFromFlowId\": \"cmf1a2b3c4d5e6f7g8h9i0j1\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://pilotstatus.com.br/v1/flows")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Agendamento v2\",\n \"categories\": [\n \"APPOINTMENT_BOOKING\"\n ],\n \"cloneFromFlowId\": \"cmf1a2b3c4d5e6f7g8h9i0j1\"\n}"
response = http.request(request)
puts response.read_body{
"id": "cmf1a2b3c4d5e6f7g8h9i0j1",
"metaFlowId": "1122334455"
}{
"error": "categories é obrigatório: uma lista não vazia de SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER.",
"errorEN": "categories is required: a non-empty array of SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER.",
"code": "FLOW_CATEGORY_INVALID"
}{
"error": "Unauthorized"
}{
"error": "Tenant-scoped keys cannot call number endpoints",
"code": "TENANT_SCOPE_NOT_ALLOWED"
}{
"error": "Flow não encontrado para o número desta chave.",
"errorEN": "Flow not found for this key's number.",
"code": "FLOW_NOT_FOUND"
}{
"error": "Este Flow já foi publicado na Meta e não pode mais ser alterado. Clone-o para criar uma nova versão.",
"errorEN": "This Flow is already published on Meta and can no longer be changed. Clone it to create a new version.",
"code": "FLOW_NOT_DRAFT"
}{
"error": "Flows existem apenas em números Meta (API Oficial). O número desta chave não é Meta ou não tem uma WABA associada — use uma chave de um número Meta.",
"errorEN": "Flows only exist on Meta (Cloud API) numbers. This key's number is not a Meta number, or has no WABA behind it — use a key bound to a Meta number.",
"code": "FLOW_REQUIRES_META_NUMBER"
}{
"error": "Too many requests"
}{
"error": "Erro interno do servidor.",
"errorEN": "Internal server error.",
"code": "INTERNAL_ERROR"
}Authorizations
Your ps_ API key
Body
Flow name. Non-empty; trimmed.
1"Agendamento v2"
One or more of Meta's closed set. A value outside it is 400 FLOW_CATEGORY_INVALID.
1SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER ["APPOINTMENT_BOOKING"]
The LOCAL id of the source Flow — the id field GET /v1/flows returns, never the metaFlowId. Confusing the two is the natural mistake and we cannot detect it for you: both are opaque strings, and Meta answers with its own error. metaFlowId is also nullable (a draft never pushed), so an id from Meta would have nothing to resolve. Omit to create from scratch; null is refused.
"cmf1a2b3c4d5e6f7g8h9i0j1"
Response
Flow created in DRAFT
Was this page helpful?
curl --request POST \
--url https://pilotstatus.com.br/v1/flows \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"name": "Agendamento v2",
"categories": [
"APPOINTMENT_BOOKING"
],
"cloneFromFlowId": "cmf1a2b3c4d5e6f7g8h9i0j1"
}
'import requests
url = "https://pilotstatus.com.br/v1/flows"
payload = {
"name": "Agendamento v2",
"categories": ["APPOINTMENT_BOOKING"],
"cloneFromFlowId": "cmf1a2b3c4d5e6f7g8h9i0j1"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Agendamento v2',
categories: ['APPOINTMENT_BOOKING'],
cloneFromFlowId: 'cmf1a2b3c4d5e6f7g8h9i0j1'
})
};
fetch('https://pilotstatus.com.br/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://pilotstatus.com.br/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' => 'Agendamento v2',
'categories' => [
'APPOINTMENT_BOOKING'
],
'cloneFromFlowId' => 'cmf1a2b3c4d5e6f7g8h9i0j1'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$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://pilotstatus.com.br/v1/flows"
payload := strings.NewReader("{\n \"name\": \"Agendamento v2\",\n \"categories\": [\n \"APPOINTMENT_BOOKING\"\n ],\n \"cloneFromFlowId\": \"cmf1a2b3c4d5e6f7g8h9i0j1\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
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://pilotstatus.com.br/v1/flows")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Agendamento v2\",\n \"categories\": [\n \"APPOINTMENT_BOOKING\"\n ],\n \"cloneFromFlowId\": \"cmf1a2b3c4d5e6f7g8h9i0j1\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://pilotstatus.com.br/v1/flows")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Agendamento v2\",\n \"categories\": [\n \"APPOINTMENT_BOOKING\"\n ],\n \"cloneFromFlowId\": \"cmf1a2b3c4d5e6f7g8h9i0j1\"\n}"
response = http.request(request)
puts response.read_body{
"id": "cmf1a2b3c4d5e6f7g8h9i0j1",
"metaFlowId": "1122334455"
}{
"error": "categories é obrigatório: uma lista não vazia de SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER.",
"errorEN": "categories is required: a non-empty array of SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER.",
"code": "FLOW_CATEGORY_INVALID"
}{
"error": "Unauthorized"
}{
"error": "Tenant-scoped keys cannot call number endpoints",
"code": "TENANT_SCOPE_NOT_ALLOWED"
}{
"error": "Flow não encontrado para o número desta chave.",
"errorEN": "Flow not found for this key's number.",
"code": "FLOW_NOT_FOUND"
}{
"error": "Este Flow já foi publicado na Meta e não pode mais ser alterado. Clone-o para criar uma nova versão.",
"errorEN": "This Flow is already published on Meta and can no longer be changed. Clone it to create a new version.",
"code": "FLOW_NOT_DRAFT"
}{
"error": "Flows existem apenas em números Meta (API Oficial). O número desta chave não é Meta ou não tem uma WABA associada — use uma chave de um número Meta.",
"errorEN": "Flows only exist on Meta (Cloud API) numbers. This key's number is not a Meta number, or has no WABA behind it — use a key bound to a Meta number.",
"code": "FLOW_REQUIRES_META_NUMBER"
}{
"error": "Too many requests"
}{
"error": "Erro interno do servidor.",
"errorEN": "Internal server error.",
"code": "INTERNAL_ERROR"
}