A valid request URL is required to generate request examples{
"success": true,
"data": {
"messageId": "8c1f4e2a-3b5d-4c6e-9f70-812a3b4c5d6e",
"conversationId": "conv_9d2f01",
"seq": 4,
"from": "+14155550100",
"to": "+14155551234",
"text": "Kiwi Cleaning: Hi Dana - your cleaning is confirmed for Tue 9am. Msg & data rates may apply. Reply HELP for help, STOP to opt out.",
"segments": 1,
"billing": {
"unit": "sms",
"quantity": 1
},
"delivered": true,
"clientMessageId": "6f1c2d3e-4a5b-6789-abcd-ef0123456789",
"kind": "initiated"
}
}Send an outbound text message from an SMS-enabled number
Send ONE text message to a person from a number your workspace owns. This is the plain, programmatic send: no AI agent composes it, no teammate has to be signed in, and no external-agent handoff is opened. The text lands on that person’s ordinary conversation for the sending number — if they reply, the reply is handled exactly like any inbound text to that number (by its assigned agent), with this message in the conversation history. Every send goes through the same compliance chokepoint as every other text the platform sends, so none of the rules below are optional or configurable per request. COMPLIANCE (all enforced server-side):
frommust be a number this workspace owns that is SMS-enabled (smsStatus: activeon getSMSRegistration) and assigned to an agent. A number you do not own is 422from_not_owned; one not yet enabled (or still pending) for SMS is 422from_not_sms_active; one with no assigned agent is 422from_has_no_agent— the assigned agent is what keeps the customer’s reply on the same conversation.tomust be a US or Canada (+1) number. The platform’s numbers and its A2P 10DLC registration are North American, so any other destination is refused 422unsupported_country— nothing is sent.tomust also be a customer: one of this workspace’s own numbers (active or not) is refused 422invalid_to, so two of your agents can never text each other in a loop.- The recipient must have a recorded consent basis:
conversational(they texted one of your numbers first) orexpress(consent your business captured — record it with PUT /sms/contacts//consent before sending). Otherwise 422consent_required. There is no per-request override; recording the consent you hold is the sanctioned path. - A recipient who replied STOP is refused 409
sms_suppresseduntil they text START (or you re-enable them with fresh consent via DELETE /sms/suppressions/). - Quiet hours depend on who spoke last. A message to a person who texted one of your numbers within the last 24 hours is a REPLY (
kind: replyon the receipt) — the same classification a teammate’s reply gets — so it is not held to quiet hours unless your workspace uses the strict posture (quietHoursRepliesExempt: falseon the SMS registration). Any other message is business-initiated (kind: initiated) and is held to the recipient’s legal texting window (8am–9pm in their local time). A send that is held is answered 409quiet_hourswithdetails.nextOpen— retry after that instant. Being a reply changes only quiet hours: consent, opt-outs, the send cap and the brand/disclosure block apply exactly as they do to any other send. - Send the message ONLY. On the first text to a person (and again after 30 days without one) the platform prefixes your registered brand name and appends the required “Msg & data rates may apply. Reply HELP for help, STOP to opt out.” block. Every body is converted to plain ASCII: smart punctuation is mapped to its plain equivalent; accented letters, emoji and other non-ASCII characters are removed. A message that is empty after that conversion is refused (400). The body is then capped at two segments, truncating at a sentence boundary. The receipt’s
textis the body exactly as sent,segmentsis its SMS segment count, andbillingis what the message was billed as (see PICTURES AND SHORT VIDEOS below for a message with attachments). - Volume is capped per workspace: a rolling 24-hour ceiling on API sends (500 by default). Past it, 429
daily_send_capcarriesdetails.cap,details.windowHours,details.nextOpenandRetry-After. An idempotent replay is answered from the original receipt and never counts against the cap. - A send that did NOT happen is always an explicit error, never a 201 with
delivered: false. - A send refused by a send-time compliance check — an opt-out (or an opt-out status that could not be confirmed), quiet hours, an inactive registration or sending number — is also recorded as an
sms_blockedevent in the workspace’s compliance trail, so the refusal is on record even though nothing was sent. PICTURES AND SHORT VIDEOS (MMS). A message can carry up to 10 pictures or short videos, named bymediaIds,mediaUrls, or both (10 in total).textis optional when the message carries at least one attachment, and required otherwise. mediaIds— ids from uploadSMSMedia (POST /sms/media). Upload is the way to send a picture or video that is not on a public web address. The id of a picture a customer sent you (themediaentries ofsms.message.received, or a transcript turn’sattachments) works too, so a received picture can be forwarded when its type and size are ones you can send. Only text-message attachments can be sent: the id of a file a visitor uploaded in a web chat ismedia_not_found.mediaUrls— publichttps://addresses, on the standard port (443) or 8443. The platform downloads each one once, checks it and stores its own copy: the carrier only ever receives a short-lived link to that copy, never your address, so your server is not fetched again when the message is delivered. Each address must answer within 10 seconds (20 seconds for all of a message’s addresses together); up to 3 redirects are followed, each re-checked by the same rules. An address that cannot be downloaded is refused 422media_fetch_failedwithdetails.urlanddetails.reason, which is always one of:unreachable(no answer, or an answer that was not the file),not_https,forbidden_destination(a private or internal network address, or a port other than 443 or 8443 — directly or through a redirect),too_many_redirects,timeout,too_largeorunsupported_type. The refusal never repeats your server’s status code or the address your host name resolved to. A file that downloads but is over 1 MiB or of a type you cannot send is refused with its own code —media_too_largeormedia_unsupported_type— carryingdetails.urland the matchingdetails.reason(too_large,unsupported_type); an empty file ismedia_fetch_failedwithunsupported_type.- Each address counts as one upload against the upload rate limit (about 20 per 10 seconds per caller); past it the send is refused 429
rate_limited. The platform downloads only a few messages’ addresses at a time; while it is busy, a send withmediaUrlsis refused 429media_fetch_busywithRetry-After: 2. Neither fetches nor sends anything, and theclient_message_idis not spent — retry the same request shortly. - A repeated id or address is sent once. * Each attachment must be a JPEG, PNG or GIF picture or an MP4, 3GP or MOV video of at most 1 MiB (1,048,576 bytes) — larger files are not reliably delivered to handsets. The type is read from the file’s bytes, never from its name or a declared content type.
- One refused attachment refuses the whole message and nothing is sent: 422
media_not_found(an id that is unknown, belongs to another workspace, is not a text-message attachment, or is an upload that expired),media_unsupported_type,media_too_largeormedia_fetch_failed. - Nothing is downloaded until the send is known to be possible. A replay of a
client_message_idalready used is answered from its receipt without fetching. Then, in this order,fromis checked (owned, SMS-enabled, assigned to an agent —from_not_owned,from_not_sms_active,from_has_no_agent), thento(not one of your own numbers —invalid_to), the recipient’s consent (consent_required) and the daily send cap (daily_send_cap); a refusal there downloads nothing. Only then are themediaUrlsdownloaded. If the send is refused after that (quiet hours, an opt-out, a busy conversation, or any other check above), the copies just stored are deleted at once — a refused send leaves no attachment behind. - A message with attachments is billed as one picture message (MMS), however many attachments it carries: the receipt’s
billingis{unit: mms, quantity: 1}. A message without attachments is billed per SMS segment:{unit: sms, quantity: <segments>}. Every rule above applies unchanged — consent, STOP, quiet hours, the send cap, and the first-contact brand and disclosure block, which is added to a picture-only message too. - The receipt’s
medialists what was sent. Each file stays downloadable with getSMSMedia (GET /sms/media/) and appears on the conversation’s transcript asattachments. IDEMPOTENCY.client_message_idis required and names ONE message. The key is (workspace,client_message_id), so a replay of the same request returns the original 201 receipt byte for byte and never sends a second text — including against a concurrent duplicate of itself. Reusing an id for a DIFFERENT message — different text or different attachments — is 409client_message_id_reused. Use a fresh UUID per message. WHEN THE OUTCOME IS UNKNOWN. If the carrier request fails in a way that does not prove the message stayed home (a timeout, a transport failure, a carrier 5xx), the answer is 409send_state_unknowncarryingdetails.clientMessageIdanddetails.conversationId. It is the ONLY refusal here that does not promise the recipient’s phone stayed silent, so do not retry with a new id: replay the SAMEclient_message_id— a replay resolves to the same answer, or to the receipt once the state settles, and never sends a second text. If the carrier ANSWERED and rejected the request, the answer is 502upstream_errorinstead: nothing was sent and the id has been released, so a corrected retry is safe. TRANSIENT REFUSALS. 503opt_out_check_unavailablemeans the recipient’s opt-out status could not be confirmed, and 503try_againmeans the platform is busy. Neither is an opt-out or a verdict on the message: nothing was sent and theclient_message_idis not spent, so retry the same request shortly. OWNERSHIP. If someone else is already handling that person’s conversation — an inbound message is being answered right now, a teammate has taken the thread over, or an external-agent handoff owns it — the send is refused 409conversation_busywithdetails.reason(inbound_turn_in_flight,seat_owns_threadorlive_escalation) rather than talking over them. A teammate’s takeover holds across the workspace: while a teammate owns an open thread with the person on ANY of your numbers, a send to them from any of your numbers is refusedseat_owns_thread(withoutdetails.conversationIdwhen the teammate’s thread is not the conversation this send would have joined). HANDLING A CONVERSATION YOURSELF. To answer a person’s texts from your own system instead of the AI, pause the AI for them first (pauseSMSContactAI) and subscribe a webhook tosms.message.received. A pause never makes a send busy — your sends are the point of it. Reply from the number the person texted (the event’sto): it is always one an agent answers, because the pause applies only there.
A valid request URL is required to generate request examples{
"success": true,
"data": {
"messageId": "8c1f4e2a-3b5d-4c6e-9f70-812a3b4c5d6e",
"conversationId": "conv_9d2f01",
"seq": 4,
"from": "+14155550100",
"to": "+14155551234",
"text": "Kiwi Cleaning: Hi Dana - your cleaning is confirmed for Tue 9am. Msg & data rates may apply. Reply HELP for help, STOP to opt out.",
"segments": 1,
"billing": {
"unit": "sms",
"quantity": 1
},
"delivered": true,
"clientMessageId": "6f1c2d3e-4a5b-6789-abcd-ef0123456789",
"kind": "initiated"
}
}Authorizations
Flowyte secret API key (Authorization: Bearer flowyte_sk_live_…). Scope-gated; is scoped to your organization — a key can never reach another tenant. The listed scopes in each operation's apiKey requirement are the scopes that key must hold. The tokenUrl is nominal: keys are minted in the dashboard.
Body
One outbound text, optionally with up to 10 pictures or short videos (MMS). from, to and client_message_id are always required; text is required unless the message carries at least one attachment (mediaIds or mediaUrls). String fields are whitespace-trimmed before validation. Unlike initiateEscalation there is no agent or destination to name: the conversation is keyed by the from number's assigned agent, so the recipient's reply lands on the same thread.
The sending number in E.164. Must be a number this workspace owns that is SMS-enabled (smsStatus: active) and assigned to an agent.
"+14155550100"
The recipient in E.164. Must be a US or Canada (+1) number, different from from and not one of this workspace's own numbers, with a recorded consent basis (conversational or express).
"+14155551234"
Idempotency key naming this one message (at most 200 characters; a UUID is ideal). A replay returns the original receipt and never sends a second text; reusing it for a different message is 409 client_message_id_reused.
200"6f1c2d3e-4a5b-6789-abcd-ef0123456789"
The message. Send the message ONLY — on a first contact the platform prefixes your brand name and appends the "Msg & data rates may apply. Reply HELP for help, STOP to opt out." block. The text is converted to plain ASCII: smart punctuation is mapped to its plain equivalent; accented letters, emoji and other non-ASCII characters are removed. A text that is empty after that conversion is refused (400), even alongside attachments — leave text out to send attachments alone. The result is capped at two segments (truncating at a sentence boundary). Optional when the message carries attachments — a picture-only message is allowed.
4000"Hi Dana — your cleaning is confirmed for Tue 9am."
Attachments by id: uploads from uploadSMSMedia (POST /sms/media) that have not expired, or any picture or video already sent or received by text in this workspace (to forward it); a web-chat upload is not a text-message attachment and is refused media_not_found. Each must be a JPEG, PNG or GIF picture or an MP4, 3GP or MOV video of at most 1 MiB. Counts toward the limit of 10 attachments per message, together with mediaUrls.
10["5b0e7c1a-9d2f-4e83-a6b4-1c2d3e4f5a6b"]
Attachments by public https:// address on port 443 or 8443 (at most 2,048 characters each). The platform downloads each picture or video before sending, checks it like an upload, and sends its own copy — the carrier never sees your address. Counts toward the limit of 10 attachments per message, together with mediaIds, and each address counts as one upload against the upload rate limit.
10[
"https://kiwicleaning.example/visits/8812/after.jpg"
]