curl --request POST \
--url https://api.flow.engineer/v1/sandbox/keys \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>"
}
'const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: '<string>'})
};
fetch('https://api.flow.engineer/v1/sandbox/keys', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/sandbox/keys"
payload = { "name": "<string>" }
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v1/sandbox/keys"
payload := strings.NewReader("{\n \"name\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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))
}{
"key": "fk_test_...",
"api_key": {
"id": "key_01JB8Z1B2D4F6H8K0M2P4R6T8W",
"mode": "test",
"last4": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z"
},
"account": {
"id": "acct_01JB8YZ3N2Q4R6T8V0X2Z4B6D8",
"name": "<string>",
"plan": "free",
"created_at": "2023-11-07T05:31:56Z"
},
"app": {
"id": "app_01JB8Z0A1C3E5G7J9K1M3P5R7T",
"account": "acct_01JB8YZ3N2Q4R6T8V0X2Z4B6D8",
"name": "<string>",
"api_version": "2023-12-25",
"settings": {
"transcription": true,
"auto_read": true
},
"created_at": "2023-11-07T05:31:56Z",
"sandbox_join_code": "brave-otter-40718263"
},
"allowance": {
"tier": "anonymous",
"scope": "app",
"channels": [
"telegram"
],
"contacts": {
"limit": 1,
"used": 1
},
"messages_per_contact": 1,
"messages": {
"limit": 1,
"used": 1,
"remaining": 1
},
"upgrade": "<string>",
"expires_at": "2023-11-07T05:31:56Z"
},
"claim_token": "fct_...",
"claim_url": "https://api.flow.engineer/admin/claim#token=fct_...",
"senders": [
{
"id": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel": "telegram",
"kind": "shared",
"livemode": true,
"status": "pending",
"address": {
"phone": "<string>",
"username": "<string>",
"handle": "<string>",
"link": "<string>"
},
"limits": {
"new_contacts_per_day": 123,
"new_contacts_per_hour": 123,
"whatsapp_tier": "<string>"
},
"created_at": "2023-11-07T05:31:56Z",
"display_name": "<string>",
"throttled_until": "2023-11-07T05:31:56Z",
"quality_rating": "green",
"join_code": "join brave-otter-40718263"
}
]
}{
"error": {
"type": "invalid_request",
"message": "limit must be between 1 and 100.",
"hint": "Pass limit between 1 and 100 (default 20), and page with after or before.",
"doc_url": "https://api.flow.engineer/docs/errors/invalid_request",
"param": "limit"
}
}{
"error": {
"type": "new_contact_limit",
"message": "This sender has started its 15 new conversations for today.",
"hint": "Retry after 3600 seconds; replies into existing conversations still go.",
"doc_url": "https://api.flow.engineer/docs/errors/new_contact_limit",
"retry_after": 3600,
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}Get a test key without an account
Creates a new app with a fk_test_ key, without an account or sign-in. This
is the first call for an AI coding agent that has no key: no API key is
sent, and the answer holds everything needed to start (the key, the
sandbox senders with their links and the app’s join code).
The key and the claim_token are shown once: save both. The app has a
sandbox allowance of 1 contact and 50 messages sent in total on the
Telegram sandbox, and WhatsApp’s when it opens (inbound messages are free;
iMessage is not included), and its keys expire after 7 days. After expiry the keys
stop working and the contact is removed from the sandbox; a person can
still claim the app.
To keep the app, a person signs in through the device flow with the
claim_token (POST /v1/device/authorizations), or opens claim_url in a
browser. The app then shares the person’s signed-in allowance (3 contacts
and 100 messages each, one allowance per person over all their apps). A
claim through claim_url revokes this key unless the person chooses to
keep their agent’s key working; a device sign-in replaces it with a new
key.
Calls are limited per client address, per network (/24, /64) and wider
network (/16, /48), and service-wide per day; going over answers 429 rate_limited with retry_after. Do not call this when you already
have a key: check FLOW_MESSAGING_KEY first, and reuse the key you saved.
curl --request POST \
--url https://api.flow.engineer/v1/sandbox/keys \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>"
}
'const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: '<string>'})
};
fetch('https://api.flow.engineer/v1/sandbox/keys', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v1/sandbox/keys"
payload = { "name": "<string>" }
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v1/sandbox/keys"
payload := strings.NewReader("{\n \"name\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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))
}{
"key": "fk_test_...",
"api_key": {
"id": "key_01JB8Z1B2D4F6H8K0M2P4R6T8W",
"mode": "test",
"last4": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z"
},
"account": {
"id": "acct_01JB8YZ3N2Q4R6T8V0X2Z4B6D8",
"name": "<string>",
"plan": "free",
"created_at": "2023-11-07T05:31:56Z"
},
"app": {
"id": "app_01JB8Z0A1C3E5G7J9K1M3P5R7T",
"account": "acct_01JB8YZ3N2Q4R6T8V0X2Z4B6D8",
"name": "<string>",
"api_version": "2023-12-25",
"settings": {
"transcription": true,
"auto_read": true
},
"created_at": "2023-11-07T05:31:56Z",
"sandbox_join_code": "brave-otter-40718263"
},
"allowance": {
"tier": "anonymous",
"scope": "app",
"channels": [
"telegram"
],
"contacts": {
"limit": 1,
"used": 1
},
"messages_per_contact": 1,
"messages": {
"limit": 1,
"used": 1,
"remaining": 1
},
"upgrade": "<string>",
"expires_at": "2023-11-07T05:31:56Z"
},
"claim_token": "fct_...",
"claim_url": "https://api.flow.engineer/admin/claim#token=fct_...",
"senders": [
{
"id": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel": "telegram",
"kind": "shared",
"livemode": true,
"status": "pending",
"address": {
"phone": "<string>",
"username": "<string>",
"handle": "<string>",
"link": "<string>"
},
"limits": {
"new_contacts_per_day": 123,
"new_contacts_per_hour": 123,
"whatsapp_tier": "<string>"
},
"created_at": "2023-11-07T05:31:56Z",
"display_name": "<string>",
"throttled_until": "2023-11-07T05:31:56Z",
"quality_rating": "green",
"join_code": "join brave-otter-40718263"
}
]
}{
"error": {
"type": "invalid_request",
"message": "limit must be between 1 and 100.",
"hint": "Pass limit between 1 and 100 (default 20), and page with after or before.",
"doc_url": "https://api.flow.engineer/docs/errors/invalid_request",
"param": "limit"
}
}{
"error": {
"type": "new_contact_limit",
"message": "This sender has started its 15 new conversations for today.",
"hint": "Retry after 3600 seconds; replies into existing conversations still go.",
"doc_url": "https://api.flow.engineer/docs/errors/new_contact_limit",
"retry_after": 3600,
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v1/messages with content.type=template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}Body
Optional details for a new sandbox app.
The app's name, shown in the dashboard once it is claimed. Defaults to "Sandbox app".
80Response
The new app and its key. The key and the claim token are not shown again.
A new app made without an account, with its test key. key and claim_token are shown only here.
The API key itself (fk_test_...). Shown once; store it as FLOW_MESSAGING_KEY.
"fk_test_..."
An API key's record. The key itself is shown once, at creation, and stored only as a hash.
Show child attributes
Show child attributes
A customer company. Holds the plan, billing and members. Every app belongs to one account.
Show child attributes
Show child attributes
One agent integration. Owns API keys (per mode), webhook endpoints and settings.
Show child attributes
Show child attributes
What the app may still send on the shared sandbox senders for free. Present
only on apps that have one: apps made with POST /v1/sandbox/keys
(anonymous, one allowance per app), and apps of people who signed in
(signed_in, one allowance per person: every app the person owns or claimed
draws on the same contacts and messages, so the counts here are the
person's, over all those apps). Only messages your agent sends count, on the
channels in channels; inbound messages are free. A contact counts once it
joins an app on a sandbox sender, and keeps counting after it leaves. Sends
past the allowance answer 403 permission with channel_code
sandbox_allowance_used; a join past contacts.limit is refused in the
chat.
Show child attributes
Show child attributes
Proves you hold this app when a person signs in to claim it: pass it to
POST /v1/device/authorizations. Shown once; keep it with the key. It
stops working once the app is claimed.
"fct_..."
A page where a person signs in with GitHub and claims the app in the browser, without the CLI. It holds the claim token, so treat it like one. The claim hands out no key and revokes the app's keys unless the person ticks "Keep my agent's current key working"; afterwards they make keys on the dashboard's Keys page.
"https://api.flow.engineer/admin/claim#token=fct_..."
The shared sandbox senders the key can use, each with the link a person opens to join the app (address.link) and the join message (join_code).
Show child attributes
Show child attributes