# Account AI copilot Source: https://docs.flowyte.com/api-reference/agents/account-ai-copilot /openapi.yaml post /assist Sends a natural-language request to an AI copilot that can configure your entire account and review call history by calling your own API operations on your behalf. It streams progress and returns its reply with any changes applied. Actions that could be destructive are paused and listed for you to approve; resend them marked as confirmed to carry them out. # Compare a version with the draft Source: https://docs.flowyte.com/api-reference/agents/compare-a-version-with-the-draft /openapi.yaml get /agents/{id}/versions/{versionId}/diff Returns the differences between a published version and the agent's current draft, so you can review exactly what changed before publishing. # Configure an agent with AI Source: https://docs.flowyte.com/api-reference/agents/configure-an-agent-with-ai /openapi.yaml post /agents/{id}/assist Sends a natural-language request to an AI assistant that edits this agent for you. It streams progress and returns the assistant's reply along with the list of changes it applied to the agent. # Create an agent Source: https://docs.flowyte.com/api-reference/agents/create-an-agent /openapi.yaml post /agents Creates a new agent. The agent starts as a draft; publish it before it can take live calls or chats. # Delete an agent Source: https://docs.flowyte.com/api-reference/agents/delete-an-agent /openapi.yaml delete /agents/{id} Deletes an agent so it no longer appears in your list or handles calls and chats. # Get an agent Source: https://docs.flowyte.com/api-reference/agents/get-an-agent /openapi.yaml get /agents/{id} Returns a single agent by its ID, including its current draft configuration. # Get an AI copilot thread Source: https://docs.flowyte.com/api-reference/agents/get-an-ai-copilot-thread /openapi.yaml get /assist/threads/{threadId} Returns a single AI copilot conversation and all of its messages so you can review or resume it. # Get the pre-publish report Source: https://docs.flowyte.com/api-reference/agents/get-the-pre-publish-report /openapi.yaml get /agents/{id}/prepublish-report Returns a plain-language summary of how the agent will behave once published, including what it can answer, what it will decline, and what information it discloses. Use it to review the agent before going live. # List agent versions Source: https://docs.flowyte.com/api-reference/agents/list-agent-versions /openapi.yaml get /agents/{id}/versions Returns a paginated history of the agent's published versions. # List agents Source: https://docs.flowyte.com/api-reference/agents/list-agents /openapi.yaml get /agents Returns a paginated list of your agents. You can filter by status (draft or published) or search by name. # List AI copilot threads Source: https://docs.flowyte.com/api-reference/agents/list-ai-copilot-threads /openapi.yaml get /assist/threads Returns your saved AI copilot conversations, most recent first. Optionally filter to a single agent with the agentId query parameter. # Publish an agent Source: https://docs.flowyte.com/api-reference/agents/publish-an-agent /openapi.yaml post /agents/{id}/publish Publishes the agent's current draft as a new fixed version that live calls and chats will use. You can include an optional note describing the change. # Roll back to a previous version Source: https://docs.flowyte.com/api-reference/agents/roll-back-to-a-previous-version /openapi.yaml post /agents/{id}/rollback Makes a previously published version the live one again. Pass the versionId of the version you want to restore. # Scaffold a draft agent Source: https://docs.flowyte.com/api-reference/agents/scaffold-a-draft-agent /openapi.yaml post /agents/scaffold Generates a draft agent automatically from a website URL, a written procedure, or a plain-language description. Streams progress while it builds and returns the finished draft agent at the end of the stream. Review and publish the draft before it goes live. # Set the agent's voice Source: https://docs.flowyte.com/api-reference/agents/set-the-agents-voice /openapi.yaml put /agents/{id}/voice Assigns a voice to the agent for a given language, defaulting to the agent's primary language. This is a shortcut for changing the voice without sending a full agent update. Look up a voice ID with the voice catalog or voice search endpoints first. # Update an agent Source: https://docs.flowyte.com/api-reference/agents/update-an-agent /openapi.yaml patch /agents/{id} Updates one or more fields on an agent's draft. To avoid overwriting concurrent edits, send the version you last read in the If-Match header; if it is out of date the request returns 409 with the current version and agent state so you can refetch and retry. Changes apply to the draft only and take effect once you publish the agent. # Create an API key (token shown once) Source: https://docs.flowyte.com/api-reference/apikeys/create-an-api-key-token-shown-once /openapi.yaml post /api-keys Create a secret API key scoped to the actions you choose. The full token (`flowyte_sk_…`) is returned ONLY in this response and is never recoverable — store it immediately. Pick `env:'live'` for real traffic or `env:'test'` for a sandbox key, and grant the narrowest scopes the integration needs. Optionally set `expiresAt` to auto-expire the key. # List API keys Source: https://docs.flowyte.com/api-reference/apikeys/list-api-keys /openapi.yaml get /api-keys Page through the org's API keys. Only safe metadata is returned — name, scopes, environment, prefix, and last-4; the secret itself is never retrievable after creation. Managing keys is a privileged action, so reading them also requires the `keys:write` scope. # Revoke an API key Source: https://docs.flowyte.com/api-reference/apikeys/revoke-an-api-key /openapi.yaml delete /api-keys/{id} Permanently revoke a key. It stops authenticating immediately — including any in-flight requests using it — and cannot be restored. Use this the moment a key may be compromised; if you just want a new secret on the same key, use rotateApiKey instead. # Rotate an API key (new token shown once) Source: https://docs.flowyte.com/api-reference/apikeys/rotate-an-api-key-new-token-shown-once /openapi.yaml post /api-keys/{id}/rotate Issue a fresh secret for an existing key, keeping its name, scopes, and environment. The OLD token stops working immediately (there is no grace window), so swap the new token into your integration before rotating. Like createApiKey, the new token is returned once and never recoverable. # List activity logs Source: https://docs.flowyte.com/api-reference/auditlogs/list-activity-logs /openapi.yaml get /audit-logs Returns a log of actions taken in your account, showing who performed each request, the endpoint and method, the response status, and when it happened. Filter by date range and by who performed the action (a user, API key, visitor, or the system). Results are paginated. # Add credit to the wallet Source: https://docs.flowyte.com/api-reference/billing/add-credit-to-the-wallet /openapi.yaml post /billing/wallet/credit Adds funds directly to the prepaid wallet without a card payment, and returns the updated balance. Useful for manual adjustments or onboarding. # Change subscription plan Source: https://docs.flowyte.com/api-reference/billing/change-subscription-plan /openapi.yaml post /billing/subscription Switches your organization to the plan you specify. Returns the updated subscription. # Configure wallet auto-reload Source: https://docs.flowyte.com/api-reference/billing/configure-wallet-auto-reload /openapi.yaml put /billing/wallet/auto-reload Sets whether the wallet automatically tops up by a fixed amount when its balance falls below a threshold. Returns the updated wallet. # Get current subscription Source: https://docs.flowyte.com/api-reference/billing/get-current-subscription /openapi.yaml get /billing/subscription Returns your organization's current plan and the date it next renews. # Get wallet balance Source: https://docs.flowyte.com/api-reference/billing/get-wallet-balance /openapi.yaml get /billing/wallet Returns your prepaid wallet's current balance along with its auto-reload settings. # List billing plans Source: https://docs.flowyte.com/api-reference/billing/list-billing-plans /openapi.yaml get /billing/plans Returns the subscription plans available to your organization, including each plan's monthly price, included minutes, and overage rate. # List invoices Source: https://docs.flowyte.com/api-reference/billing/list-invoices /openapi.yaml get /billing/invoices Returns your organization's invoices, newest first, each with a link to download the invoice PDF. # List itemized usage charges Source: https://docs.flowyte.com/api-reference/billing/list-itemized-usage-charges /openapi.yaml get /billing/usage/items Returns individual usage charges (one per call, chat, or transfer) within a date range, newest first. # List usage records Source: https://docs.flowyte.com/api-reference/billing/list-usage-records /openapi.yaml get /billing/usage Returns a paginated list of daily usage records, each with the minutes used and the resulting cost. Filter by date range or by agent. # Top up the wallet by card Source: https://docs.flowyte.com/api-reference/billing/top-up-the-wallet-by-card /openapi.yaml post /billing/wallet/topup Starts a card payment to add funds to the prepaid wallet and returns a payment client secret you use to complete the charge. Pass an idempotency key to safely retry without double-charging. # Create a chat session Source: https://docs.flowyte.com/api-reference/chat/create-a-chat-session /openapi.yaml post /chat/sessions Starts a new chat session with an agent and returns its ID. Send messages to the session to converse with the agent. # End a chat session Source: https://docs.flowyte.com/api-reference/chat/end-a-chat-session /openapi.yaml post /chat/sessions/{id}/end Closes a chat session. No further messages can be sent once it is ended. # Get a chat session Source: https://docs.flowyte.com/api-reference/chat/get-a-chat-session /openapi.yaml get /chat/sessions/{id} Returns a chat session and its current state. # Get widget bootstrap config Source: https://docs.flowyte.com/api-reference/chat/get-widget-bootstrap-config /openapi.yaml get /widget/bootstrap Returns the public configuration for the browser widget (theme, copy, and behavior) for a given publishable key. It contains no secrets and is safe to call from the browser, but the request's origin must be allowed for the key. # List chat messages Source: https://docs.flowyte.com/api-reference/chat/list-chat-messages /openapi.yaml get /chat/sessions/{id}/messages Returns the paginated message history for a chat session. # OpenAI-compatible chat completions Source: https://docs.flowyte.com/api-reference/chat/openai-compatible-chat-completions /openapi.yaml post /chat/completions An OpenAI-compatible chat completions endpoint where the model field is your agent ID. Set stream to true for streamed chunks. Existing OpenAI SDK clients work by pointing the base URL at this API. # Send a chat message Source: https://docs.flowyte.com/api-reference/chat/send-a-chat-message /openapi.yaml post /chat/sessions/{id}/messages Sends a message to a chat session and returns the agent's reply. Set stream to true to receive the reply as a server-sent event stream; otherwise the complete reply is returned in a single response. # Start a widget chat session Source: https://docs.flowyte.com/api-reference/chat/start-a-widget-chat-session /openapi.yaml post /chat/widget/sessions Starts a chat session for the embeddable browser widget, authenticated with a publishable key from an allowed origin. Creates an anonymous visitor and returns the connection details; a bot-check token may be required. # Claim an escalation Source: https://docs.flowyte.com/api-reference/escalation/claim-an-escalation /openapi.yaml post /escalations/{id}/claim Claim a `requested`/`notified` escalation, setting a per-channel activity-based lease. Idempotent for the SAME key (re-claim extends the lease); a claim by a DIFFERENT key returns 409 `claim_conflict`. The claim preconditions are revalidated at claim time: the destination's AI-Harness integration must still be connected and — unless the session is a handshake test — the destination must still be active, else 409 `destination_unavailable` (the pre-claim SLA sweeper still fires the fallback so the visitor is not stranded). The returned `contextPackage.transcriptWindow` is rebuilt at claim so the claimer starts current with any turns forwarded between `requested` and `claimed`. Requires the org to be on a Starter plan or higher. # Create an escalation destination (signing secret shown once) Source: https://docs.flowyte.com/api-reference/escalation/create-an-escalation-destination-signing-secret-shown-once /openapi.yaml post /escalation-destinations Register an external operator-agent endpoint. The server generates a `whsec_` signing secret returned EXACTLY ONCE in this response and never again — store it immediately. The endpoint must be https and pass an SSRF/private-IP check; `provider` must be an AI Harness whose integration is connected; any `sms` channel requires the org's 10DLC/TCR campaign to be ACTIVE. The new destination starts `unverified` — run testEscalationDestination to flip it `active` before it can route live. `capabilities` is a free-form list of capability tokens the external agent advertises (e.g. "reply", "resolve"); `fallbackRef` uses the grammar `external:` | `email:
` | `none`. # Delete an escalation destination (soft) Source: https://docs.flowyte.com/api-reference/escalation/delete-an-escalation-destination-soft /openapi.yaml delete /escalation-destinations/{id} Soft-delete a destination. Refuses with 409 if an agent escalation policy still routes to it, unless `force=true`. # Extend the claim lease Source: https://docs.flowyte.com/api-reference/escalation/extend-the-claim-lease /openapi.yaml post /escalations/{id}/heartbeat The pure keepalive: extend the owning key's activity-based lease (per-channel TTL) WITHOUT any other side effect — no spine write and nothing visible to the visitor. Any other owner action (posting a message, typing, reading messages) also extends the lease, so call heartbeat only when you have no other action to send but want to hold the claim. (vs escalationTyping, which also extends the lease but additionally shows the visitor a typing indicator.) # Get an agent's escalation policy Source: https://docs.flowyte.com/api-reference/escalation/get-an-agents-escalation-policy /openapi.yaml get /agents/{id}/escalation-policy Return the agent's DRAFT external-agent (AI Harness) escalation policy — the ordered deterministic routing rules + fallback. An agent with no policy returns an empty rule list. # Get an escalation Source: https://docs.flowyte.com/api-reference/escalation/get-an-escalation /openapi.yaml get /escalations/{id} Fetch one escalation session. The `contextPackage` (the handoff bundle) is included ONLY when the requesting connector key is the CLAIMING owner; fetching it as the owner extends the activity-based lease. The bundle is free-form but typically carries `transcriptWindow` (recent turns), `completedActions` / `failedActions` (what the AI already did/attempted), and `verifiedIdentifiers` (identity the AI confirmed) so the human/agent starts fully in context. # Get an escalation destination Source: https://docs.flowyte.com/api-reference/escalation/get-an-escalation-destination /openapi.yaml get /escalation-destinations/{id} Fetch one escalation destination. The signing secret is NEVER returned by a read path. # Initiate an outbound escalation (harness-started SMS thread) Source: https://docs.flowyte.com/api-reference/escalation/initiate-an-outbound-escalation-harness-started-sms-thread /openapi.yaml post /escalations Open a BRAND-NEW SMS conversation with a customer and send its first message, with the thread owned by the calling connector from the first instant. This is the outbound-initiated sibling of the inbound escalation path: once the session exists the customer's replies route to your harness automatically (the AI does not answer them), so you continue on POST /escalations/{id}/messages exactly as you would on any escalation you claimed. The session is created ALREADY CLAIMED by the calling key and active — you do not race a claim SLA on a thread you started. COMPLIANCE (all enforced server-side, none of it optional): * There is no `from` field. The sending number is derived from the agent's SMS-active number; an agent with none returns 422 `no_sms_number`. * `to` must be a US or Canada (+1) number. The platform's numbers and its A2P 10DLC/TCR registration are North American, so any other destination is refused 422 `unsupported_country` — nothing is sent. * Volume is capped per workspace: a rolling 24-hour ceiling on how many conversations a harness may START (200 by default). Past it, 429 `daily_initiate_cap` carries `details.nextOpen` and `Retry-After`. An idempotent replay is answered from the original receipt and is never capped. * The recipient must have a recorded consent basis — `conversational` (a customer who texted you first) or `express` (captured in a call or chat). Otherwise 422 `consent_required`. Orgs that hold consent out-of-band can be granted a bypass, which is recorded in the compliance trail. * The first text is business-initiated, so it is quiet-hours gated and carries the required brand + "Msg & data rates may apply. Reply HELP for help, STOP to opt out." disclosure block, appended by the platform. Do not add your own. * An initiate that did NOT send is always an explicit error, never a 200 with delivered:false. A quiet-hours refusal is 409 `quiet_hours` and carries `details.nextOpen` — retry after it. IDEMPOTENCY. The key is (organization, `client_message_id`) — durable and stable, so a replay returns the original receipt (byte for byte) and never sends a second text: after the original escalation was resolved, after the 72-hour conversation-episode rollover, and against a concurrent duplicate of itself. Reusing an id for a DIFFERENT message is 409 `client_message_id_reused`. 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 5xx), the answer is 409 `send_state_unknown` carrying `details.escalationId` and `details.conversationId`. It is the ONLY refusal here that does not promise the recipient's phone stayed silent, so do not blindly retry: read the thread, or replay the SAME `client_message_id` — a replay resolves to the same answer, or to the receipt once the state settles, and never sends a second text. Requires the deployment to have the Agent Bridge enabled (503 `bridge_disabled`), the org to be enabled for harness-initiated conversations (403 `harness_outbound_disabled`) and on a Starter plan or higher (403 `plan_required`). # List escalation destinations Source: https://docs.flowyte.com/api-reference/escalation/list-escalation-destinations /openapi.yaml get /escalation-destinations List the org's external-agent (AI Harness) escalation destinations. The signing secret is NEVER returned by a read path. # List escalations Source: https://docs.flowyte.com/api-reference/escalation/list-escalations /openapi.yaml get /escalations List the org's escalation sessions (newest first). The handoff context package is NEVER included in the list — fetch a single escalation as its claiming owner to receive it. # Post a harness reply to the visitor Source: https://docs.flowyte.com/api-reference/escalation/post-a-harness-reply-to-the-visitor /openapi.yaml post /escalations/{id}/messages Append an `external_agent` turn to the conversation spine and deliver it to the visitor (chat push / SMS reply through the same compliance chokepoint as the AI — opt-out, quiet-hours, and consent are enforced identically). Idempotent by `client_message_id`: a replay returns the original receipt. The first message flips the session claimed→active. Harness messages meter exactly like AI messages. `mediaUrls` is chat-only (supplying it on an SMS session → 422). # Replace an agent's escalation policy (writes the DRAFT) Source: https://docs.flowyte.com/api-reference/escalation/replace-an-agents-escalation-policy-writes-the-draft /openapi.yaml put /agents/{id}/escalation-policy Replace the agent's escalation policy. Writes the DRAFT agent config; publish freezes it into the compiled config (the tester exercises the draft, live traffic uses the published copy). Validated at write AND re-validated at runtime: every `external:` route must point to an OWNED destination that is `active`, whose AI-Harness integration is connected, and whose allowedChannels superset the rule's channels; an sms-channel rule additionally requires the org's 10DLC/TCR campaign ACTIVE. The reserved LLM-classifier conditions (intent_in / confidence_gte / sentiment) are rejected with 422 `unsupported_condition`. The PAYG plan gate is a non-blocking WARNING here (save is allowed); it is enforced at runtime (external rules are skipped on PAYG). Each rule's `routeTo` (and the fallback's) uses the ref grammar `external:` | `email:
` | `queue:`. # Replay the conversation spine (sync / mailbox) Source: https://docs.flowyte.com/api-reference/escalation/replay-the-conversation-spine-sync-mailbox /openapi.yaml get /escalations/{id}/messages Return the conversation's turns with `seq > after_seq`, ordered oldest→newest (both roles, harness turns included). The webhook `escalation.message.created` is the doorbell; THIS endpoint is the mailbox — dedupe by `seq` and gap-sync from it. Extends the owner's lease. # Request a human takeover Source: https://docs.flowyte.com/api-reference/escalation/request-a-human-takeover /openapi.yaml post /escalations/{id}/request-human HUMAN TAKEOVER terminal: route the conversation onward to the human fallback chain (support email / seat queue when phone infra is wired). Use when the external agent cannot handle it and a Flowyte-side human should take over. Owner only. (vs resolveEscalation = case closed; vs returnEscalation = hand back to the AI.) # Resolve an escalation Source: https://docs.flowyte.com/api-reference/escalation/resolve-an-escalation /openapi.yaml post /escalations/{id}/resolve CASE CLOSED terminal: end the escalation as resolved with an optional outcome + note. This is terminal — the session moves to `resolved` and the connector's turn is over; the AI does NOT resume. Owner only. (Pick between the three terminals: resolve = case closed here; returnEscalation = hand back to the AI, which resumes on the next visitor message; requestHumanEscalation = escalate onward to the human fallback chain.) # Return the thread to the AI Source: https://docs.flowyte.com/api-reference/escalation/return-the-thread-to-the-ai /openapi.yaml post /escalations/{id}/return AI RESUMES terminal: hand control back to the Flowyte AI, which picks up on the next visitor message with the full conversation history. Use this when the human/external agent is done but the conversation should continue with the AI (NOT closed). Owner only. (vs resolveEscalation = case closed, AI does not resume; vs requestHumanEscalation = route onward to a human.) # Rotate the destination signing secret (new secret shown once) Source: https://docs.flowyte.com/api-reference/escalation/rotate-the-destination-signing-secret-new-secret-shown-once /openapi.yaml post /escalation-destinations/{id}/rotate-secret Generate a new `whsec_` signing secret, returned EXACTLY ONCE — store it immediately. The previous secret stays valid for a 24-hour dual-sign window so an in-flight rotation does not drop deliveries. # Run the destination handshake (go-live gate) Source: https://docs.flowyte.com/api-reference/escalation/run-the-destination-handshake-go-live-gate /openapi.yaml post /escalation-destinations/{id}/test Prove the destination's external agent can talk back and forth BEFORE it can go live. Opens a sandbox escalation (`is_test`), delivers a signed `escalation.test` webhook to the endpoint, and (synchronous-wait) polls until the connector claims → posts one message → resolves, or the deadline lapses. On a green run the destination flips to `status='active'` with `lastVerifiedAt` set; on a red run it stays `unverified` (an already-active destination regresses to `unverified` and emits `escalation.destination.unavailable`) with an actionable `hint`. Returns the step-by-step result either way — HTTP 200 for BOTH pass and fail (the call itself succeeded; the `result` field carries the verdict). The test works on ANY plan (setup precedes upgrade) and never touches a visitor or a wallet. # Send a typing indicator (chat only) Source: https://docs.flowyte.com/api-reference/escalation/send-a-typing-indicator-chat-only /openapi.yaml post /escalations/{id}/typing Push an ephemeral, visitor-VISIBLE typing indicator (chat only — SMS returns 422 `typing_unsupported_channel`). It writes nothing to the conversation spine but DOES extend the owner's lease. (vs heartbeatEscalation, the pure keepalive: same lease extension but nothing the visitor sees.) # Update an escalation destination Source: https://docs.flowyte.com/api-reference/escalation/update-an-escalation-destination /openapi.yaml patch /escalation-destinations/{id} Partial update — an omitted field is left unchanged. Changing `endpointUrl` re-validates it (https + SSRF) and resets `status` to `unverified` — the new endpoint must re-pass the handshake test before it can route live. Note `status` can only be set to `disabled` or `unverified` here; a destination reaches `active` ONLY via testEscalationDestination, never a direct PATCH. # Get caller verification settings Source: https://docs.flowyte.com/api-reference/guardrails/get-caller-verification-settings /openapi.yaml get /agents/{agentId}/caller-verification Returns the agent's caller-identity verification configurations, including the verification method (such as date of birth plus postcode, an SMS one-time code, an account PIN, or knowledge-based questions) and the fields required to confirm a caller. # Get guardrail policies Source: https://docs.flowyte.com/api-reference/guardrails/get-guardrail-policies /openapi.yaml get /agents/{agentId}/guardrails Returns the agent's guardrail policies: rules that constrain what the agent may do, such as verifying a caller before disclosing information, limiting which fields can be shared, gating actions that write data, rate limits, business hours, and content rules. # Replace caller verification settings Source: https://docs.flowyte.com/api-reference/guardrails/replace-caller-verification-settings /openapi.yaml put /agents/{agentId}/caller-verification Replaces the agent's full set of caller-verification configurations with the list you send. This is a full overwrite, so include every configuration you want to keep; any one you omit is removed. # Replace guardrail policies Source: https://docs.flowyte.com/api-reference/guardrails/replace-guardrail-policies /openapi.yaml put /agents/{agentId}/guardrails Replaces the agent's full set of guardrail policies with the list you send. This is a full overwrite, so include every policy you want to keep; any policy you omit is removed. # Add an integration's actions as skills Source: https://docs.flowyte.com/api-reference/integrations/add-an-integrations-actions-as-skills /openapi.yaml post /agents/{agentId}/integrations/{kind}/provision Turns a connected integration into ready-to-use skills on an agent: one skill per action, or every key action when none are specified. Re-running is safe, as actions that already exist as skills are skipped. Actions that still need configuration are created as disabled drafts until you complete their settings. # Auto-map a binding from a goal Source: https://docs.flowyte.com/api-reference/integrations/auto-map-a-binding-from-a-goal /openapi.yaml post /agents/{agentId}/integrations/{kind}/bindings/auto Describe what you want in plain language ("look up a caller by phone and return their open tickets") and the AI assistant proposes a binding over the provider's discovered schema: which operation to call, how to map your agent's inputs to its arguments, and which fields to return. The proposal is validated against the schema and saved as a disabled draft for you to review before you enable it. Returns 422 if it can't produce a valid binding, 404 if the provider hasn't been discovered yet. # Browse a provider's objects Source: https://docs.flowyte.com/api-reference/integrations/browse-a-providers-objects /openapi.yaml get /integrations/{kind}/objects Returns the searchable, paginated list of objects in the provider's discovered schema, each with its capabilities and field, relationship, and operation counts, so you can pick what to map without loading the whole schema. Returns 404 if the provider hasn't been discovered yet. # Browse a provider's operations Source: https://docs.flowyte.com/api-reference/integrations/browse-a-providers-operations /openapi.yaml get /integrations/{kind}/operations Returns the operations a binding can call. By default only read operations; pass readOnly=false to include writes. Filter by object (operations that return that object) or q (a name substring). # Claim a Shopify App Store install Source: https://docs.flowyte.com/api-reference/integrations/claim-a-shopify-app-store-install /openapi.yaml post /integrations/{kind}/claim Completes a Shopify install that began on the Shopify App Store before the merchant had a Flowyte account. After installing from the App Store, the merchant signs in and POSTs `{ shop, claimSecret }` (the one-time secret carried in the post-install redirect); the stored token is then attached to their organization as a normal connection. Returns 403 if the secret doesn't match, or 404 if there's no pending install for that shop. Shopify only. # Connect an integration Source: https://docs.flowyte.com/api-reference/integrations/connect-an-integration /openapi.yaml post /integrations/{kind}/connect Starts a connection to an integration. For OAuth providers it returns a URL to send the user to for authorization. For API-key providers, send the credentials in the request body; they are stored securely and never returned. Returns an error if the provider is not yet available to connect. # Disconnect an integration Source: https://docs.flowyte.com/api-reference/integrations/disconnect-an-integration /openapi.yaml delete /integrations/{kind} Disconnects an integration from your organization and removes its stored credentials. You can reconnect it again later. # Discover an integration's schema Source: https://docs.flowyte.com/api-reference/integrations/discover-an-integrations-schema /openapi.yaml post /integrations/{kind}/discover Scans a connected integration and builds a normalized map of all its objects, fields, relationships, and operations. Running it again replaces the previous snapshot. It returns a summary of what was found; read the full schema back with the get-schema endpoint. The integration must be connected first. # Dry-run the softphone contact card against a number Source: https://docs.flowyte.com/api-reference/integrations/dry-run-the-softphone-contact-card-against-a-number /openapi.yaml post /integrations/{kind}/contact-card/probe Runs the connection's effective ContactCardSpec against a typed E.164 number and returns the preview card the softphone would show (matched name/company, open deal, deep link, and the multiple-matches flag). A config-time helper; makes ONE guarded, budgeted CRM read. 404 if the provider isn't connected. # Freeze an MCP tool into a skill Source: https://docs.flowyte.com/api-reference/integrations/freeze-an-mcp-tool-into-a-skill /openapi.yaml post /agents/{agentId}/integrations/{kind}/mcp-skills Turns one equipped MCP tool (see the tools list) into a frozen `mcp` skill on the agent. The agent never freeforms the call: only the parameters you expose become the skill's inputs, and pinned arguments are fixed at runtime. Every required input must be pinned or exposed, and sensitive fields (government ID, financial, credential, health) can't be pinned or exposed. Because an MCP tool carries no read/write metadata, the skill is treated as a write (needs confirmation, runs non-parallel) unless you attest that it's read-only. `allowedFields` is deny-by-default — list the dotted field paths the agent is allowed to receive back. # Freeze one resolved Composio tool into a `composio` skill on the agent (Agent Bridge ) Source: https://docs.flowyte.com/api-reference/integrations/freeze-one-resolved-composio-tool-into-a-`composio`-skill-on-the-agent-agent-bridge- /openapi.yaml post /agents/{agentId}/integrations/{kind}/skills Turn ONE Composio tool (see GET /integrations/composio/tools + POST …/tools/{slug}/resolve) into a frozen `composio` skill on the agent, pinned to a connected Composio account. It is the sibling of the Zapier mcp-skills freeze, GENERALIZED for Composio: the tool's `input_parameters` are closed through the SAME `additionalProperties:false` synthesizer, and it reuses the SAME freeze gates verbatim — only the operator-EXPOSED params become the skill's parameters_schema, PINNED args are layered on at runtime, every schema-required input must be pinned OR exposed, and pinning/exposing a government-id / financial / credential / health field is refused. Because a Composio tool carries NO read/write metadata, the skill lands FAIL-CLOSED as a WRITE (confirm + non-parallel + high stakes) UNLESS `readAttestation.attestedRead` is true — a human downgrade recorded verbatim for audit. `allowedFields` (dotted leaf paths) is deny-by-default; `outputPick` is an optional top-level output narrowing. `fireAndForget` (WRITE only, the voice-turn default — Composio has no latency SLA) enqueues the send off-turn on the CONFIRMED call. The pinned `connectedAccountId` must be an ACTIVE account for `toolkitSlug`. Composio-only. 404 if the agent/kind is wrong; 409 `integration_not_connected` if the account isn't connected/active; 422 for any validation failure; 503 if Composio is unconfigured. Scope `skills:write` (creates a skill). Use this when the connector is **Composio** (`kind=composio`). For a **Zapier MCP** tool use the sibling `freezeMcpSkill` (POST /agents/{agentId}/integrations/zapier/mcp-skills) instead — the two have an identical request/response shape and differ only by connector `kind`. # Generate database setup scripts Source: https://docs.flowyte.com/api-reference/integrations/generate-database-setup-scripts /openapi.yaml get /integrations/{kind}/sql/scripts Generates the copy-paste SQL to create a scoped database user for the connection: a least-privilege read-only user (SELECT on current and future tables) and a scoped read-write script (INSERT/UPDATE on only the tables your write skills target — never blanket write). If you don't supply a password, a strong one is generated and returned once. Pure generation — it never touches your database. Pass database (required), and optionally ro_user, rw_user, tables (comma-separated write-skill tables), and password. # Get a connection's data scoping Source: https://docs.flowyte.com/api-reference/integrations/get-a-connections-data-scoping /openapi.yaml get /integrations/{kind}/scoping Returns the connection's data scoping — the tables and columns the agent may NOT see or bind. Empty lists mean everything is allowed. This is a privacy control you set on a connected SQL database, and it survives re-discovery. # Get a connection's softphone contact-card spec Source: https://docs.flowyte.com/api-reference/integrations/get-a-connections-softphone-contact-card-spec /openapi.yaml get /integrations/{kind}/contact-card Returns the per-connection ContactCardSpec the Flowyte Phone screen-pop card is assembled from — which provider fields fill the card's name/company slots, the optional open-deals read, the deep-link template, the screen-pop priority, and whether the card is enabled. A connected CRM with no override returns its Connector Pack DEFAULT card (isDefault=true). 404 if the provider isn't connected. # Get a connection's write-back settings Source: https://docs.flowyte.com/api-reference/integrations/get-a-connections-write-back-settings /openapi.yaml get /integrations/{kind}/writeback Returns the connection's interaction write-back settings — the logging mode (off, summary, or summary + transcript), the on-no-match policy, when it was enabled, and delivery counters (sent, failed, skipped). When on, a summary (and optionally a transcript) of each conversation is written back to the connected CRM after the call; transcripts are scrubbed of card and ID numbers first. Default is off. Returns 404 if the provider isn't connected. # Get a connector's author guidance Source: https://docs.flowyte.com/api-reference/integrations/get-a-connectors-author-guidance /openapi.yaml get /integrations/{kind}/guidance Returns the natural-language guidance for a connector that ships no preset pack — for example Zapier, whose skills come from freezing your equipped MCP tools rather than curated presets. It's the guidance-only companion to the Connector Pack: the same "how to build for this provider" the AI assistant and builder read (when to use a native connector instead, how to author an MCP skill, the records store, and billing). Returns 404 for a provider that has no standalone guidance (a pack provider's guidance is on its pack endpoint). # Get a provider's preset pack Source: https://docs.flowyte.com/api-reference/integrations/get-a-providers-preset-pack /openapi.yaml get /integrations/{kind}/pack Returns the provider's Connector Pack: its curated preset skills (ready to install) plus guidance on how its operations behave, how to identify a caller, and what isn't possible. Use it to build skills and playbooks for the provider, or install the presets directly. Returns 404 if the provider ships no pack. # Get an integration's discovered schema Source: https://docs.flowyte.com/api-reference/integrations/get-an-integrations-discovered-schema /openapi.yaml get /integrations/{kind}/schema Returns the most recently discovered schema for a connected integration, including all of its objects, fields, relationships, and operations. Run the discover endpoint first; this returns an error if the integration has never been scanned. # Get one object's detail Source: https://docs.flowyte.com/api-reference/integrations/get-one-objects-detail /openapi.yaml get /integrations/{kind}/objects/{object} Returns one object's full detail: its fields (type, whether it's required, allowed values, relationship target, and whether it's sensitive), its operations, and its relationships to other objects. Returns 404 for an unknown object. # Install a provider's preset skills Source: https://docs.flowyte.com/api-reference/integrations/install-a-providers-preset-skills /openapi.yaml post /agents/{agentId}/integrations/{kind}/pack/install Installs a provider's curated preset skills (its Connector Pack) onto the agent in one call. Idempotent: a preset whose skill already exists is skipped and returned with created:false. Returns 404 if the agent doesn't exist or the provider ships no pack, 409 if the provider isn't connected for your organization. # List a connected MCP endpoint's tools Source: https://docs.flowyte.com/api-reference/integrations/list-a-connected-mcp-endpoints-tools /openapi.yaml get /integrations/{kind}/tools For an MCP-backed connector like Zapier, lists the tools equipped on your connected MCP endpoint — each tool's name, description, input schema, and any annotations. This is the discovery step for authoring an `mcp` skill: pick a tool, then freeze it into a skill. The call is SSRF-guarded and domain-allow-listed. Returns 409 if the connector isn't connected, 400 if the provider isn't MCP-backed, or 502 if the MCP endpoint is unavailable. # List a provider's connected accounts (Composio) Source: https://docs.flowyte.com/api-reference/integrations/list-a-providers-connected-accounts-composio /openapi.yaml get /integrations/{kind}/accounts Composio (Agent Bridge ) only: the org's Composio connected accounts — each account's toolkit, mirrored Composio status (INITIALIZING | INITIATED | ACTIVE | FAILED | EXPIRED | DELETED | INACTIVE), and label. NEVER a credential (the third-party OAuth tokens live at Composio and never transit Flowyte). A non-composio kind returns 404 (no connected-accounts list for that provider). # List a provider's declarative trigger events (the outbound-automation catalog) Source: https://docs.flowyte.com/api-reference/integrations/list-a-providers-declarative-trigger-events-the-outbound-automation-catalog /openapi.yaml get /integrations/{kind}/triggers The provider's Trigger Pack — the declarative trigger EVENTS it ships (each event's output-field tree, the curated default binding, the delivery mode, an author guidance blurb) the Assist / builder / any API/MCP caller read to author an outbound trigger ("when {event} in {provider} → call {agent}"). Author one via POST /agents/{agentId}/integrations/{kind}/triggers. 404 for a kind with no trigger pack. # List an integration's available actions Source: https://docs.flowyte.com/api-reference/integrations/list-an-integrations-available-actions /openapi.yaml get /integrations/{kind}/actions Returns the actions an integration offers, each of which can be turned into a skill. For every action you get its name, description, input parameters, and which configuration values you still need to provide before it can run. # List connected integrations Source: https://docs.flowyte.com/api-reference/integrations/list-connected-integrations /openapi.yaml get /integrations Returns your organization's integrations and their connection status. Stored credentials are never included in the response. # List the integration catalog Source: https://docs.flowyte.com/api-reference/integrations/list-the-integration-catalog /openapi.yaml get /integrations/catalog Returns every integration you can connect, including its name, category, description, logo, and how many actions it offers, combined with whether your organization has already connected it and the connected account label. # Look up a connected spreadsheet Source: https://docs.flowyte.com/api-reference/integrations/look-up-a-connected-spreadsheet /openapi.yaml get /integrations/{kind}/spreadsheet Helps you build a spreadsheet skill: given a spreadsheet link or id, it confirms your connected account can read it and returns the spreadsheet's title, its tabs, and each tab's column headers so you can pick a tab and map fields to the real columns. It is read-only and returns an error if the spreadsheet isn't connected or can't be accessed. # Map an integration's fields to a skill Source: https://docs.flowyte.com/api-reference/integrations/map-an-integrations-fields-to-a-skill /openapi.yaml post /agents/{agentId}/integrations/{kind}/bindings Creates a skill on an agent by mapping a discovered integration operation: pick the operation, map your agent's inputs to its arguments, and choose which provider fields to return. The mapping is validated against the integration's discovered schema and compiles to a runnable skill — created as a draft unless you enable it. Connect and discover the integration first. # Probe an MCP tool (runs the real action) Source: https://docs.flowyte.com/api-reference/integrations/probe-an-mcp-tool-runs-the-real-action /openapi.yaml post /integrations/{kind}/tools/{tool}/probe ⚠️ This runs the real action. An MCP tool has no output schema and no sandbox, so to learn which fields it returns (for choosing `allowed_fields` when you freeze it) we execute the tool once against your connected MCP endpoint with the `args` you supply. Because it performs a live action, you must set `acknowledge_execution: true` (422 otherwise). On success it returns the response flattened to dotted field paths with sample values and a PII class per field; on a provider failure it returns empty results with an error class instead of an HTTP error. Returns 409 if the connector isn't connected. # Remove an integration's skills from an agent Source: https://docs.flowyte.com/api-reference/integrations/remove-an-integrations-skills-from-an-agent /openapi.yaml delete /agents/{agentId}/integrations/{kind}/provision Removes the skills that were created from a given integration on this agent. Skills you built by hand, and skills from other integrations, are left in place. # Resolve one enabled agentic action (Zapier) or one Composio tool into a pinnable schema Source: https://docs.flowyte.com/api-reference/integrations/resolve-one-enabled-agentic-action-zapier-or-one-composio-tool-into-a-pinnable-schema /openapi.yaml post /integrations/{kind}/tools/{tool}/resolve For a Zapier AGENTIC (dynamic-discovery) MCP server, re-resolve ONE enabled action against the parent pins materialized so far and return the CLOSED-so-far input schema plus the materialized dynamic-enum options for the requested `enumProperty`. This is the pin-parent loop that turns an agentic server's open executor meta-tools into a concrete, pinnable schema the freeze flow accepts: while a required dynamic parent is still unpinned the schema is not yet closable (`needsParentPin` is non-empty and `inputSchema` is omitted) — a NORMAL loop step, not an error. Drive the loop off `needsParentPin` + `dynamicEnumValues` (pinning one option each round via `params`) until `inputSchema` closes, then freeze with the resolved `agentic` block. 0-task authoring read (rides the per-org resolve limiter, off the hot execute path); it never surfaces the credential/endpoint. For `composio` (Agent Bridge ) the `{tool}` path segment is the Composio tool slug: the server fetches its `input_parameters` and closes them through the SAME `additionalProperties:false` synthesizer, returning the same `ResolvedActionSchema` (Composio's static schema has no dynamic enums, so `needsParentPin` is inherited but not naturally triggered). Zapier/composio only (400 `tools_not_supported` for any other kind); 409 `integration_not_connected` if a zapier connector isn't connected; 422 `mcp_structured_param_unsupported` when a structured (object/array) param can't be closed into a skill schema; 503 if composio is unconfigured. Scope `integrations:read`. # Search a connected agentic account's available actions to enable (Zapier) Source: https://docs.flowyte.com/api-reference/integrations/search-a-connected-agentic-accounts-available-actions-to-enable-zapier /openapi.yaml post /integrations/{kind}/tools/discover For a Zapier AGENTIC (dynamic-discovery) MCP server, search the connected account's AVAILABLE actions (by app/action free text — empty ⇒ the server's default set) so the picker can offer them to enable. This is the escape hatch that keeps a fresh/empty agentic server from being a dead-end picker: discover → enable → resolve → freeze. Each returned action carries the identity the enable call needs (`selectedApi` + `action`) plus display text. 0-task authoring read; never surfaces the credential/endpoint. Zapier-only (400 `tools_not_supported` for a non-MCP kind); 409 `integration_not_connected` if not connected. Scope `integrations:read`. Use this to search a Zapier agentic account's AVAILABLE ACTIONS to enable. Do NOT confuse with the similarly-named `discoverIntegrationSchema` (POST /integrations/{kind}/discover), which introspects a NATIVE provider's full object/field schema graph — a different job despite the near-identical path. # Search a provider's schema Source: https://docs.flowyte.com/api-reference/integrations/search-a-providers-schema /openapi.yaml get /integrations/{kind}/search Ranked keyword search across a connected provider's discovered schema (objects and fields), most-relevant first. When a schema is large, search for what your goal needs instead of browsing everything: each hit carries the object, field, type, relationship target, and badges (core, PII, payment-card). Use it to find the right field or object before authoring a binding, then expand the object for detail. Pass `q` (what to find) and an optional `limit`. # Set a connection's data scoping Source: https://docs.flowyte.com/api-reference/integrations/set-a-connections-data-scoping /openapi.yaml patch /integrations/{kind}/scoping Sets the tables and columns the agent may NOT see or bind for this connection. Blocked tables and columns disappear from the schema browser and are refused at bind time. Empty lists clear the blocks (allow all). # Set a connection's softphone contact-card spec Source: https://docs.flowyte.com/api-reference/integrations/set-a-connections-softphone-contact-card-spec /openapi.yaml put /integrations/{kind}/contact-card Upserts the connection's ContactCardSpec override — the slot projections, open-deals read, the deep-link template (only {{id}} and {{portal_id}} placeholders are allowed; an unknown placeholder or a non-https URL is rejected), the screen-pop priority (lower wins when multiple CRMs are connected), and the enabled flag. All fields optional; omitted fields keep their current value. 404 if the provider isn't connected. The `spec` is a free-form object. Expected keys: `searchObject` (the CRM object matched on the caller number), `nameFields` / `companyField` (provider field paths filling the card's name/company slots), `openDeals` (optional { object, statusFilter, limit } read), and `deepLinkTemplate` (an https URL using only {{id}} / {{portal_id}} placeholders). # Set a connection's write-back settings Source: https://docs.flowyte.com/api-reference/integrations/set-a-connections-write-back-settings /openapi.yaml patch /integrations/{kind}/writeback Sets the interaction-logging mode (off, summary, or summary + transcript) and the on-no-match policy (skip or create) for this connection. The on-no-match policy is left unchanged if omitted. Turning it on starts logging from now — past interactions aren't back-filled. Returns 404 if the provider isn't connected. # Test a database connection Source: https://docs.flowyte.com/api-reference/integrations/test-a-database-connection /openapi.yaml post /integrations/{kind}/sql/test For a SQL database connector (Postgres or MySQL), runs an ordered set of checks against the credentials you supply — reachability and TLS, authentication, reading the schema, a timed read probe, a read-only-session proof, and a live-call latency verdict — without storing anything (this powers the connect wizard's "Test connection" button). Always returns 200 with the ordered check results and an overall ok flag; a failed check is data to render, not an error. A host that resolves to a private, loopback, or metadata address is refused. # Turn one discovered agentic action ON so it can be resolved and frozen (Zapier) Source: https://docs.flowyte.com/api-reference/integrations/turn-one-discovered-agentic-action-on-so-it-can-be-resolved-and-frozen-zapier /openapi.yaml post /integrations/{kind}/enable For a Zapier AGENTIC (dynamic-discovery) MCP server, turn ONE discovered action (see POST …/tools/discover) ON in the connected account so it appears in the enabled set and can be resolved + frozen — so a fresh/empty server isn't a dead-end. Because it MUTATES the connected account's enabled set, this is `integrations:write` (unlike the discover/resolve reads); it never surfaces the credential/endpoint and invalidates the org's enabled-actions cache so the freshly-enabled action shows on the next GET …/tools. Zapier-only (400 `tools_not_supported` for a non-MCP kind); 409 `integration_not_connected` if not connected; 422 when `action` is missing. # Using the API Source: https://docs.flowyte.com/api-reference/introduction Conventions that apply to every endpoint — base URL, auth, envelopes, errors, pagination, and streaming. The endpoint pages in this section are generated from the OpenAPI contract. This page covers the conventions that apply across all of them. ## Base URL ``` https://builder.flowyte.com/api/v1 ``` The API is versioned in the path and additive within `v1`. ## Authentication Send a secret key as a bearer token: `Authorization: Bearer flowyte_sk_…`. See [Authentication](/get-started/authentication) for keys, scopes, and the publishable-key path for the chat widget. ## Response envelope Successful responses are wrapped in an `ApiResponse` envelope: ```json theme={null} { "success": true, "data": { /* the resource */ } } ``` List responses use `PaginatedResponse` with **cursor** pagination — pass the returned cursor to fetch the next page rather than page numbers. ## Errors Errors use [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem+json (`application/problem+json`): ```json theme={null} { "type": "https://docs.flowyte.com/errors/no_published_version", "title": "No published version", "status": 409, "detail": "agent has no published version; publish it or retry with draftMode:true" } ``` Treat the `type` / `status` pair as the recovery contract. ## Streaming (SSE) Streaming endpoints (simulate, assist, probe) return `text/event-stream`. Each frame is `event:\ndata:\n\n`; the stream terminates with `event: done` (or `event: error`). Consume them with `fetch()` streaming so your `Authorization` header attaches. ## Idempotency Money-moving endpoints accept an idempotency key so a retry replays the original result instead of charging twice. ## Rate limits The API is rate-limited per organization. Limits are generous for normal use; if you hit one, back off and retry. # Add a knowledge source Source: https://docs.flowyte.com/api-reference/knowledge/add-a-knowledge-source /openapi.yaml post /agents/{agentId}/knowledge/sources Attaches a new knowledge source to the agent from a web page URL, an uploaded file, raw text, or an FAQ. Ingestion runs in the background, so the source is returned with a 'pending' status; poll the source until its status becomes 'indexed' before relying on it in conversations. # Delete a knowledge source Source: https://docs.flowyte.com/api-reference/knowledge/delete-a-knowledge-source /openapi.yaml delete /agents/{agentId}/knowledge/sources/{id} Permanently removes a knowledge source and all of the indexed content derived from it, so the agent will no longer use it. # Get a knowledge source Source: https://docs.flowyte.com/api-reference/knowledge/get-a-knowledge-source /openapi.yaml get /agents/{agentId}/knowledge/sources/{id} Returns a single knowledge source, including its full text content and current ingestion status. Poll this endpoint after creating a source to track progress until the status is 'indexed' (or 'error'). # List knowledge sources Source: https://docs.flowyte.com/api-reference/knowledge/list-knowledge-sources /openapi.yaml get /agents/{agentId}/knowledge/sources Returns a paginated list of the knowledge sources attached to an agent, each with its current ingestion status. The full text of a source is omitted here; fetch a single source to see its content. # Preview knowledge search Source: https://docs.flowyte.com/api-reference/knowledge/preview-knowledge-search /openapi.yaml post /agents/{agentId}/knowledge/preview Runs a search query against the agent's indexed knowledge and returns the best-matching passages with similarity scores. Use it to test coverage and see what the agent would retrieve for a question. You can optionally limit the search to specific sources. # Refresh a knowledge source now Source: https://docs.flowyte.com/api-reference/knowledge/refresh-a-knowledge-source-now /openapi.yaml post /agents/{agentId}/knowledge/sources/{id}/refresh Triggers an immediate re-crawl of a website knowledge source, whether or not auto-refresh is on. Returns 202 and re-crawls in the background; the source is re-indexed only if its content changed. Poll the source to watch `lastCheckedAt` and `lastChangedAt` update. # Set a knowledge source's auto-refresh Source: https://docs.flowyte.com/api-reference/knowledge/set-a-knowledge-sources-auto-refresh /openapi.yaml patch /agents/{agentId}/knowledge/sources/{id} Opts a website knowledge source into automatic re-crawl and re-index, or changes its cadence. Auto-refresh is opt-in and applies to URL sources only; the cadence has a 24-hour minimum (default weekly). Re-indexing happens only when the re-crawled content actually changed, so an unchanged site costs nothing beyond the crawl. # Get language capability matrix Source: https://docs.flowyte.com/api-reference/meta/get-language-capability-matrix /openapi.yaml get /meta/languages Returns which languages are available for each agent type (single-language or multilingual) and each voice-quality tier (budget, standard, or premium). Any authenticated caller can read this. # Get your account info Source: https://docs.flowyte.com/api-reference/meta/get-your-account-info /openapi.yaml get /meta/account Returns organization-scoped capability flags your app can read on load — for example whether outbound calling is enabled for your organization. Available to any authenticated caller; no special scope is required. # Update the organization's report settings (org/me write) Source: https://docs.flowyte.com/api-reference/meta/update-the-organizations-report-settings-orgme-write /openapi.yaml patch /meta/account The WRITE half of the org "org/me" settings surface. Sets the org's writable preferences — today the Observe REPORT timezone (`reportTimezone`). The report timezone is the first source in the reporting-tz precedence chain: when set, the app applies it (over the browser tz) as the default `?tz=` on every metric query, the day-bucket axis labels, the report schedule's default tz, and the print/PDF window; when unset, the client falls through to the browser tz. A non-empty `reportTimezone` MUST be a valid IANA zone (an unknown/malformed zone is rejected 400); an empty string CLEARS the preference (revert to the browser-tz default). Org-scoped: writes the caller's OWN organization row. Available to any authenticated principal (dual-auth) — no special scope is required. # Assign a number to an agent Source: https://docs.flowyte.com/api-reference/numbers/assign-a-number-to-an-agent /openapi.yaml post /numbers/{id}/assign Points a phone number at an agent so its inbound calls are answered by that agent. # Buy a phone number Source: https://docs.flowyte.com/api-reference/numbers/buy-a-phone-number /openapi.yaml post /numbers/purchase Purchases a number and charges your prepaid wallet. Optionally pass a reservation id from the reserve endpoint so the number is already held for you and can't be taken during checkout. Returns 402 if your wallet balance is too low. # Cancel a port-in request Source: https://docs.flowyte.com/api-reference/numbers/cancel-a-port-in-request /openapi.yaml post /numbers/port/{id}/cancel Cancel every non-terminal order on the request and mark it cancelled. Note the carrier blocks cancellation within 48 hours of a confirmed port date — that window is surfaced back to the caller. # Check one available number Source: https://docs.flowyte.com/api-reference/numbers/check-one-available-number /openapi.yaml get /numbers/available/{e164} Re-checks a single available number by its E.164 value to confirm its current price and availability right before purchase. Returns 404 if the number is no longer purchasable. # Check whether numbers can be ported in Source: https://docs.flowyte.com/api-reference/numbers/check-whether-numbers-can-be-ported-in /openapi.yaml post /numbers/port/precheck Free pre-flight check: given up to 10 E.164 numbers, report per number whether the carrier says it is portable, its type (local/mobile/toll-free) and current carrier when known, and whether Flowyte supports porting it today. `supported` is false (with a `reason`, e.g. `toll_free_deferred`) when we cannot port it in this release even if the carrier reports it portable. Requires no paid plan — it is the teaser that runs before the wizard. # Delete a draft port-in request Source: https://docs.flowyte.com/api-reference/numbers/delete-a-draft-port-in-request /openapi.yaml delete /numbers/port/{id} Soft-delete a request that is still a draft (best-effort deleting any draft order created for it). Submitted requests must be cancelled, not deleted. # Edit a port-in request Source: https://docs.flowyte.com/api-reference/numbers/edit-a-port-in-request /openapi.yaml patch /numbers/port/{id} Edit a request. In `draft` all account fields and the numbers list are editable; in `action_needed` only the account fields are editable (the numbers are frozen). Any other state rejects the edit. # Get a port-in request Source: https://docs.flowyte.com/api-reference/numbers/get-a-port-in-request /openapi.yaml get /numbers/port/{id} Full detail for one port-in request: the account fields (for resume/fix), its numbers (pre-assignment + landing), its order(s) (stage, FOC date, support key, normalized action-needed items with the field to fix), and live porting-desk notes for non-terminal requests. # Import a number you already own Source: https://docs.flowyte.com/api-reference/numbers/import-a-number-you-already-own /openapi.yaml post /numbers/import Brings a phone number you already hold with your telephony provider into your account at no charge, since it is already paid for. You can optionally assign it to an agent in the same request; once imported, inbound calls to it reach that agent. # List port-in requests Source: https://docs.flowyte.com/api-reference/numbers/list-port-in-requests /openapi.yaml get /numbers/port List this org's port-in requests, newest first (excludes deleted). Each request embeds its numbers (with pre-assignment + landing) and its order(s) (stage, FOC date, support key, and any action-needed items). # List your phone numbers Source: https://docs.flowyte.com/api-reference/numbers/list-your-phone-numbers /openapi.yaml get /numbers Returns the phone numbers your account owns. Results are paginated. # Release a number Source: https://docs.flowyte.com/api-reference/numbers/release-a-number /openapi.yaml delete /numbers/{id} Permanently releases a number back to the carrier and stops billing for it. This cannot be undone. To keep the number but detach it from an agent, unassign it instead. # Release a number hold Source: https://docs.flowyte.com/api-reference/numbers/release-a-number-hold /openapi.yaml delete /numbers/reserve/{id} Cancels a reservation you no longer need and frees the held number for others. # Reserve a number before buying Source: https://docs.flowyte.com/api-reference/numbers/reserve-a-number-before-buying /openapi.yaml post /numbers/reserve Holds a number for about 30 minutes so it can't be taken by someone else while you finish checkout. This does not charge your wallet. Pass the returned reservation id to the purchase endpoint to claim the held number. # Search available numbers Source: https://docs.flowyte.com/api-reference/numbers/search-available-numbers /openapi.yaml get /numbers/search Browses purchasable phone numbers. All filters are optional: search by area code, city, state, full number, or a prefix/suffix for vanity and last-four matches, and require capabilities such as voice or SMS. A too-narrow filter is widened to nearby matches by default; set bestEffort to false for strict matching. # Set or clear a number's display label Source: https://docs.flowyte.com/api-reference/numbers/set-or-clear-a-numbers-display-label /openapi.yaml patch /numbers/{id} Set (or clear) the org-editable display label for an owned number — e.g. "Google Ads" or "Denver line" — which backs the "Calls by number" marketing-attribution widget's number chip. The label never routes, gates, or affects billing. Pass `label` as a string (trimmed, at most 64 characters) to set it, or `null` (or a blank string) to clear it. **The label is also ENTRY CONTEXT.** A call or text arriving on this number gives the agent the LABEL — never the digits — as `{entry.label}`, so one agent behind several numbers knows which door the customer came in through. Write it as something an agent could sensibly read ("Denver line", "Spanish line", "Spring tune-up"). It is a HINT the agent may use to pre-seed a question it still asks; it is never identity and never satisfies a skill's parameter binding. See `EntryContext`. # Set per-number agent pre-assignments Source: https://docs.flowyte.com/api-reference/numbers/set-per-number-agent-pre-assignments /openapi.yaml patch /numbers/port/{id}/assignments Choose which agent each number should ring at go-live (or clear it — "decide later"). Editable at any time until a number lands. Each agent must be visible to the org and published; the number goes live already connected so there is no gap. # Sign the letter of authorization Source: https://docs.flowyte.com/api-reference/numbers/sign-the-letter-of-authorization /openapi.yaml post /numbers/port/{id}/sign Attach the signer's Letter of Authorization signature (a base64 PNG, ≤200KB) to a draft request. The request must be complete first (all required account fields, bill, and ≥1 number); the server records the signer name, time, and IP for the anti-slamming audit trail. # Start a port-in request (draft) Source: https://docs.flowyte.com/api-reference/numbers/start-a-port-in-request-draft /openapi.yaml post /numbers/port Create a draft port-in request from the wizard's account details plus the numbers to port (each with an optional agent pre-assignment — which agent the number should ring at go-live). The numbers are re-checked server-side; unsupported, toll-free, or non-portable numbers are rejected with per-number reasons. Requires a paid plan. # Submit a port-in request Source: https://docs.flowyte.com/api-reference/numbers/submit-a-port-in-request /openapi.yaml post /numbers/port/{id}/submit Submit a signed draft (or resubmit a request that came back needing action) to the carrier: creates the porting order(s), attaches the letter of authorization and bill, presets the destination so numbers route to Flowyte the instant they activate, and confirms. Requires a paid plan. # Unassign a number Source: https://docs.flowyte.com/api-reference/numbers/unassign-a-number /openapi.yaml delete /numbers/{id}/assign Detaches a number from its agent without releasing it. You keep owning and paying for the number, and it returns to your available pool so you can reassign it any time. This is the safe, reversible alternative to releasing a number. # Approve or deny an OAuth2 authorization (mints the code) Source: https://docs.flowyte.com/api-reference/oauth2/approve-or-deny-an-oauth2-authorization-mints-the-code /openapi.yaml post /oauth/consent Called by the web consent screen AFTER a signed-in user approves (or denies). It re-validates the client_id, exact redirect_uri, requested scope (subset of the client's max ∩ ValidScopes), state and optional PKCE server-side — the query params carried through the browser are untrusted — and confirms the user is acting as the org being authorized (orgId == the token's active org). On approve it mints a single-use, 10-minute authorization code and returns the `redirectTo` URL (redirect_uri?code=…&state=…); on deny it returns redirect_uri?error=access_denied. HUMAN-ONLY: a service principal (API key or OAuth token) is rejected with 403 even if scoped. # Disconnect an OAuth client (revoke all the org's tokens) Source: https://docs.flowyte.com/api-reference/oauth2/disconnect-an-oauth-client-revoke-all-the-orgs-tokens /openapi.yaml delete /oauth/connections/{clientId} Revokes EVERY live access/refresh token family the org holds for the given client — the "disconnect Zapier" dashboard action. Idempotent; returns the number of token rows revoked. Use this when… a signed-in owner wants to fully disconnect a client from the org (revokes ALL of that client's tokens). To revoke just ONE specific token the client is holding, the client itself calls the RFC 7009 oauthRevoke endpoint. # OAuth2 authorization endpoint (RFC 6749) Source: https://docs.flowyte.com/api-reference/oauth2/oauth2-authorization-endpoint-rfc-6749 /openapi.yaml get /oauth/authorize Validates the authorization request and 302-redirects into the Flowyte consent screen. An unknown client_id or a redirect_uri that is not an EXACT registered match renders an error page and NEVER redirects (anti-open-redirect); any other error redirects back to the validated redirect_uri with error=…&state=…. `state` is required; PKCE is optional (S256 only). # OAuth2 token endpoint (RFC 6749 /) Source: https://docs.flowyte.com/api-reference/oauth2/oauth2-token-endpoint-rfc-6749- /openapi.yaml post /oauth/token Exchanges an authorization code for a token pair, or rotates a refresh token. The confidential client authenticates via client_secret_basic (HTTP Basic) OR client_secret_post (form fields). Authorization codes are single-use; refresh tokens rotate on every use and REUSE of a spent refresh token revokes the whole token family. Errors use the RFC 6749 shape. # OAuth2 token revocation (RFC 7009) Source: https://docs.flowyte.com/api-reference/oauth2/oauth2-token-revocation-rfc-7009 /openapi.yaml post /oauth/revoke A client-authenticated request revokes the presented token (and its whole rotation family). Always returns 200 for an authenticated client — even for an unknown token — so it never leaks token existence. A failed client authentication is 401 invalid_client. Use this when… the CLIENT itself wants to revoke ONE token it holds (standard RFC 7009). To disconnect a client entirely and revoke ALL of an org's tokens for it from the dashboard, an org owner uses oauthRevokeConnection instead. # Create a dashboard Source: https://docs.flowyte.com/api-reference/observe/create-a-dashboard /openapi.yaml post /dashboards Create a new user-owned dashboard. Body: `{ name, scope?, widgets[] }`. Every widget is validated against the metric registry: `metricId` must exist (422 with the offending id) and `viz` must be one of that metric's `allowedViz` (422; an empty `viz` defaults to the metric's `defaultViz`). `span` is clamped to the 2..20 grid and a widget without an `id` is assigned one. The board is owned by the acting user; a stock board can never be created via the API. # Create a scheduled report Source: https://docs.flowyte.com/api-reference/observe/create-a-scheduled-report /openapi.yaml post /report-schedules Create a schedule that emails a dashboard digest on a cadence. Body: `{ dashboardId, name, cadence?, hourLocal?, tz?, recipients[], formats?, delivery?, enabled? }`. `dashboardId` (must exist in the org — else 422) and `name` are required, and at least one valid recipient email is required. Defaults: cadence `daily`, hourLocal `7`, tz `UTC`, formats `["pdf","link"]`, delivery `email`, enabled `true`. `formats` is any subset of `link`/`csv`/`pdf`, all LIVE: the worker delivers the deep link, an optional CSV, and a PDF of the REAL dashboard charts (headless-chromium print of the read-only print page; a box without a renderer degrades to "no pdf" + a note, never a failed send). `delivery` is `email` in v1 — `slack` is a reserved value that currently 422s (no house Slack sender yet). The worker composes the digest via the same metric-registry compile path the live dashboard uses, so the emailed numbers equal the on-screen numbers. # Delete a dashboard Source: https://docs.flowyte.com/api-reference/observe/delete-a-dashboard /openapi.yaml delete /dashboards/{id} Permanently delete a dashboard. Stock boards are read-only → 409. 404 if the dashboard is absent/cross-org. This is a destructive operation. # Delete a scheduled report Source: https://docs.flowyte.com/api-reference/observe/delete-a-scheduled-report /openapi.yaml delete /report-schedules/{id} Permanently delete a schedule (stops the recurring digest). 404 if absent/cross-org. This is a destructive operation. # Get a conversation Source: https://docs.flowyte.com/api-reference/observe/get-a-conversation /openapi.yaml get /conversations/{id} Returns the summary for a single conversation, including its outcome and key details. # Get a conversation receipt Source: https://docs.flowyte.com/api-reference/observe/get-a-conversation-receipt /openapi.yaml get /conversations/{id}/receipt Returns the ordered list of events for a conversation: a step-by-step audit record of what the agent did, including tool calls and handoffs. # Get a conversation transcript Source: https://docs.flowyte.com/api-reference/observe/get-a-conversation-transcript /openapi.yaml get /conversations/{id}/transcript Returns the full transcript of a conversation as a list of turns. Works for both voice and chat. # Get a dashboard Source: https://docs.flowyte.com/api-reference/observe/get-a-dashboard /openapi.yaml get /dashboards/{id} The full dashboard document including its complete `widgets[]` array. # Get a scheduled report Source: https://docs.flowyte.com/api-reference/observe/get-a-scheduled-report /openapi.yaml get /report-schedules/{id} The full schedule document. 404 if absent/cross-org. # Get account analytics overview Source: https://docs.flowyte.com/api-reference/observe/get-account-analytics-overview /openapi.yaml get /analytics/overview Returns account-wide totals rolled up across every agent: total, answered, and missed calls, answer/containment/transfer rates, the voice versus chat split, active agent count, and spend for the period. Pass your average ticket value to estimate recovered revenue, and filter by date range and channel. # Get account trend over time Source: https://docs.flowyte.com/api-reference/observe/get-account-trend-over-time /openapi.yaml get /analytics/timeseries Returns per-day call volume and spend across your whole account, ordered oldest to newest. Filter by date range and channel. Only daily buckets are currently supported. # Get agent analytics Source: https://docs.flowyte.com/api-reference/observe/get-agent-analytics /openapi.yaml get /agents/{agentId}/analytics Returns post-call analytics for a single agent over a date range, including how many calls were answered, missed, contained, or transferred. Pass your average ticket value to estimate recovered revenue. Filter by channel (voice or chat). # Get agent trend over time Source: https://docs.flowyte.com/api-reference/observe/get-agent-trend-over-time /openapi.yaml get /agents/{agentId}/analytics/timeseries Returns per-day call volume and spend for a single agent, ordered oldest to newest. Filter by date range and channel. Only daily buckets are currently supported. # Get analytics per agent Source: https://docs.flowyte.com/api-reference/observe/get-analytics-per-agent /openapi.yaml get /analytics/by-agent Returns one rollup row per active agent with total calls, answer rate, containment rate, last call time, and spend. Agents with no calls in the selected window are still included with zeros. Filter by date range and channel. # List conversation topics Source: https://docs.flowyte.com/api-reference/observe/list-conversation-topics /openapi.yaml get /agents/{agentId}/topics Returns the most common topics callers discussed with this agent, with volume, trend, and sample questions. Topics are grouped automatically from conversation text, and results appear only once enough conversations have accumulated. # List conversations Source: https://docs.flowyte.com/api-reference/observe/list-conversations /openapi.yaml get /conversations Returns paginated conversation history, newest first, with each row's outcome, a one-line summary, and topic. Use the q parameter to full-text search across transcripts and summaries, and filter by date range, channel, outcome, and agent. # List conversations for a topic Source: https://docs.flowyte.com/api-reference/observe/list-conversations-for-a-topic /openapi.yaml get /agents/{agentId}/topics/{clusterId}/conversations Returns the paginated list of conversations grouped under a single topic, so you can drill into the calls behind it. # List knowledge gaps Source: https://docs.flowyte.com/api-reference/observe/list-knowledge-gaps /openapi.yaml get /agents/{agentId}/knowledge-gaps Returns questions callers asked that the agent could not answer, grouped by question and ordered by frequency, so you know what to add to its knowledge. Once enough data accumulates the gaps become ranked and can be filtered by status (open, in progress, covered, dismissed). # List saved dashboards Source: https://docs.flowyte.com/api-reference/observe/list-saved-dashboards /openapi.yaml get /dashboards The org's saved dashboards (stock boards first, then by name), each as a lightweight header — `id`, `name`, `ownerUserId`, `updatedAt`, `widgetCount`, `isStock` — without the widget bodies. `isStock` marks a product-shipped board that is read-only via the API (see updateDashboard/deleteDashboard). # List scheduled reports Source: https://docs.flowyte.com/api-reference/observe/list-scheduled-reports /openapi.yaml get /report-schedules The org's scheduled reports (newest first), each emailing a dashboard digest on a cadence. Optional `dashboardId` query filters to one dashboard's schedules. The list is small, so the full document is returned per row (not a header). # Query one metric over a filter window Source: https://docs.flowyte.com/api-reference/observe/query-one-metric-over-a-filter-window /openapi.yaml get /metrics/{id} Compute a single metric over a time window and a set of filter lenses, returning `{ series[], total, delta, n, unit, format }` (plus a `breakdown[]` for non-scalar metrics). `total` is the scalar value for count/ratio/avg metrics and `null` for distribution/percentile metrics (whose per-slice numbers live in `breakdown[]`). `n` is the honest sample count behind the number — a client should refuse to over-claim below a metric's minimum-n. `series[]` is the tz-aware per-bucket trend (empty for point-in-time backlog metrics). Filters (all optional): `from`/`to` bound the window as a half-open `[from, to)` range (RFC3339); `tz` is the IANA timezone used to bucket the series (default UTC), which fixes day-boundary drift; `bucket` sets the series granularity (day|week|month|hour, default day); `channel` restricts to one channel; `agent` and `queue` are INDEPENDENT lenses (both may be set — they AND together, one narrowing to a single agent, the other to a single queue); `minN` overrides the metric's low-volume suppression floor. Applying a lens the metric does not declare (see the catalog's `dimensions`) is a 400. `compare=true` additionally computes `delta` = current minus the immediately preceding equal-length window (scalar metrics with a bounded window only; otherwise `delta` is null). # The conversations behind a metric number (drill-down) Source: https://docs.flowyte.com/api-reference/observe/the-conversations-behind-a-metric-number-drill-down /openapi.yaml get /metrics/{id}/calls The conversation rows a metric number is computed from — the drill drawer behind a tile. It applies the SAME filter lenses AND the same counted-conversation predicate as `queryMetric`, PLUS the metric's own drill predicate (e.g. Transfers drills only transferred conversations), so the list always matches the number. Rows are shaped like `ConversationListItem`. Only metrics whose catalog `hasDrilldown` is true support this (others → 400). Pagination is KEYSET on `(createdAt, id)` DESC for stability under concurrent inserts: pass the returned `pagination.nextCursor` as `cursor` to fetch the next page. Accepts the same window/lens params as `queryMetric`, plus `key` to drill a categorical metric to ONE breakdown row (e.g. `topics?key=Billing` returns the conversations in that one category). Use this (the metric-anchored drill) when you want the exact conversations behind a registry tile's number. It differs from two siblings that return the same PaginatedConversationListItem shape: listConversations is the general searchable/filterable history (full-text `q`, sentiment); listTopicConversations is the per-agent topic-cluster drill (this endpoint's `?key=` is the registry-wide categorical equivalent). # The exact moments behind a knowledge gap (M4 drill) Source: https://docs.flowyte.com/api-reference/observe/the-exact-moments-behind-a-knowledge-gap-m4-drill /openapi.yaml get /agents/{agentId}/knowledge-gaps/{gapId}/occurrences The raw unanswered_queries rows folded into a clustered gap (knowledge_gap_id = gapId), newest-first — each carrying the verbatim failing query, when it happened, and the conversation it lives in. This is the data-correct gap drill: the occurrences are PRECISELY linked, so the UI opens the real transcripts behind the gap instead of a phrase search that never matches a caller's verbatim wording. 404 if the agent or gap is absent/cross-org. # The Observe metric catalog (self-describing registry) Source: https://docs.flowyte.com/api-reference/observe/the-observe-metric-catalog-self-describing-registry /openapi.yaml get /metrics/catalog The complete, self-describing metric registry — everything a client (or an LLM holding only an API key + this contract) needs to compose a dashboard WITHOUT any out-of-band knowledge. Each entry names the metric's `unit`/`format` (how to render the number), its `agg`/`grain` (how it is computed), the `dimensions` it accepts as filter lenses, and — critically — its `allowedViz` (the ONLY visualizations this metric may render as; `defaultViz` is the natural pick) and its `target` thresholds. Two flags gate composition: `requiresCapture` is non-empty ("coming soon") when the metric depends on a data-capture pass not yet emitting — such a metric is discoverable but should be shown disabled, not placed live; `hasDrilldown` says whether `GET /metrics/{id}/calls` will return the conversations behind the number. A widget's `viz` MUST be one of that metric's `allowedViz` or the dashboard write is rejected. This registry is the general, forward path for analytics — the composable engine behind Observe. The bespoke `/analytics/*` endpoints (getOrgAnalyticsOverview, getOrgAnalyticsByAgent, getOrgAnalyticsTimeseries, getAgentAnalytics) are pre-built, fixed money cards that compute similar numbers; reach for the registry when you want to pick and chart metrics yourself, and for the fixed cards when you just want the ready-made hero numbers. # The org's Settings→Activity trail (what any user — and Flowyte staff — did) Source: https://docs.flowyte.com/api-reference/observe/the-orgs-settings→activity-trail-what-any-user-—-and-flowyte-staff-—-did /openapi.yaml get /activity The customer-facing account ACTIVITY trail: a unified, newest-first timeline of what happened in the org — the org's own mutations (audit_logs) merged with the customer-visible Flowyte-STAFF access ledger (staff_access_log). Flowyte-staff access/action rows carry isFlowyteStaff=true (+ staffEmail, reason) so the UI can highlight them ("Flowyte staff accessed your account on — reason "). Org-scoped (RLS): a customer sees only their OWN org. VALUE-FREE — route/verb/status/reason only, never request bodies or PII. Use this for the human-readable Settings→Activity view (it includes Flowyte-staff access rows). For a raw, machine-oriented forensic export of API/key events filtered by date range and actor type, use listAuditLogs instead. # Update a dashboard (full-document replace) Source: https://docs.flowyte.com/api-reference/observe/update-a-dashboard-full-document-replace /openapi.yaml put /dashboards/{id} Replace a dashboard's `name`, `scope`, and `widgets`. This is a FULL-DOCUMENT replace — ALWAYS send the COMPLETE `widgets[]` array, not a delta. There is no per-widget endpoint: adding, moving, resizing, or removing a widget all mean sending the whole new widget list here. The server stores the array verbatim after the same registry validation as createDashboard (unknown metricId / illegal viz → 422; span clamped 2..20). Stock boards are read-only → 409 (customizing one creates a copy). 404 if the dashboard is absent/cross-org. # Update a knowledge gap Source: https://docs.flowyte.com/api-reference/observe/update-a-knowledge-gap /openapi.yaml patch /agents/{agentId}/knowledge-gaps/{gapId} Curate a knowledge gap by setting its status to open, in progress, or dismissed. The covered status is set automatically when the gap is resolved and cannot be set here. # Update a scheduled report Source: https://docs.flowyte.com/api-reference/observe/update-a-scheduled-report /openapi.yaml put /report-schedules/{id} Replace a schedule's mutable fields (same validation + defaults as create). The server never touches `lastSentAt` (the worker owns it). 404 if absent/cross-org. # Ask to join the org your email domain matches Source: https://docs.flowyte.com/api-reference/onboarding/ask-to-join-the-org-your-email-domain-matches /openapi.yaml post /onboarding/join-requests Records a request to join `orgId`, but only after re-deriving the caller's domain match server-side and requiring it to equal `orgId` (403 otherwise). The response status is always "pending" (a previously dismissed requester is never resurrected and cannot spam). Notifies the org's admins best-effort. # Discover the workspace that invited you, or the one your email domain belongs to Source: https://docs.flowyte.com/api-reference/onboarding/discover-the-workspace-that-invited-you-or-the-one-your-email-domain-belongs-to /openapi.yaml post /onboarding/discover Called by a just-signed-in account that has no workspace yet, BEFORE offering to create one. It returns two independent, server-derived answers about the caller's own VERIFIED identity — never about any address supplied in the request (the body is ignored): `invitation` — a PENDING organization invitation addressed to the caller's exact verified email. When present the client must offer to JOIN that workspace instead of creating a new one; auto-creating here strands an invited teammate in an empty workspace while the real invitation sits unaccepted. Null when nobody has invited this address, and also null (fail OPEN) whenever the identity provider cannot be reached, so an outage never blocks a signup. `match` — the org most people on the caller's email DOMAIN already belong to (the "your team may already be here" nudge). Null for a free-mail domain, no email, or no match. The two are independent: an invited teammate is usually the first person from their domain, so `match` is typically null exactly when `invitation` is set. Rate-limited per user. # Add a sync source to a contact list Source: https://docs.flowyte.com/api-reference/outbound/add-a-sync-source-to-a-contact-list /openapi.yaml post /outbound/lists/{id}/sources Bind a connected integration that can list records (Google Sheets, or a connector) to a contact list so the platform syncs it on a schedule — the POLL complement to outbound triggers. Each synced record runs through the SAME normalize / validate / dedupe / scrub pipeline as a CSV import. sourceKind is google_sheets or ; columnMapping maps canonical fields (phone required) to the source's fields exactly like an import; syncMode is snapshot (re-read every run, e.g. Sheets) or incremental (updatedAfter, e.g. GraphQL). A synced list is a reusable SEGMENT by default; set autoEnroll true with a targetCampaignId to also enroll newly-synced valid contacts into that RUNNING campaign. consentBasis is stamped on every synced contact. Use this for a scheduled POLL of a data source. For an event-PUSH automation (a provider fires the moment something happens), use `createOutboundTrigger` instead. # Auto-map a goal to an outbound trigger binding (the AI assistant) Source: https://docs.flowyte.com/api-reference/outbound/auto-map-a-goal-to-an-outbound-trigger-binding-the-ai-assistant /openapi.yaml post /agents/{agentId}/integrations/{kind}/triggers/auto Given a natural-language GOAL, an LLM proposes the event-leaf → intent-var mapping over the trigger's declared output fields; the proposal is VALIDATED by the compiler (with one self-correct retry) and created as a DISABLED trigger for review. 503 if no LLM is configured; 422 if a valid mapping couldn't be produced; 409 if not connected. Use this when you'd rather describe the goal than hand-map fields. To specify the event→field bindings explicitly, use `createOutboundTrigger` (POST .../triggers) instead. Either way the trigger lands DISABLED for your review before it can dial. # Bulk-import attested PEWC consent Source: https://docs.flowyte.com/api-reference/outbound/bulk-import-attested-pewc-consent /openapi.yaml post /outbound/consent-records/import Bulk-import existing ATTESTED consent (every row is imported_attested). Accepts a JSON body {records:[...]} or a multipart CSV (field 'file', header row with at least phone and disclosure_text columns). The import is ATOMIC and A3-validated: if ANY row has an invalid phone or a disclosure that does not authorize an artificial/prerecorded voice + autodialer, the WHOLE batch is rejected (422 with a per-row error) and NOTHING is written — defective consent can never be minted, even partially. Use this to load many attested rows atomically. To append a single web-form or attested proof, use `recordOutboundConsent` (POST /outbound/consent-records) instead. # Cancel a campaign Source: https://docs.flowyte.com/api-reference/outbound/cancel-a-campaign /openapi.yaml post /outbound/campaigns/{id}/cancel Cancels the campaign and its not-yet-dialed attempts; in-flight calls finish normally. This is terminal — unlike pauseOutboundCampaign, a cancelled campaign cannot be resumed. # Compile an outbound trigger (when {event} → call) onto an agent Source: https://docs.flowyte.com/api-reference/outbound/compile-an-outbound-trigger-when--→-call-onto-an-agent /openapi.yaml post /agents/{agentId}/integrations/{kind}/triggers The "open to author, frozen to call" route for OUTBOUND automations: bind a connected provider's declarative trigger event (see GET /integrations/{kind}/triggers) to the canonical outbound-call fields, and compile it to a frozen, DISABLED outbound_triggers row. The binding is validated — every event path must resolve to a declared LEAF (no object/array), a sensitive PII leaf may not be read aloud, recipient_phone must be bound — and the server-authored fields (agent, caller number, message_category informational, intent type, consent basis) are PINNED so the inbound payload can never choose them. The connection is resolved server-side from the org's connection of this kind. Omit `bindings` to use the trigger's curated default. 409 if not connected; 400 for any validation failure. Enable it (and stand up its delivery) via PATCH /outbound/triggers/{id}. Use this when you want to specify the event→field mapping yourself. To describe the goal in plain language and let an LLM propose the mapping, use `autoMapOutboundTrigger` (POST .../triggers/auto) instead. Both are event-PUSH automations; for a scheduled POLL of a data source, use `createOutboundListSource` instead. # Create an outbound campaign (draft) Source: https://docs.flowyte.com/api-reference/outbound/create-an-outbound-campaign-draft /openapi.yaml post /outbound/campaigns Create a draft campaign over a contact list. The agent, caller-ID number, and list must all belong to your organization. Created in status draft; call launch to begin dialing. This release dials message_category informational only. Use this (with `launchOutboundCampaign`) for paced bulk dialing over a contact list. For a single ad-hoc dial, use `enqueueOutboundCall` (POST /outbound/calls) instead. # Create an outbound contact list Source: https://docs.flowyte.com/api-reference/outbound/create-an-outbound-contact-list /openapi.yaml post /outbound/lists Create an empty contact list to import recipients into. A list is a reusable, scrubbed container; the dialing agent, caller-ID number, and channel are set on the CAMPAIGN, so any agentId/callerNumberId/channel sent here are optional defaults (a supplied callerNumberId is validated to belong to your organization). # Delete a sync source Source: https://docs.flowyte.com/api-reference/outbound/delete-a-sync-source /openapi.yaml delete /outbound/lists/{id}/sources/{sourceId} Removes the sync source. The list and its already-synced contacts are untouched. # Delete an outbound trigger Source: https://docs.flowyte.com/api-reference/outbound/delete-an-outbound-trigger /openapi.yaml delete /outbound/triggers/{id} Removes the trigger and tears down any provider-side subscription (orphan guard). # Enable or disable an outbound trigger Source: https://docs.flowyte.com/api-reference/outbound/enable-or-disable-an-outbound-trigger /openapi.yaml patch /outbound/triggers/{id} Enable or disable a trigger. ENABLING requires a standing consent attestation (consentAttested=true) and stands up its delivery (a no-op for a static webhook; a provider subscription for subscription-delivery). DISABLING tears down any provider subscription. Enabling makes the trigger originate a real call on every matched event. # Enqueue a single outbound call Source: https://docs.flowyte.com/api-reference/outbound/enqueue-a-single-outbound-call /openapi.yaml post /outbound/calls Enqueue ONE outbound voice call to a recipient. The call is not placed inline: this writes a normalized intent that the outbound worker schedules, compliance-gates (informational-only, callee quiet-hours, internal opt-out, and — for a consumer audience — a resolved consent basis), and dials when due; an answered call is billed per minute. message_category is always informational. A consumer audience requires a resolved consentBasis (pewc, express, or ebr); none is only valid for a business audience. Send an Idempotency-Key to make a retry safe; identical requests also collapse to one intent via a deterministic dedupe key. Returns 202 with the intent id. Use this for a single ad-hoc dial. To place many paced calls over a contact list, use a campaign instead (`createOutboundCampaign` + `launchOutboundCampaign`) — both dial through the same worker and compliance gate. # Get an outbound campaign and its attempt rollup Source: https://docs.flowyte.com/api-reference/outbound/get-an-outbound-campaign-and-its-attempt-rollup /openapi.yaml get /outbound/campaigns/{id} Returns the campaign with its per-status call-attempt counts, the live outbound-voice spend booked against its calls (spendCents, in credits), and the answering-machine-detection breakdown of its attempts (amdBreakdown). # Get an outbound contact list and its counts Source: https://docs.flowyte.com/api-reference/outbound/get-an-outbound-contact-list-and-its-counts /openapi.yaml get /outbound/lists/{id} Returns the list with its cumulative row counts (total / valid / invalid / duplicate / suppressed). # Get an outbound trigger Source: https://docs.flowyte.com/api-reference/outbound/get-an-outbound-trigger /openapi.yaml get /outbound/triggers/{id} Returns one compiled trigger with its frozen binding + state. # Import contacts into a list from a CSV (multipart) Source: https://docs.flowyte.com/api-reference/outbound/import-contacts-into-a-list-from-a-csv-multipart /openapi.yaml post /outbound/lists/{id}/import Upload a CSV and a column mapping to import recipients. Each row's phone is normalized to E.164 and validated; rows are deduplicated within the list and scrubbed against your opt-out list. Returns the per-import counts and the list's new cumulative state. The columnMapping form field is a JSON object mapping canonical fields (phone [required], name, email, timezone, zip, appointment_time, priority) to the source column header; every other column becomes a merge variable. Use this to upload a CSV file. To import rows you already have as JSON (a paste or CRM sync), use `addOutboundContacts` (POST .../contacts) instead — same pipeline, different input format. # Import contacts into a list from JSON rows Source: https://docs.flowyte.com/api-reference/outbound/import-contacts-into-a-list-from-json-rows /openapi.yaml post /outbound/lists/{id}/contacts Import recipients from JSON rows (the paste / CRM path) through the SAME normalize / validate / dedupe / scrub pipeline as the CSV import. columnMapping maps canonical fields to the keys present in each row object. Use this when you have the rows as JSON. To upload a CSV file instead, use `importOutboundList` (POST .../import) — same result, different input format. # Launch a campaign (begins dialing) Source: https://docs.flowyte.com/api-reference/outbound/launch-a-campaign-begins-dialing /openapi.yaml post /outbound/campaigns/{id}/launch Launch a draft campaign: validates that it is an informational campaign (a 422 is returned otherwise — marketing campaigns are not yet launchable), records the required consent attestation, schedules one call attempt per valid contact, and sets the campaign running. Requires consentAttested true. # List a contact list's sync runs Source: https://docs.flowyte.com/api-reference/outbound/list-a-contact-lists-sync-runs /openapi.yaml get /outbound/lists/{id}/sync-runs Returns the list's recent sync runs (newest first), optionally filtered to one source via ?sourceId. Each run carries its row tally (seen/added/updated/removed/invalid/suppressed/enrolled/errors) and status — the audit behind the list's last-synced / enrolled-count surface. # List a contact list's sync sources Source: https://docs.flowyte.com/api-reference/outbound/list-a-contact-lists-sync-sources /openapi.yaml get /outbound/lists/{id}/sources Returns the list's configured sync sources (newest first), each with its schedule and last-sync status. # List consent records (optionally for one number) Source: https://docs.flowyte.com/api-reference/outbound/list-consent-records-optionally-for-one-number /openapi.yaml get /outbound/consent-records Returns your organization's consent records, optionally filtered to one phone via ?phone= (normalized to E.164). REVOKED records ARE included — the ledger is append-only, so a revoked row is returned with revokedAt stamped. # List your outbound campaigns Source: https://docs.flowyte.com/api-reference/outbound/list-your-outbound-campaigns /openapi.yaml get /outbound/campaigns Returns your organization's campaigns, newest first, each with its per-status call-attempt rollup (counts). Capped at the 200 most recent campaigns. The live spend and answer-machine breakdown are returned only by getOutboundCampaign. # List your outbound contact lists Source: https://docs.flowyte.com/api-reference/outbound/list-your-outbound-contact-lists /openapi.yaml get /outbound/lists Returns your organization's contact lists, newest first, each with its cumulative row counts (total / valid / invalid / duplicate / suppressed) — the same shape as getOutboundList, as an array. Capped at the 200 most recent lists. # List your outbound triggers Source: https://docs.flowyte.com/api-reference/outbound/list-your-outbound-triggers /openapi.yaml get /outbound/triggers Returns your organization's compiled outbound triggers (newest first), optionally filtered to one agent via ?agentId. Each carries its frozen event→intent binding, delivery mode, pinned routing/consent, and enabled state. # List your standing inbound-webhook tokens Source: https://docs.flowyte.com/api-reference/outbound/list-your-standing-inbound-webhook-tokens /openapi.yaml get /outbound/webhook-tokens Returns your organization's live (non-revoked) inbound-webhook tokens — token-free (the plaintext is only ever returned at mint). # Marketing onboarding state + readiness checklist Source: https://docs.flowyte.com/api-reference/outbound/marketing-onboarding-state-+-readiness-checklist /openapi.yaml get /outbound/marketing/onboarding Returns your organization's marketing-onboarding state (seller identity + National-DNC SAN + status) and the GREEN/YELLOW/RED readiness checklist that gates the marketing un-gate. Org-level items (scrub subscription active, PEWC disclosure approved + stored, seller identity set) are always present; pass ?listId= to also get the per-list contact readiness (contactsNotScrubbed, contactsWithoutPewc) and the EXACT launchBlockedReason a marketing campaign over that list would hit — the same pure gate the launch endpoint runs. ready is true ONLY when a list was evaluated and its launchBlockedReason is empty. This is READ-ONLY state; it never activates anything. # Mint a standing inbound-webhook token Source: https://docs.flowyte.com/api-reference/outbound/mint-a-standing-inbound-webhook-token /openapi.yaml post /outbound/webhook-tokens Mint a non-expiring, REVOCABLE credential for a custom-delivery (Zapier/Make/no-signature) source's inbound URL (POST /webhooks/inbound/custom/{token}). The plaintext token and the inbound path are returned ONCE and never retrievable again — only the token's hash is stored. Pair it with a custom-provider trigger whose provider is the token's kind. (A provider app-level webhook, e.g. , authenticates by HMAC signature and needs no token.) # Pause a running campaign Source: https://docs.flowyte.com/api-reference/outbound/pause-a-running-campaign /openapi.yaml post /outbound/campaigns/{id}/pause Halts dialing without discarding scheduled attempts; call `resumeOutboundCampaign` to continue where it left off (use `cancelOutboundCampaign` to stop for good). # Record one PEWC consent proof Source: https://docs.flowyte.com/api-reference/outbound/record-one-pewc-consent-proof /openapi.yaml post /outbound/consent-records Append ONE prior-express-written-consent (PEWC) proof to the append-only consent ledger. signatureType is web_form or imported_attested. An imported_attested disclosure is HARD-BLOCKED (422) unless its verbatim text authorizes an artificial/prerecorded voice AND an autodialer/automated dialing system (the FCC 24-17 AI-voice requirement) — this is necessary, not sufficient (counsel owns the copy). A web_form record is refused (422) until the org's PEWC disclosure copy is counsel-approved (pewcDisclosureApproved) and the org's seller identity exists. Recording a consent proof does NOT, by itself, un-block any marketing dial — gating dials on consent is a later release. Use this to append ONE proof. To load many attested rows at once (all-or-nothing), use `importOutboundConsentRecords` (POST .../import) instead. # Resume a paused campaign Source: https://docs.flowyte.com/api-reference/outbound/resume-a-paused-campaign /openapi.yaml post /outbound/campaigns/{id}/resume Resumes dialing a paused campaign (the counterpart to pauseOutboundCampaign). # Revoke a consent record (append-only stamp) Source: https://docs.flowyte.com/api-reference/outbound/revoke-a-consent-record-append-only-stamp /openapi.yaml post /outbound/consent-records/{id}/revoke Stamp revokedAt on one consent record — APPEND-ONLY, never a delete. After a revoke the record is excluded from the dial-time consent lookup, so a later gate honors the revocation. A missing or already-revoked id is a 404. # Revoke an inbound-webhook token Source: https://docs.flowyte.com/api-reference/outbound/revoke-an-inbound-webhook-token /openapi.yaml delete /outbound/webhook-tokens/{id} Revokes a standing inbound-webhook token; it stops verifying at the gateway immediately. # Store the org's PEWC opt-in disclosure copy Source: https://docs.flowyte.com/api-reference/outbound/store-the-orgs-pewc-opt-in-disclosure-copy /openapi.yaml post /outbound/marketing/onboarding/disclosure Store the org's consumer-facing PEWC opt-in disclosure copy (rendered by the hosted opt-in form). STORING the copy is self-serve; APPROVING it is NOT — this NEVER flips pewcDisclosureApproved (the four 47 CFR 64.1200(f)(9) elements are a legal artifact counsel / an admin must finalize, A6). The org must have started onboarding (a submitted identity) first. # Submit seller identity + DNC SAN Source: https://docs.flowyte.com/api-reference/outbound/submit-seller-identity-+-dnc-san /openapi.yaml post /outbound/marketing/onboarding Save the org's seller identity (sellerName, legalEntity, ein, sellerAddress, sellerPhone, posture) + the National-DNC SAN + per-area-code entitlement, and move the registration draft → submitted. posture ('tenant_seller' default | 'flowyte_caller') is the liability-posture stored as DATA. THE SAFETY INVARIANT — this CAPTURES state, it does NOT ACTIVATE: it never sets status=active (the scrub-vendor/credential validation does that off-box) and never approves the PEWC disclosure. So a marketing campaign stays un-launchable until the founder completes onboarding. A raw SAN secret is never stored in a column. # Trigger a sync now Source: https://docs.flowyte.com/api-reference/outbound/trigger-a-sync-now /openapi.yaml post /outbound/lists/{id}/sources/{sourceId}/sync Force the source to sync on the worker's next tick (the worker owns the actual sync — it holds the integration transport). Returns 202 with the source id and status queued. # Update a sync source Source: https://docs.flowyte.com/api-reference/outbound/update-a-sync-source /openapi.yaml patch /outbound/lists/{id}/sources/{sourceId} Apply a partial change to a sync source (enable/disable, schedule, column mapping, source config, sync mode, consent basis, or auto-enroll + its target campaign). An omitted field is left unchanged. Enabling autoEnroll requires a targetCampaignId that dials this list. # Check a playbook for problems before it goes live Source: https://docs.flowyte.com/api-reference/playbooks/check-a-playbook-for-problems-before-it-goes-live /openapi.yaml post /agents/{agentId}/playbooks/{id}/verify Statically check the playbook's saved graph and report what would go wrong on a real call. It answers the questions a canvas cannot: can every route actually reach the end, can a caller get stuck on a question, and — the important one — could an action step fire with a value that nothing in the conversation ever filled in. Nothing is changed or executed: this reads the saved graph, so it is safe to call as often as you like. It requires only the read scope for that reason. Findings come back in two lists. `blockers` are defects that will break a call (no start step; a question with no way onward; an action step whose argument nothing can ever produce, which would send that field empty). `warnings` are worth reading but do not stop anything (a step nothing connects to; a value bound on some routes and not others; spoken text naming a variable the playbook never produces, which the caller would hear read out literally). `graphStats` summarises the shape of what was checked. The same checks run automatically as part of getPrePublishReport, so a blocker here also appears there. Run this after editing a graph with putPlaybookGraph and fix the blockers before publishing. # Create a playbook Source: https://docs.flowyte.com/api-reference/playbooks/create-a-playbook /openapi.yaml post /agents/{agentId}/playbooks Creates a new playbook for the agent with a name and description, plus optional input and output schemas. After creating it, define its steps with the playbook graph endpoint. # Delete a playbook Source: https://docs.flowyte.com/api-reference/playbooks/delete-a-playbook /openapi.yaml delete /agents/{agentId}/playbooks/{id} Permanently deletes a playbook and its step graph from the agent. # Get a playbook Source: https://docs.flowyte.com/api-reference/playbooks/get-a-playbook /openapi.yaml get /agents/{agentId}/playbooks/{id} Returns a single playbook's details, including its name, description, and input and output schemas. # Get a playbook's steps Source: https://docs.flowyte.com/api-reference/playbooks/get-a-playbooks-steps /openapi.yaml get /agents/{agentId}/playbooks/{id}/graph Returns the playbook's step graph: the nodes and the connections between them that define the procedure the agent runs. # List playbooks Source: https://docs.flowyte.com/api-reference/playbooks/list-playbooks /openapi.yaml get /agents/{agentId}/playbooks Returns a paginated list of the playbooks defined for an agent. A playbook is a reusable, step-by-step procedure the agent can follow during a conversation. # Replace a playbook's steps Source: https://docs.flowyte.com/api-reference/playbooks/replace-a-playbooks-steps /openapi.yaml put /agents/{agentId}/playbooks/{id}/graph Replaces the playbook's entire step graph with the nodes and connections you provide. This overwrites the existing graph, so send the complete set of steps. Publish the agent for the change to take effect on live conversations. # Update a playbook Source: https://docs.flowyte.com/api-reference/playbooks/update-a-playbook /openapi.yaml patch /agents/{agentId}/playbooks/{id} Updates an existing playbook's name, description, and input or output schemas. # Create a publishable key Source: https://docs.flowyte.com/api-reference/publishablekeys/create-a-publishable-key /openapi.yaml post /agents/{agentId}/publishable-keys Creates a publishable key for an agent and returns the full key. Publishable keys are safe to use in the browser, so the value can be retrieved again later. # List publishable keys Source: https://docs.flowyte.com/api-reference/publishablekeys/list-publishable-keys /openapi.yaml get /agents/{agentId}/publishable-keys Returns the paginated publishable keys for an agent. Publishable keys are safe to embed in the browser and authorize the chat widget. # Revoke a publishable key Source: https://docs.flowyte.com/api-reference/publishablekeys/revoke-a-publishable-key /openapi.yaml delete /agents/{agentId}/publishable-keys/{id} Revokes a publishable key immediately. Any widget still using it will stop working. # Rotate a publishable key Source: https://docs.flowyte.com/api-reference/publishablekeys/rotate-a-publishable-key /openapi.yaml post /agents/{agentId}/publishable-keys/{id}/rotate Issues a new value for a publishable key while keeping its agent and allowed origins. The old value keeps working during a short grace window so you can swap it in without downtime. # Update a publishable key Source: https://docs.flowyte.com/api-reference/publishablekeys/update-a-publishable-key /openapi.yaml patch /agents/{agentId}/publishable-keys/{id} Updates a publishable key, such as its label or its list of allowed origins. # Bulk-upsert caller-context records Source: https://docs.flowyte.com/api-reference/records/bulk-upsert-caller-context-records /openapi.yaml post /records/bulk Upserts up to 500 records in one request (a backfill batch), each through the same merge-and-learn pipeline as a single upsert. Returns per-item results so a partial failure doesn't fail the whole batch. Costs one ingest token per batch (429 with Retry-After when exhausted). # Delete a caller-context record Source: https://docs.flowyte.com/api-reference/records/delete-a-caller-context-record /openapi.yaml delete /records/{objectType}/{externalId} Soft-deletes (tombstones) a record so the deletion propagates — the agent stops greeting a removed contact. Lookups exclude it; a later upsert restores it. # Erase all caller-context records Source: https://docs.flowyte.com/api-reference/records/erase-all-caller-context-records /openapi.yaml post /records/purge Hard-deletes every caller-context record for your organization — the GDPR erasure backstop. Requires `records:write` plus an explicit confirmation in the body. # Get a caller-context record Source: https://docs.flowyte.com/api-reference/records/get-a-caller-context-record /openapi.yaml get /records/{objectType}/{externalId} Returns one caller-context record by object type and ID. # Get a record type's fields Source: https://docs.flowyte.com/api-reference/records/get-a-record-types-fields /openapi.yaml get /record-types/{type}/fields Returns a type's learned field definitions — the exact shape used to render structured inputs for records of that type. # List caller-context record types Source: https://docs.flowyte.com/api-reference/records/list-caller-context-record-types /openapi.yaml get /record-types Returns your organization's registry of synced object types and their learned field definitions. Each type carries its live record count and last sync time. # List caller-context records Source: https://docs.flowyte.com/api-reference/records/list-caller-context-records /openapi.yaml get /records Paged list of live records of an object type, with an optional case-insensitive `q` filter over external ID, phone, email, or the record's fields. Deleted (tombstoned) records are excluded. # Update a record type Source: https://docs.flowyte.com/api-reference/records/update-a-record-type /openapi.yaml patch /record-types/{type} Applies label and PII-class overrides to a type's existing fields (by name), and optionally its display name. The field set and kinds are learned from writes and can't be changed here. # Upsert a caller-context record Source: https://docs.flowyte.com/api-reference/records/upsert-a-caller-context-record /openapi.yaml put /records Creates or updates one external record, keyed on (object type, source, external ID). Incoming fields are shallow-merged — they win and are timestamped, while omitted fields are preserved — and the object type's field registry is learned from the write. A write also restores a previously deleted record. `externalId` is required. Rate-limited per organization (429 with Retry-After). # Create a skill Source: https://docs.flowyte.com/api-reference/skills/create-a-skill /openapi.yaml post /agents/{agentId}/skills Adds a new skill to an agent so it can perform an action, such as sending an email or booking an appointment, during conversations. Publish the agent for the new skill to take effect on live calls and chats. # Delete a skill Source: https://docs.flowyte.com/api-reference/skills/delete-a-skill /openapi.yaml delete /agents/{agentId}/skills/{id} Removes a skill from an agent. Publish the agent for the removal to take effect on live calls and chats. # Geocode a location roster Source: https://docs.flowyte.com/api-reference/skills/geocode-a-location-roster /openapi.yaml post /agents/{agentId}/geocode-roster Converts a batch of locations (label, address, phone) into map coordinates for use in a find-nearest-location skill, so the agent can quickly identify the closest location during a call. This endpoint does not save anything itself, so store the returned coordinates in your skill's configuration. Large lists must be sent in batches, and any address that can't be resolved is returned flagged so you can correct it. # Get a skill Source: https://docs.flowyte.com/api-reference/skills/get-a-skill /openapi.yaml get /agents/{agentId}/skills/{id} Returns the full configuration of a single skill on an agent. # List an agent's skills Source: https://docs.flowyte.com/api-reference/skills/list-an-agents-skills /openapi.yaml get /agents/{agentId}/skills Returns a paginated list of the skills configured on an agent. Skills are the actions an agent can take during a conversation. # List available skill types Source: https://docs.flowyte.com/api-reference/skills/list-available-skill-types /openapi.yaml get /skill-types A skill type is a category of capability a skill can use, such as sending an email, looking up an order, or transferring a call. This returns the full catalog of skill types you can choose from when creating a skill. # Update a skill Source: https://docs.flowyte.com/api-reference/skills/update-a-skill /openapi.yaml patch /agents/{agentId}/skills/{id} Changes the configuration of an existing skill on an agent. Publish the agent for the changes to apply to live calls and chats. # Enable or disable an owned number for SMS (assign to the campaign) Source: https://docs.flowyte.com/api-reference/sms/enable-or-disable-an-owned-number-for-sms-assign-to-the-campaign /openapi.yaml patch /sms/numbers/{e164} Turn texting on or off for one owned number by adding it to (or removing it from) the org's 10DLC campaign. After enabling, the number's `smsStatus` goes `pending` while the carriers add it (up to ~1h) and then flips to `active` — it can't send reliably until active. Requires an active org registration first (see submitSMSRegistration). # Export SMS contacts + consent as CSV (TCPA proof) Source: https://docs.flowyte.com/api-reference/sms/export-sms-contacts-+-consent-as-csv-tcpa-proof /openapi.yaml get /sms/contacts/export Streams the full filtered contact set (same filters as the list, no paging) as CSV — the 4-year TCPA consent-proof export (phone, consent tier, opt-out, first/last contact, latest consent basis/source/timestamp). The same consent + optedOut filters apply, so `consent=conversational,express&optedOut=false` exports the outbound-ready opted-in list. Use this when… you need the whole set as a file for records/proof (no paging). To read contacts programmatically page-by-page as JSON, use listSMSContacts instead. # Get the org's SMS (10DLC) registration, numbers, and status Source: https://docs.flowyte.com/api-reference/sms/get-the-orgs-sms-10dlc-registration-numbers-and-status /openapi.yaml get /sms Read the SMS Hub state in one call: the org's 10DLC registration (draft fields + carrier status), each owned number's SMS-enablement state, the policy-page URL, and the activation + monthly fees. This is the surface a client renders the whole SMS setup from. # List opted-out (do-not-text) contacts Source: https://docs.flowyte.com/api-reference/sms/list-opted-out-do-not-text-contacts /openapi.yaml get /sms/suppressions List the org-wide opt-out (suppression) list — the numbers you must NOT text — with paging and phone-substring search. Use this when… you specifically want the opt-out subset (and to add/remove entries via addSMSSuppression / removeSMSSuppression). To see ALL contacts with their consent tier (opted-out or not), use listSMSContacts instead; suppressions are just the opted-out slice of that fuller consent dashboard. # List SMS contacts with consent status (Consent & contacts) Source: https://docs.flowyte.com/api-reference/sms/list-sms-contacts-with-consent-status-consent-&-contacts /openapi.yaml get /sms/contacts The per-contact consent dashboard: each contact's coarse consent tier (none | conversational | express), org-wide opt-out status, first/last contact times, and the latest consent-ledger record's basis/source/timestamp. Searchable by phone substring and paged. Backs the 4-year TCPA consent-proof surface. Filter by consent tier and/or opt-out status to build a campaign list: pass `consent=conversational,express&optedOut=false` to get the outbound-ready opted-in list (every contact with at-least conversational consent that has NOT opted out). # Manually record an opt-out (do-not-text) Source: https://docs.flowyte.com/api-reference/sms/manually-record-an-opt-out-do-not-text /openapi.yaml post /sms/suppressions Records an opt-out that arrived outside SMS (a phone call, an email) so the business stops texting that number. Opt-outs must be honored regardless of how they arrive. Returns 200 (not 201) — an opt-out is an idempotent record, not a created resource. # Re-enable a number (record new consent) — gated Source: https://docs.flowyte.com/api-reference/sms/re-enable-a-number-record-new-consent-—-gated /openapi.yaml delete /sms/suppressions/{e164} Clears an opt-out. This is NOT a plain delete: re-texting someone who opted out is a compliance risk, so the caller must attest fresh, documented consent (consentConfirmed=true). The normal re-opt-in is the customer texting START. # Reconcile the registration status from the carrier (non-billable) Source: https://docs.flowyte.com/api-reference/sms/reconcile-the-registration-status-from-the-carrier-non-billable /openapi.yaml post /sms/refresh Pulls the brand/campaign status directly from the provider and updates the local status. Used as a manual "refresh" when a status webhook is delayed; surfaces a failed campaign's rejection reasons. Does not charge. # Run the AI compliance review on the saved draft (free, no charge) Source: https://docs.flowyte.com/api-reference/sms/run-the-ai-compliance-review-on-the-saved-draft-free-no-charge /openapi.yaml post /sms/review Runs the deterministic pre-flight checks plus an AI compliance review and returns a green/yellow/red verdict with specific fixes. Does NOT submit or charge. The draft must be saved first (saveSMSRegistration); the review reflects exactly what would be submitted. # Save the 10DLC registration draft (auto-generates compliant campaign text) Source: https://docs.flowyte.com/api-reference/sms/save-the-10dlc-registration-draft-auto-generates-compliant-campaign-text /openapi.yaml put /sms Save (upsert) the org's 10DLC registration draft — brand details, use case, and opt-in info — and have Flowyte auto-generate compliant campaign text and sample messages from it. This does NOT submit to the carrier or charge; it just persists the draft. Run reviewSMSRegistration next to get a compliance verdict, then submitSMSRegistration to go live. # Submit the 10DLC registration (billable — debits the activation fee) Source: https://docs.flowyte.com/api-reference/sms/submit-the-10dlc-registration-billable-—-debits-the-activation-fee /openapi.yaml post /sms/submit Re-runs the full compliance review and refuses unless GREEN, then debits the SMS activation (2,500 credits / $25) and registers the brand + campaign with the carrier registry. Status then updates asynchronously via the provider webhook. Activating SMS also starts a recurring 10DLC MANAGEMENT fee of 500 credits ($5) per month while the registration stays active — the first month is included in the activation charge, so the first monthly fee is debited 30 days after activation. # Submit a support request Source: https://docs.flowyte.com/api-reference/support/submit-a-support-request /openapi.yaml post /support/requests Submits an in-app help request — a category and a message. Your signed-in email, name, and organization are attached from your verified identity (never typed), and the request is emailed to support with you set as reply-to. Available to any authenticated caller. # Approve a join request (send an invitation) Source: https://docs.flowyte.com/api-reference/team/approve-a-join-request-send-an-invitation /openapi.yaml post /team/join-requests/{id}/approve Sends an invitation (same dedupe as a direct invite) and marks the request invited. # Change a member's role Source: https://docs.flowyte.com/api-reference/team/change-a-members-role /openapi.yaml patch /team/members/{userId} Guarded per the Role model: only an OWNER may grant owner, or change/remove another owner; nobody may change the org's LAST owner (409 last_owner) or their OWN role (409 self_change). Syncs the platform (owner maps to org:admin) and profiles.role in the same request. # Dismiss a join request Source: https://docs.flowyte.com/api-reference/team/dismiss-a-join-request /openapi.yaml post /team/join-requests/{id}/dismiss # Invite a teammate (no phone seat) Source: https://docs.flowyte.com/api-reference/team/invite-a-teammate-no-phone-seat /openapi.yaml post /team/invitations Sends a the platform org invitation with the role PINNED (admin|staff — default staff), redirecting the invitee to the BUILDER. NO softphone seat is provisioned and NO seat marker is set, so a general teammate never consumes a phone license. A staff invite is allowed even without phone entitlement. # List pending join requests Source: https://docs.flowyte.com/api-reference/team/list-pending-join-requests /openapi.yaml get /team/join-requests The pending domain-discovery requests — people who signed up on the org's email domain and asked to join. # List team members, pending invitations, and the join-request count Source: https://docs.flowyte.com/api-reference/team/list-team-members-pending-invitations-and-the-join-request-count /openapi.yaml get /team/members The org's members merged from the the platform roster and the DB-authoritative profiles.role (the DB role WINS — owner has no the platform equivalent), each with a seatState when the org is Phone-entitled, plus the pending the platform invitations and the count of pending domain-discovery join requests. # Remove a member from the workspace Source: https://docs.flowyte.com/api-reference/team/remove-a-member-from-the-workspace /openapi.yaml delete /team/members/{userId} Removes the the platform membership + the profiles row and suspends any softphone seat, in-request. Guarded: an admin may remove admins/staff but NOT an owner; nobody may remove themselves (409 self_change) or the org's last owner (409 last_owner). # Resend a pending invitation Source: https://docs.flowyte.com/api-reference/team/resend-a-pending-invitation /openapi.yaml post /team/invitations/{id}/resend Revokes the pending invitation and re-issues an identical one. 404 if it is not pending. # Revoke a pending invitation Source: https://docs.flowyte.com/api-reference/team/revoke-a-pending-invitation /openapi.yaml delete /team/invitations/{id} # Get a probe run result Source: https://docs.flowyte.com/api-reference/test/get-a-probe-run-result /openapi.yaml get /agents/{agentId}/probe/runs/{runId} Returns the current status and result of a probe run by its id. # Simulate a conversation Source: https://docs.flowyte.com/api-reference/test/simulate-a-conversation /openapi.yaml post /agents/{agentId}/simulate Runs a text or voice simulation against the agent and streams the conversation back as server-sent events. Set draftMode to test unpublished changes without billing. Each event is a turn or tool-call frame, and the stream ends with a done or error event. # Start a probe run Source: https://docs.flowyte.com/api-reference/test/start-a-probe-run /openapi.yaml post /agents/{agentId}/probe/run Kicks off a probe run against the agent for a given test scenario, in either loopback or phone mode. Returns a run id you can poll or stream for results. # Start an in-browser voice test Source: https://docs.flowyte.com/api-reference/test/start-an-in-browser-voice-test /openapi.yaml post /agents/{agentId}/talk-token Returns the credentials needed to start a real-time voice test session in the browser. Set draftMode to true to test the current draft without billing; when false (the default) it uses the latest published version and returns 409 if the agent has never been published. # Stream probe run progress Source: https://docs.flowyte.com/api-reference/test/stream-probe-run-progress /openapi.yaml get /agents/{agentId}/probe/runs/{runId}/stream Streams a probe run's progress in real time as server-sent events until the run finishes. # Upload a file Source: https://docs.flowyte.com/api-reference/uploads/upload-a-file /openapi.yaml post /uploads Upload a file (max 25 MB) and get back a file_id you can pass to a knowledge file source, the create-from-document flow, or a skill file input. # Declare or annotate a variable Source: https://docs.flowyte.com/api-reference/variables/declare-or-annotate-a-variable /openapi.yaml post /agents/{agentId}/variables Optionally attach a note, a type hint, or a manual declaration to a variable. This is documentation only — it carries no runtime meaning and never gates a call. Annotating a name that's already derived returns 200 (not a conflict). Names are case-sensitive and must match `^[a-zA-Z_][a-zA-Z0-9_]*$`; the reserved caller variables (`caller_info`, `caller_number`, `caller_name`, `verified`) can't be declared. # Delete a variable annotation Source: https://docs.flowyte.com/api-reference/variables/delete-a-variable-annotation /openapi.yaml delete /agents/{agentId}/variables/{name} Removes only the annotation. If the name is still produced or used by the agent, it reappears as a derived variable on the next read. Deleting a name that has no annotation does nothing. # Edit a variable annotation Source: https://docs.flowyte.com/api-reference/variables/edit-a-variable-annotation /openapi.yaml patch /agents/{agentId}/variables/{name} Edits the note or type hint on an existing variable annotation (only the fields you send change). Returns 404 if the name has no annotation. The annotation carries no runtime meaning. # List interaction variables Source: https://docs.flowyte.com/api-reference/variables/list-interaction-variables /openapi.yaml get /agents/{agentId}/variables Returns the agent's interaction variables — the values that flow through a call (collected slots, skill outputs, and `{placeholder}` references). The list is derived by scanning the agent's enabled playbooks and skills, then merged with any notes or type hints you've added. It's a read-only view of how variables flow; it never validates references or affects publishing or runtime. Sorted by name. # Get voice filter counts Source: https://docs.flowyte.com/api-reference/voices/get-voice-filter-counts /openapi.yaml get /voices/facets Returns grouped counts for each voice filter using the same filters as the voice list, so a picker can show a live count next to each option (for example 'Female: 142') as the user narrows their choice. # List the voice catalog Source: https://docs.flowyte.com/api-reference/voices/list-the-voice-catalog /openapi.yaml get /voices Returns the voice catalog, optionally narrowed by facets such as language, accent, gender, age, tone, and use case (all combined together). The catalog is returned as a flat list rather than paginated. Pair it with the facets endpoint for live counts and the search endpoint for natural-language lookup. # Search voices by description Source: https://docs.flowyte.com/api-reference/voices/search-voices-by-description /openapi.yaml post /voices/search Turns a plain-language phrase like 'calm older british woman for support' into structured filters, runs the search, and returns both the resolved filters and a short ranked shortlist of voices to audition. # Create a webhook endpoint Source: https://docs.flowyte.com/api-reference/webhooks/create-a-webhook-endpoint /openapi.yaml post /webhooks Registers a URL to receive event notifications and returns a signing secret used to verify that deliveries came from us. The secret is shown only once in this response, so store it now; you cannot retrieve it again later. # Delete a webhook endpoint Source: https://docs.flowyte.com/api-reference/webhooks/delete-a-webhook-endpoint /openapi.yaml delete /webhooks/{id} Permanently removes a webhook endpoint so it will no longer receive event deliveries. # Get a webhook endpoint Source: https://docs.flowyte.com/api-reference/webhooks/get-a-webhook-endpoint /openapi.yaml get /webhooks/{id} Returns the details of a single webhook endpoint, including its URL, subscribed events, and status. The signing secret is not included. # List webhook deliveries Source: https://docs.flowyte.com/api-reference/webhooks/list-webhook-deliveries /openapi.yaml get /webhooks/{id}/deliveries Returns the recent delivery attempts for an endpoint, including each event type, status, attempt count, and the next scheduled retry. Use this to debug failures and confirm events are reaching your URL. Results are paginated. # List webhook endpoints Source: https://docs.flowyte.com/api-reference/webhooks/list-webhook-endpoints /openapi.yaml get /webhooks Returns your configured webhook endpoints, with the events each is subscribed to and its current status. Results are paginated. # Send a test delivery Source: https://docs.flowyte.com/api-reference/webhooks/send-a-test-delivery /openapi.yaml post /webhooks/{id}/test Sends a sample event to the endpoint so you can confirm your receiver is reachable and verifies signatures correctly. Returns the resulting delivery record, whose status updates as the attempt completes. # Update a webhook endpoint Source: https://docs.flowyte.com/api-reference/webhooks/update-a-webhook-endpoint /openapi.yaml patch /webhooks/{id} Changes a webhook endpoint's URL, subscribed events, or status. Set the status to disabled to pause deliveries without deleting the endpoint. # Get the widget embed snippet Source: https://docs.flowyte.com/api-reference/widget/get-the-widget-embed-snippet /openapi.yaml get /agents/{agentId}/widget/embed Returns the HTML snippet for embedding the chat widget on your site, along with a publishable key. Optionally specify which publishable key to include. # Get widget config Source: https://docs.flowyte.com/api-reference/widget/get-widget-config /openapi.yaml get /agents/{agentId}/widget Returns the chat widget configuration for an agent, including its appearance, copy, and behavior settings. # Replace widget config Source: https://docs.flowyte.com/api-reference/widget/replace-widget-config /openapi.yaml put /agents/{agentId}/widget Replaces an agent's chat widget configuration with the supplied settings and returns the saved result. # Chat Source: https://docs.flowyte.com/channels/chat Talk to an agent over text — sessions, streaming, and an OpenAI-compatible endpoint. The same published agent that answers the phone also answers over text. There are two ways to drive a chat from your backend: the **session API** (durable conversations you create, message, and end) and an **OpenAI-compatible completions endpoint** (a drop-in for any client that already speaks the OpenAI chat shape). ## Sessions A session is a server-side conversation tied to one agent. Create it, post messages, and end it when done. History is reconstructed for you, so you can fetch the full transcript any time. Posting a message with `stream: true` returns Server-Sent Events (SSE) — token deltas, tool calls, and knowledge retrievals arrive as they happen, terminating with an `event: done` (or `event: error`) frame. With `stream: false` you get the assembled messages in one JSON response. ```bash theme={null} # Create a session, then send a streaming message curl -X POST https://builder.flowyte.com/api/v1/chat/sessions \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{"agentId": "AGENT_ID"}' curl -N -X POST https://builder.flowyte.com/api/v1/chat/sessions/SESSION_ID/messages \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{"content": "Do you deliver to 80202?", "stream": true}' ``` ## OpenAI-compatible completions `POST /chat/completions` accepts the OpenAI request shape — set `model` to the **agent's id**. Point an existing OpenAI SDK at the base URL `https://builder.flowyte.com/api/v1` and it works with only a base-URL swap. ```bash curl theme={null} curl -X POST https://builder.flowyte.com/api/v1/chat/completions \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "model": "AGENT_ID", "messages": [{"role": "user", "content": "What are your hours?"}], "stream": true }' ``` ```python Python theme={null} from openai import OpenAI client = OpenAI( base_url="https://builder.flowyte.com/api/v1", api_key="flowyte_sk_…", ) stream = client.chat.completions.create( model="AGENT_ID", # the agent id is the model messages=[{"role": "user", "content": "What are your hours?"}], stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="") ``` ```javascript Node theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://builder.flowyte.com/api/v1", apiKey: "flowyte_sk_…", }); const stream = await client.chat.completions.create({ model: "AGENT_ID", // the agent id is the model messages: [{ role: "user", content: "What are your hours?" }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); } ``` When `stream: true`, this endpoint emits OpenAI-style SSE chunks (`data: {choices:[{delta:…}]}`) ending with `data: [DONE]`. ## In the API | Action | Endpoint | Scope | | ---------------------------------- | ----------------------------------- | ------------ | | Create a session | `POST /chat/sessions` | `chat:write` | | Get a session | `GET /chat/sessions/{id}` | `chat:read` | | List messages | `GET /chat/sessions/{id}/messages` | `chat:read` | | Post a message (SSE when `stream`) | `POST /chat/sessions/{id}/messages` | `chat:write` | | End a session | `POST /chat/sessions/{id}/end` | `chat:write` | | OpenAI-compatible completion | `POST /chat/completions` | `chat:write` | These endpoints use your **secret key**. To put chat in a browser without exposing a secret, use a publishable key and the [Embeddable Widget](/channels/widget). # DTMF / Keypad Source: https://docs.flowyte.com/channels/dtmf Collect keypad digits from a caller during a voice call. On a voice call, callers can answer with their **keypad** as well as their voice. The agent recognizes the touch-tones (DTMF) a caller presses and treats those digits as input — the same way it treats spoken words. This is the right tool when a value is awkward to say out loud or must be exact: an account or order number, a numeric menu choice, a PIN, or a yes/no confirmation ("press 1 to confirm"). ## When to reach for the keypad * **Exact numbers** — account, order, member, or case IDs where a misheard digit is costly. * **Quick choices** — "press 1 for billing, 2 for support" style menus. * **Confirmations** — "press 1 to confirm, 2 to start over." * **Sensitive entry** — values a caller would rather key in than speak aloud. ## How it works Keypad input is a property of the **voice** channel — there is no separate setup to turn it on. During a call the agent listens for both speech and digits, so a caller can press a number when the agent asks for one. Those digits flow into the conversation just like spoken answers: when a skill needs a value (for example, an order number for a lookup, or a destination choice for a transfer), the caller can speak it *or* key it in, and the agent uses whichever it receives. Because digits become ordinary collected values, you don't build a rigid phone-tree. You describe what the agent should collect — in the agent's prompt and in each skill's parameters — and the agent decides when keypad entry makes sense. Phrase your prompts to invite it explicitly when precision matters: *"Read me your order number, or type it on your keypad."* Callers default to the keypad for long digit strings, which improves accuracy. ## Related configuration | What it controls | Where | | ---------------------------------------------------------------- | ------------------------------------------------ | | Which values a skill collects from the caller (spoken or keypad) | `POST /agents/{agentId}/skills` (`skills:write`) | | Idle / no-response handling while waiting for input | `CallControl` on the agent (`agents:write`) | | Routing a keyed-in choice to a person | Transfer rules — see [Voice](/channels/voice) | | What actually happened on a call | `GET /agents/{agentId}/calls` (`calls:read`) | Keypad entry is only available on the [Voice (PSTN)](/channels/voice) channel — text [Chat](/channels/chat) has no tones to press. Build the same data-collection step into your skill parameters and it works across both: spoken in chat, spoken-or-keyed on a call. # SMS (10DLC) Source: https://docs.flowyte.com/channels/sms Text from your phone numbers — set up in the dashboard once your brand and campaign are registered. SMS lets your business send and receive text messages from numbers you own. In the US, texting from a standard 10-digit number requires **A2P 10DLC registration**: every organization registers its own **brand** and **campaign** with the carrier registry before any message can be sent. SMS registration is **handled in the dashboard**, not the API. Carrier registration (TCR) is compliance-critical — the guided flow validates your details, runs a free compliance review, and walks you through approval so a submission can't be wrong before you pay the activation fee. Sending stays **gated** until your campaign is approved. ## Set it up 1. Open **SMS** in the dashboard. 2. Enter your business details — the platform auto-generates compliant campaign text (sample messages and opt-in language). 3. Run the **free compliance review** (green / yellow / red, with specific fixes). 4. **Submit** once it's green. This pays the one-time activation fee and registers your brand and campaign with the carrier registry; status then updates as the registry approves it. 5. Enable SMS on the numbers you want to text from. ## After approval Once your campaign is **approved**, your agent can send and receive texts on its enabled numbers. Opt-outs (STOP) are honored automatically, and the dashboard keeps a do-not-text suppression list — re-enabling a number requires fresh, documented consent. Plan the registration step before launch — approval is controlled by the carrier registry and can take time, so start it early. # Voice (PSTN) Source: https://docs.flowyte.com/channels/voice Put an agent on a real phone number and answer inbound calls. Voice is the flagship channel: a published agent answers a real phone number over the public telephone network (PSTN), greets the caller, answers from its knowledge, runs its skills, and hands off to a person when needed. There is nothing to host — you buy a number, point it at an agent, and inbound calls just work. ## Get a number, point it at an agent Browse purchasable numbers by area code, city, or vanity suffix. Filter by capability (require `voice`, and `sms` if you plan to text from it later). Hold a number for \~30 minutes so it can't be sniped while you confirm, then purchase it. Purchase debits your prepaid wallet. Already own a number elsewhere? **Import** it instead — no wallet charge. Assigning sets the number's default agent. Inbound calls to that number are routed to this deployment's voice connection and reach your agent. The agent must be **published** to take live calls — phone and chat always serve the last published version, never your draft. See [Draft vs Published](/get-started/draft-vs-published). ## On the call The agent opens with its configured greeting (spoken in the agent's primary language), listens with real-time turn detection, and responds with the platform's voice engine. Knowledge lookups, skills, and playbooks all run live. Per-agent **call control** governs the experience: a maximum call length, "are you still there?" idle reminders, and what to do on silence. ## Transfer and handoff When a caller needs a person, the agent transfers the call. You configure a default handoff destination plus a **transfer-by-context** table — plain-language rules that route specific situations (billing, Spanish line, a region) to specific numbers. A data-lookup skill can also supply a dynamic destination at call time (for example, route to the booked technician). The original caller ID is passed through to the receiving line. ## Keypad input (DTMF) Callers can press keypad digits during a call — useful for menus, account numbers, or confirmations. See [DTMF / Keypad](/channels/dtmf). ## In the API | Action | Endpoint | Scope | | ------------------------ | ----------------------------- | --------------- | | Search available numbers | `GET /numbers/search` | `numbers:read` | | Reserve a number | `POST /numbers/reserve` | `numbers:write` | | Purchase a number | `POST /numbers/purchase` | `numbers:write` | | Import a number you own | `POST /numbers/import` | `numbers:write` | | Assign to an agent | `POST /numbers/{id}/assign` | `numbers:write` | | Unassign (keep, pool it) | `DELETE /numbers/{id}/assign` | `numbers:write` | | Release a number | `DELETE /numbers/{id}` | `numbers:write` | | Review call summaries | `GET /agents/{agentId}/calls` | `calls:read` | ```bash theme={null} # Assign a purchased number to a published agent curl -X POST https://builder.flowyte.com/api/v1/numbers/NUM_ID/assign \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{"agentId": "AGENT_ID"}' ``` `DELETE /numbers/{id}` permanently **releases** the number back to the carrier. To detach a number from an agent while keeping it, use `DELETE /numbers/{id}/assign` to move it to the pool. Configure the greeting, transfer rules, and call control on the agent, then republish. Editing without republishing changes only the draft — the live phone line keeps serving the old version. # Embeddable Widget Source: https://docs.flowyte.com/channels/widget Drop a chat agent onto any website with a browser-safe publishable key. The widget puts a published agent on your own site as a chat bubble. It authenticates in the browser with a **publishable key** (`flowyte_pk_…`) — agent-pinned, origin-allowlisted, and limited to public chat — so it is safe to ship in client-side code. No secret key ever touches the browser. ## How it works Create a key for the agent and list the exact origins (sites) allowed to use it. The full key is returned on create — it is public by design, not a show-once secret. Fetch the loader ` ``` ## Origin allowlisting A publishable key only works from an origin you listed. Browser requests must send an `Origin` that matches the key's `allowedOrigins`; anything else is rejected with `403`. Keep the list tight — one key per site is a good default. You can rotate a key (same agent and origins, with a grace window) or revoke it instantly. ## Customize and harden * **Appearance & copy** live in the agent's widget config (`theme`, `copy`, `behavior`). * **Rate limiting** per key (`rateLimitRpm`) caps abuse from a single origin. * **Identity verification** (`requireIdentityVerification`) can require a signed visitor token before a session opens, for logged-in experiences. ## In the API | Action | Endpoint | Scope | | --------------------------- | ----------------------------------------------------- | -------------------------------- | | List publishable keys | `GET /agents/{agentId}/publishable-keys` | `pubkeys:read` | | Mint a publishable key | `POST /agents/{agentId}/publishable-keys` | `pubkeys:write` | | Update a key | `PATCH /agents/{agentId}/publishable-keys/{id}` | `pubkeys:write` | | Rotate a key (grace window) | `POST /agents/{agentId}/publishable-keys/{id}/rotate` | `pubkeys:write` | | Revoke a key | `DELETE /agents/{agentId}/publishable-keys/{id}` | `pubkeys:write` | | Get / replace widget config | `GET` · `PUT /agents/{agentId}/widget` | `widgets:read` · `widgets:write` | | Get the embed snippet | `GET /agents/{agentId}/widget/embed` | `widgets:read` | ```bash theme={null} # Mint a publishable key scoped to two origins curl -X POST https://builder.flowyte.com/api/v1/agents/AGENT_ID/publishable-keys \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "name": "Marketing site", "allowedOrigins": ["https://example.com", "https://www.example.com"], "env": "live" }' ``` The widget talks to the public chat surface (`chat:public` scope) using only the publishable key — visitors get an anonymous session pinned to the one agent. Your secret key stays server-side. The widget serves the agent's **published** version. Republish after editing, or visitors keep seeing the old behavior. # Agents Source: https://docs.flowyte.com/concepts/agents The single entity you configure — persona, language, and voice. An **agent** is the one user-facing entity in Flowyte. Everything else — knowledge, skills, guardrails, playbooks — attaches to an agent. You configure who the agent is; the platform handles turning that into a real-time voice and chat experience. ## What an agent holds * **Persona** — its name, role, goals, and the things it must never do. * **Language & voice** — the primary language (it can mirror a caller's language) and the voice it speaks with. * **Behavior** — greeting, tone, and how it handles handoffs. ## Draft vs published Editing an agent changes its **draft**. The in-browser tester runs the draft, but phone and chat channels serve the last **published** version. Publishing freezes a version; you can roll back to a previous one at any time. ## In the API | Action | Endpoint | | ----------------- | ----------------------------------------- | | Create an agent | `POST /agents` | | Read / update | `GET /agents/{id}` · `PATCH /agents/{id}` | | Publish a version | `POST /agents/{id}/publish` | | Roll back | `POST /agents/{id}/rollback` | | Test (streaming) | `POST /agents/{id}/simulate` | See the [Agents endpoints](/api-reference/introduction) in the reference for the full shape. # Billing & Credits Source: https://docs.flowyte.com/concepts/billing A prepaid credit wallet you top up with Stripe, metered per voice minute and per chat message. Flowyte runs on a **prepaid credit wallet**. You add funds, the platform meters usage as agents work, and credits are drawn down in real time. \*\*1 credit = 1 cent ($0.01)**, so a $10 top-up is 1,000 credits. ## How metering works Usage is metered per channel as it happens: * **Voice** — per minute of connected call time. * **Chat** — per message exchanged. * **Toll-free inbound** — a call received on a toll-free (8XX) number bills your normal voice minute **plus a flat 2 credits/min** (\$0.02), the same on every plan. Toll-free is "called-party-pays," so the number owner covers the higher carrier cost. See the toll-free note below. * **Call transfers** — per minute of the **forwarded** call, billed for the whole bridged duration including hold or queue time. Your AI charges stop at the hand-off, so forwarded minutes are a separate bucket from AI minutes. * **SMS** — per message **segment** (a 160-character part), the same rate in **both directions** (messages you send and messages you receive). See [SMS pricing](#sms-10dlc) below. * **Phone numbers** — a recurring **monthly** rental (2 credits/min are voice; the rental itself is a flat \$2/month per number, billed on each number's purchase anniversary). * **SMS management** — a recurring **monthly** 10DLC campaign fee (\$5/month) while SMS is active. Each charge becomes a usage item with a `channel` (`voice` · `chat` · `transfer_minute` · `number` · `sms` · `sms_management`), a `quantity` (the metered units), and the `credits` debited. Pull the day-by-day breakdown from `GET /billing/usage/items`, or the rolled-up records from `GET /billing/usage`. **Toll-free pass-through.** The flat 2-credit/min toll-free surcharge covers ordinary inbound traffic. In rare cases the underlying carrier charges more than that for a particular call — for example high-access or international originators, or the federal per-call payphone-origination fee — and those above-surcharge carrier costs are **passed through at cost**. Toll-free numbers may also be subject to inbound usage limits to protect against artificial traffic inflation.

SMS (10DLC)

Text messaging in the US requires a one-time carrier registration (A2P 10DLC) for your business. Once registered, SMS is billed three ways: * **Activation** — a one-time **2,500 credits (\$25)** when you submit your registration. It covers the carrier brand fee, the first campaign vetting, and a one-resubmit buffer. * **Management fee** — a recurring **500 credits (\$5) per month** while your registration stays active, which renews the carrier campaign. **The first month is included in activation**, so the first monthly fee is debited **30 days after** you activate. It stops when SMS is deactivated. * **Per-segment usage** — each message **segment** (160 characters), the **same rate in both directions** — messages your agent sends *and* messages it receives: | Plan | Credits / segment | Price | | ------------------ | ----------------- | ------ | | Pay-as-you-go | 4 | \$0.04 | | Starter · Growth | 3 | \$0.03 | | Scale · Enterprise | 2 | \$0.02 | An inbound reply is capped at **2 segments** for billing, so a long inbound message can't run up an unbounded charge. Usage segments land on the `sms` channel; the monthly management fee lands on `sms_management`. The \$5/month management fee is disclosed on the SMS page before you activate and again on the active status banner — there are no surprise recurring charges. Deactivating SMS (a support request today) stops the fee; there is no proration or refund for a partial month. ## Topping up with Stripe `POST /billing/wallet/topup` starts a Stripe payment and returns a `clientSecret` your checkout UI confirms. Once the payment settles, the credits land in the wallet. Downloadable invoices are available from `GET /billing/invoices`. `topup`, `auto-reload`, and changing your **plan** are completed through the dashboard session (Stripe checkout), not with an API key. Read endpoints — wallet, usage, plans, invoices — accept a key with `billing:read`. ## Auto-reload So an agent never goes silent mid-day, configure **auto-reload**: when the balance drops below `thresholdUsd`, the platform charges `amountUsd` automatically. ```json theme={null} PUT /billing/wallet/auto-reload { "enabled": true, "thresholdUsd": 20, "amountUsd": 50 } ``` ## Plans Plans (`Starter`, `Growth`, `Scale`) bundle included minutes at a lower per-minute rate plus an overage rate beyond them; the wallet covers everything else. List them with `GET /billing/plans` and read your current plan from `GET /billing/subscription`. ## In the API | Action | Endpoint | Scope | | ---------------------------------- | ------------------------------------ | ---------------------------------- | | Get wallet balance | `GET /billing/wallet` | `billing:read` | | Top up via Stripe | `POST /billing/wallet/topup` | dashboard session | | Configure auto-reload | `PUT /billing/wallet/auto-reload` | dashboard session | | Credit the wallet (manual / admin) | `POST /billing/wallet/credit` | `billing:write` | | List usage records | `GET /billing/usage` | `billing:read` | | Itemized charges in a window | `GET /billing/usage/items` | `billing:read` | | List plans | `GET /billing/plans` | `billing:read` | | Get / change subscription | `GET /billing/subscription` · `POST` | `billing:read` · dashboard session | | List invoices (PDF links) | `GET /billing/invoices` | `billing:read` | ```bash theme={null} # Check the balance before kicking off a batch of outbound work curl https://builder.flowyte.com/api/v1/billing/wallet \ -H "Authorization: Bearer flowyte_sk_…" ``` Watch the balance from a server key with `billing:read` and alert your own ops channel — pair it with auto-reload so a spike in call volume never drains the wallet to zero. # Guardrails Source: https://docs.flowyte.com/concepts/guardrails Deterministic policies that keep an agent on-script and verify callers before disclosure. **Guardrails** are the deterministic rules that constrain what an agent will say and do — the difference between "usually behaves" and "provably won't cross a line." Unlike the agent's prompt, guardrails are enforced, not suggested. ## What guardrails cover * **Topic and policy limits** — things the agent must refuse or redirect. * **Caller verification** — require identity verification *before* disclosing sensitive information, so the agent never reads back an account detail to an unverified caller. * **Escalation rules** — when to hand off to a human. ## In the API | Action | Endpoint | | -------------------------- | --------------------------------------- | | Read / set guardrails | `GET` · `PATCH /agents/{id}/guardrails` | | Caller verification config | `GET /agents/{id}/caller-verification` | Guardrails are part of the published version — publish to apply changes. # Integrations Source: https://docs.flowyte.com/concepts/integrations Connect an external system once, then turn it into agent skills — either pre-built actions or fields you map yourself. An **integration** lets an agent act inside an external system — recognize a caller in your CRM, book on a calendar, look up an order, capture a request. You connect the provider **once** for your organization, then turn it into [skills](/concepts/skills) on any agent. No code, no per-agent credentials. There are two ways to turn a connected provider into a skill: For curated providers, ready-made actions (e.g. "find a customer", "book a visit") install as skills in one call. For any REST/GraphQL provider, discover its full schema and map its fields onto your agent's parameters — like a Zapier you build for your agent. ## 1. Provision pre-built actions For curated providers (Google Calendar, Google Sheets, Shopify, and field-service software), the actions are already built — you just connect and provision. `POST /integrations/{kind}/connect` — OAuth returns an `oauthUrl` to approve; API-key providers take `{ "credentials": { … } }`. Credentials are stored encrypted, never echoed. `GET /integrations/{kind}/actions` lists each action's slug, the parameters it collects, and any config an operator must still fill. `POST /agents/{agentId}/integrations/{kind}/provision` turns chosen actions into skills (omit `actions` to provision the important ones). Re-running is idempotent. | Provider | Connect via | Status | | --------------- | ----------- | ------ | | Google Calendar | OAuth | Live | | Google Sheets | OAuth | Live | | Shopify | API key | Live | ## 2. Map fields from a discovered schema (the open model) For any provider whose schema can be introspected (REST/OpenAPI, GraphQL, or a **SQL database**), you don't wait for pre-built actions — you **discover** the provider's entire data model and **map** the fields you want onto your agent's parameters. The mapping is the integration; there's no per-provider code. Your own **[Postgres or MySQL database](/integrations/sql-database)** is the clearest example: connect with a least-privilege credential, discover the schema, scope out sensitive columns, and bind a read (or a scoped write) as a skill — walk through it in [Connect a SQL database](/guides/connect-sql-database). `POST /integrations/{kind}/discover` introspects the connected provider into a normalized **schema** — every object, field, relationship, and operation — and returns a summary (object / field / relationship counts). Re-running refreshes it. `GET /integrations/{kind}/schema` returns the discovered schema: the objects, their fields (with types, whether they're required, allowed values, and what's sensitive), the relationships between objects, and the operations you can run. This is the catalog you map from. `POST /agents/{id}/integrations/{kind}/bindings` authors a **binding**: pick an operation, map your agent's inputs to its arguments, and **project** which provider fields come back as your own output names. It's validated against the schema and compiles to a skill. A binding maps to your **canonical parameters** (`caller_phone`, `caller_name`, `service_address`, …), so the same mapping concept works across any provider, and your agent's prompt stays the same whatever system is behind it. Bindings are authored with a human (or the AI assistant) in the loop and validated up front — what reaches a live call is a frozen, pre-approved skill. Walk through it end to end in [Map an integration's fields](/guides/map-integration-fields). ### Shortcuts: auto-map and preset packs You don't have to author every binding by hand: * **Auto-map from a goal** — `POST /agents/{id}/integrations/{kind}/bindings/auto` takes a plain-language goal ("look up a caller by phone and return their open tickets"). The AI assistant proposes a binding over the discovered schema, validates it, and saves it as a **disabled draft** for you to review before enabling. This is the fastest way to go from intent to a working skill. * **Install a preset pack** — many providers ship a **Connector Pack**: curated, ready-to-install preset skills plus guidance on how the provider's operations behave and how to identify a caller. `GET /integrations/{kind}/pack` reads it; `POST /agents/{id}/integrations/{kind}/pack/install` installs the presets onto an agent in one call. To build a mapping UI of your own, browse the discovered schema piece by piece with `GET /integrations/{kind}/objects`, `GET /integrations/{kind}/objects/{object}`, and `GET /integrations/{kind}/operations` instead of pulling the whole schema at once. ## Custom integrations and the universal fallback If a provider can't be introspected, an agent can still reach any system you run: a webhook skill that calls your own HTTP endpoint, or tools exposed over MCP. See [Build a custom integration](/integrations/custom). When there's no native connector for an app, **[Zapier](/integrations/zapier)** is the universal fallback for **write actions**: connect your Zapier MCP endpoint and freeze any of its 8,000+ app tools into a skill the agent runs on a call. To *read* caller data, sync records into Flowyte's caller-context store instead of reading live through Zapier — the agent looks them up locally in milliseconds. ## In the API | Action | Endpoint | Scope | | ---------------------------------- | ---------------------------------------------------------- | -------------------- | | Browse the provider catalog | `GET /integrations/catalog` | `integrations:read` | | Connect a provider | `POST /integrations/{kind}/connect` | `integrations:write` | | List a provider's actions | `GET /integrations/{kind}/actions` | `integrations:read` | | Provision actions as skills | `POST /agents/{agentId}/integrations/{kind}/provision` | `skills:write` | | **Discover the schema** | `POST /integrations/{kind}/discover` | `integrations:write` | | **Read the discovered schema** | `GET /integrations/{kind}/schema` | `integrations:read` | | Browse the discovered objects | `GET /integrations/{kind}/objects` | `integrations:read` | | Get one object's detail | `GET /integrations/{kind}/objects/{object}` | `integrations:read` | | Browse bindable operations | `GET /integrations/{kind}/operations` | `integrations:read` | | **Map a binding** | `POST /agents/{agentId}/integrations/{kind}/bindings` | `skills:write` | | **Auto-map a binding from a goal** | `POST /agents/{agentId}/integrations/{kind}/bindings/auto` | `skills:write` | | Read a provider's preset pack | `GET /integrations/{kind}/pack` | `integrations:read` | | Install a provider's preset pack | `POST /agents/{agentId}/integrations/{kind}/pack/install` | `skills:write` | | Disconnect a provider | `DELETE /integrations/{kind}` | `integrations:write` | Skills run on the **published** agent — [publish](/concepts/agents) after provisioning or binding. # Knowledge Source: https://docs.flowyte.com/concepts/knowledge The sources an agent answers from, grounded so it doesn't make things up. **Knowledge** is what your agent draws on to answer accurately. You add sources — your help center URL, uploaded documents, FAQs, or plain text — and the platform indexes them so the agent retrieves the right passage at answer time instead of guessing. ## Adding a source Ingestion is **asynchronous**: after you add a source it moves through `pending` → `indexed`. Poll the source until it's `indexed` before relying on it. You can preview what the agent would retrieve for a question to sanity-check coverage. ## Knowledge gaps When callers ask things your knowledge doesn't cover, those questions surface as **knowledge gaps** so you can close them — the loop that makes the agent better over time. ## In the API | Action | Endpoint | | ------------------- | ----------------------------------------- | | Add a source | `POST /agents/{id}/knowledge/sources` | | Check ingest status | `GET /agents/{id}/knowledge/sources/{id}` | | Preview retrieval | `POST /agents/{id}/knowledge/preview` | | Review gaps | `GET /agents/{id}/knowledge-gaps` | # Languages & Voice Source: https://docs.flowyte.com/concepts/languages-and-voice How an agent picks its language, mirrors the caller, and chooses a voice. Every agent speaks a **primary language** and can **mirror the caller's language** mid-call. Separately, it has a **voice** — the identity it speaks with. Language and voice are set independently: one agent can answer in around 40 languages while keeping the same voice. ## Primary language and mirroring An agent holds a `primaryLanguage` (a BCP-47 code like `en`, `es`, or `fr`) plus a list of `languages` it supports. The greeting and default behavior use the primary language. When a caller speaks a different supported language, the agent can switch to it for the rest of the conversation — no separate flow required. Set `primaryLanguage` and `languages` when you create the agent, or update them later with `PATCH /agents/{id}`. Mirroring works across the supported set; pick the languages you actually want to serve. ## How a voice is chosen A voice is a catalog entry with a normalized set of facets — `language`, `accent`, `gender`, `age`, `category`, and mood `descriptorTags`. You find a voice three ways, then assign it. `GET /voices` returns the catalog, narrowed by any combination of facets (all AND-combined): `language`, `accent`, `region`, `gender`, `age`, `category`, `tone`, `useCase`, or free-text `q`. `GET /voices/facets` returns live counts per facet ("Female · 142") over the same filters, so a picker can show how each chip shrinks the result set. `POST /voices/search` turns a phrase like *"calm older british woman for support"* into facets and returns the resolved filters plus a short ranked shortlist to audition. `PUT /agents/{id}/voice` assigns a `voiceId` to the agent for a language (defaults to the primary language). This is the one-call way to set the voice. A voice carries a `previewUrl` to audition it, `supportedLanguages` it can speak, `capabilities` (which fine-tuning settings the voice supports and its safe speed band), and `verifiedLanguages` — languages the voice is explicitly verified for, with a localized preview. Per-language voices are stored in the agent's `voiceMap`, and the primary-language voice is also its `defaultVoiceId`. Assigning a voice for one language never changes the others. ## In the API | Action | Endpoint | Scope | | ------------------------------------------ | ------------------------ | -------------- | | Browse the voice catalog | `GET /voices` | `agents:read` | | Live facet counts | `GET /voices/facets` | `agents:read` | | Natural-language voice search | `POST /voices/search` | `agents:read` | | Assign a voice to an agent | `PUT /agents/{id}/voice` | `agents:write` | | Set primary language / supported languages | `PATCH /agents/{id}` | `agents:write` | ```bash curl theme={null} curl https://builder.flowyte.com/api/v1/voices/search \ -H "Authorization: Bearer flowyte_sk_..." \ -H "Content-Type: application/json" \ -d '{"query":"calm older british woman for support","limit":5}' ``` ```js Node theme={null} const res = await fetch("https://builder.flowyte.com/api/v1/agents/AGENT_ID/voice", { method: "PUT", headers: { Authorization: "Bearer flowyte_sk_...", "Content-Type": "application/json", }, body: JSON.stringify({ voiceId: "VOICE_ID", language: "en" }), }); ``` ```python Python theme={null} import requests requests.put( "https://builder.flowyte.com/api/v1/agents/AGENT_ID/voice", headers={"Authorization": "Bearer flowyte_sk_..."}, json={"voiceId": "VOICE_ID", "language": "en"}, ) ``` Voice and language are part of the agent's **draft**. [Publish](/concepts/agents) the agent so phone and chat callers hear the change. # Numbers Source: https://docs.flowyte.com/concepts/numbers Own a phone number and point it at an agent — search, reserve, purchase, assign. A **number** is a phone line your organization owns. Inbound calls to it ring the agent you assign. You can buy a new number from the carrier, or import one you already own, then route it to any agent — all over the API. ## The lifecycle `GET /numbers/search` browses purchasable inventory. Filter by `areaCode`, `locality`, `administrativeArea` (state), `numberType` (`local` or `toll_free`), required `features` (`voice`, `sms`, …), or a vanity `contains` / `endsWith`. `bestEffort` (default true) widens a too-narrow filter; set it false for strict last-four or vanity matching. `POST /numbers/reserve` holds a number (about 30 minutes) so it can't be taken while you confirm. This does **not** charge your wallet. Pass the returned reservation id to purchase. `POST /numbers/purchase` buys the number and debits your prepaid wallet. Include the `reservationId` from the hold so the order can't be sniped. Returns the owned number. `POST /numbers/{id}/assign` points the number at an agent (sets its default agent). Inbound calls now reach that agent. Already own a number elsewhere? `POST /numbers/import` brings a number you hold on your carrier account into Flowyte with **no wallet charge** — and can assign it to an agent in the same call. ## Releasing vs unassigning Detaching a number has two very different outcomes: | You want to… | Call | Result | | ------------------------------------------- | ----------------------------- | ----------------------------------------------------------- | | Keep owning it, just free it from the agent | `DELETE /numbers/{id}/assign` | Moves to your pool (`status: available`); reassign any time | | Stop owning and paying for it | `DELETE /numbers/{id}` | Permanently released to the carrier | `DELETE /numbers/{id}` is irreversible — the number goes back to the carrier and may be gone for good. To park a number without losing it, **unassign** it instead. ## Vendor-neutral provider field Each number carries a `provider` field. It is an **opaque, vendor-neutral label** for the carrier behind the line — treat it as an identifier, not a brand to depend on. Cost is reported as `monthlyCost` (and `setupCost` on available numbers). ## In the API | Action | Endpoint | Scope | | ----------------------------- | ------------------------------- | --------------- | | List owned numbers | `GET /numbers` | `numbers:read` | | Search available numbers | `GET /numbers/search` | `numbers:read` | | Re-check one number's price | `GET /numbers/available/{e164}` | `numbers:read` | | Reserve a hold | `POST /numbers/reserve` | `numbers:write` | | Release a hold | `DELETE /numbers/reserve/{id}` | `numbers:write` | | Purchase | `POST /numbers/purchase` | `numbers:write` | | Import an owned number | `POST /numbers/import` | `numbers:write` | | Assign to an agent | `POST /numbers/{id}/assign` | `numbers:write` | | Unassign (keep, move to pool) | `DELETE /numbers/{id}/assign` | `numbers:write` | | Release to carrier | `DELETE /numbers/{id}` | `numbers:write` | ```bash curl theme={null} # 1. Find a local number in area code 415 curl "https://builder.flowyte.com/api/v1/numbers/search?areaCode=415&numberType=local" \ -H "Authorization: Bearer flowyte_sk_..." # 2. Purchase it (debits the wallet) curl https://builder.flowyte.com/api/v1/numbers/purchase \ -H "Authorization: Bearer flowyte_sk_..." \ -H "Content-Type: application/json" \ -d '{"e164":"+14155550100"}' ``` ```js Node theme={null} // Assign an owned number to an agent await fetch("https://builder.flowyte.com/api/v1/numbers/NUMBER_ID/assign", { method: "POST", headers: { Authorization: "Bearer flowyte_sk_...", "Content-Type": "application/json", }, body: JSON.stringify({ agentId: "AGENT_ID" }), }); ``` A purchase needs funds in your prepaid wallet — an empty balance returns `402`. A number that was taken between search and purchase returns `409`. The agent must be [published](/concepts/agents) to answer live calls on its assigned number. # Observe & Receipts Source: https://docs.flowyte.com/concepts/observe Post-call analytics, full transcripts, and an event-by-event receipt for every conversation. Once an agent is live, **Observe** is where you see what actually happened — how many calls were answered, what callers asked about, where the agent fell short, and exactly what it did on any one conversation. Every call (voice **or** chat) produces a summary, a transcript, and a **receipt**. ## The answer-first analytics card `GET /agents/{agentId}/analytics` returns the money card first: total calls, answered vs. missed, containment rate, transfer rate, and `recoveredRevenueUsd` (captured calls × your own average ticket). Pass `?avgTicketCents=` or `?avgTicketUsd=` so the dollar figure is yours, and filter by `from`, `to`, and `channel`. For org-wide rollups across every agent, use `/analytics/overview`, `/analytics/by-agent`, and `/analytics/timeseries`. ## Conversations, transcripts, and receipts The **history list** (`GET /conversations`) is newest-first and full-text searchable over the transcript and the one-line summary — filter by `q`, `agentId`, `from`, `to`, `channel`, and `outcome`. Each row already carries the deterministic outcome, summary, primary topic, and sentiment. Drill into one conversation for its summary, transcript, signed recording URL (voice only), and the **receipt** — the ordered list of audit events that reconstructs the call turn by turn: `user_utterance`, `agent_message`, `tool_call`, `kb_retrieval`, `guardrail`, `verification`, `handoff`, and more. The receipt is the same event stream you watched live in [Test & Simulate](/concepts/testing). ## Sentiment, topics, and knowledge gaps * **Sentiment** is a per-call label — `positive`, `neutral`, `negative`, or `frustrated` — computed deterministically (no LLM). It's triage: the rollup surfaces the worst calls to review first. * **Topics** are zero-config clusters of what callers actually say. `GET /agents/{agentId}/topics` ranks them by volume; drill any cluster into its conversations. * **Knowledge gaps** are deduped questions the agent couldn't answer. List them, then `PATCH` a gap to dismiss it or mark it in progress — close the loop by adding [knowledge](/concepts/knowledge). ## In the API | Action | Endpoint | Scope | | ------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------ | | Agent analytics card | `GET /agents/{agentId}/analytics` | `analytics:read` | | Per-agent / org timeseries | `GET /agents/{agentId}/analytics/timeseries` · `GET /analytics/timeseries` | `analytics:read` | | Org overview / per-agent leaderboard | `GET /analytics/overview` · `GET /analytics/by-agent` | `analytics:read` | | Topics (and drill-down) | `GET /agents/{agentId}/topics` · `…/topics/{clusterId}/conversations` | `analytics:read` | | Knowledge gaps (list / curate) | `GET /agents/{agentId}/knowledge-gaps` · `PATCH …/{gapId}` | `analytics:read` · `analytics:write` | | Call summaries | `GET /agents/{agentId}/calls` | `calls:read` | | Conversation history (searchable) | `GET /conversations` | `analytics:read` | | One conversation summary | `GET /conversations/{id}` | `calls:read` | | Receipt (ordered audit events) | `GET /conversations/{id}/receipt` | `calls:read` | | Transcript | `GET /conversations/{id}/transcript` | `calls:read` | | Signed recording URL (voice) | `GET /conversations/{id}/audio` | `calls:read` | ```bash theme={null} # Pull the receipt — the full event trail for one conversation curl https://builder.flowyte.com/api/v1/conversations/CONV_ID/receipt \ -H "Authorization: Bearer flowyte_sk_…" ``` Analytics and history are reporting endpoints — give read-only keys `analytics:read` and `calls:read` and nothing more. Sentiment, topics, and gaps populate only once an agent crosses a small volume floor; below it they report `dataState: collecting`. # Playbooks Source: https://docs.flowyte.com/concepts/playbooks Scripted, multi-step flows for repeatable tasks. A **playbook** is a structured, multi-step flow the agent follows for a repeatable task — collecting the fields needed to book a job, qualifying a lead, or walking a caller through a return. Where skills are single actions and knowledge is recall, a playbook is a *procedure*: do this, then this, branch on the answer. ## When to use one Reach for a playbook when a task has a fixed shape — a set of inputs to gather in order, with branches — rather than an open-ended Q\&A. The agent stays conversational, but the playbook makes sure every required step happens. ## In the API | Action | Endpoint | | ---------------------- | ------------------------------------------- | | List / create | `GET` · `POST /agents/{id}/playbooks` | | Read / update | `GET` · `PATCH /agents/{id}/playbooks/{id}` | | Inspect the flow graph | `GET /agents/{id}/playbooks/{id}/graph` | In the API and dashboard these are called **playbooks**; some internal references use the older term *procedures*. They're the same thing. # Skills Source: https://docs.flowyte.com/concepts/skills The tools an agent can use to take actions, not just answer. A **skill** is a tool the agent can call mid-conversation to *do* something — transfer a call, send an email, book an appointment, look up an order, or check a service area. Skills are what turn an answering machine into an assistant. ## How skills work Each skill has a type (what kind of action it performs) and a configuration (the specifics — which mailbox, which calendar, what parameters to collect from the caller). When the agent decides the skill is relevant, it collects the needed inputs in natural language and invokes it. ## Native integrations Some skills connect to external systems through a **native integration**. You connect the provider once (OAuth or an API key), then provision its actions as skills on any agent — no code. ## In the API | Action | Endpoint | | ----------------------- | ------------------------------------------------ | | List a skill catalog | `GET /skill-types` | | Add a skill to an agent | `POST /agents/{id}/skills` | | Update / remove | `PATCH` · `DELETE /agents/{id}/skills/{skillId}` | | Browse integrations | `GET /integrations` | Skills only take effect on the **published** version of the agent — publish after adding one. # Test & Simulate Source: https://docs.flowyte.com/concepts/testing Try your agent against the draft before you publish — over text, or live in-browser voice. Every edit you make changes a **draft**. The tester lets you talk to that draft — by text or by voice — before you publish it to phone and chat. Nothing you do here touches the live, published version, and draft runs are **not billed**. The tester always runs the **draft**. Phone and chat serve the last **published** version. If a change works in the tester but not on a real call, you probably haven't published yet — see [Versioning & Publishing](/concepts/versioning). ## Simulate over text (and voice) `POST /agents/{agentId}/simulate` opens a **server-sent events (SSE)** stream. Set `mode` to `text` for the chat tester or `voice` for the in-browser voice tester. Leave `draftMode` as its default (`true`) to run against the freshly-compiled draft. Each SSE frame is a single audit event: the `event:` line is the event type, and the `data:` line is the event JSON. You'll see `user_utterance`, `agent_message`, `tool_call`, `kb_retrieval`, `guardrail`, and more stream by in real time — the same shapes you'll later read back as a [receipt](/concepts/observe). The stream ends with `event: done` (or `event: error`). ```bash curl theme={null} curl -N https://builder.flowyte.com/api/v1/agents/AGENT_ID/simulate \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{"mode":"text","draftMode":true,"transcript":["What are your hours?"]}' ``` ```js Node theme={null} const res = await fetch( "https://builder.flowyte.com/api/v1/agents/AGENT_ID/simulate", { method: "POST", headers: { Authorization: "Bearer flowyte_sk_…", "Content-Type": "application/json", }, body: JSON.stringify({ mode: "text", draftMode: true, transcript: ["What are your hours?"] }), }, ); const reader = res.body.getReader(); // read the SSE stream until `event: done` ``` ```python Python theme={null} import httpx with httpx.stream( "POST", "https://builder.flowyte.com/api/v1/agents/AGENT_ID/simulate", headers={"Authorization": "Bearer flowyte_sk_…"}, json={"mode": "text", "draftMode": True, "transcript": ["What are your hours?"]}, ) as r: for line in r.iter_lines(): print(line) # SSE frames; stops at `event: done` ``` ## In-browser voice with a talk-token To test **voice** live in the browser, mint a short-lived **talk-token**. It returns the credentials your browser needs to join a real-time voice session — a URL, a token, and a room. `POST /agents/{agentId}/talk-token` is a **dashboard-only** call. It is authenticated by your dashboard session, not by an API key, so it cannot be called with `flowyte_sk_…`. Use it from the in-browser tester. With `draftMode: true` the session runs the ephemeral draft and is **not billed**. With `draftMode: false` it runs the latest **published** version — and returns `409 no_published_version` if the agent has never been published. ## In the API | Action | Endpoint | Scope | | ------------------------------------ | ------------------------------------------------- | ---------------------- | | Simulate (text / voice) over SSE | `POST /agents/{agentId}/simulate` | `agents:write` | | Mint an in-browser voice talk-token | `POST /agents/{agentId}/talk-token` | dashboard session only | | Kick a probe run (scripted scenario) | `POST /agents/{agentId}/probe/run` | `agents:write` | | Poll a probe run | `GET /agents/{agentId}/probe/runs/{runId}` | `agents:read` | | Stream probe progress (SSE) | `GET /agents/{agentId}/probe/runs/{runId}/stream` | `agents:read` | **Probe** runs a saved scenario end-to-end and reports whether quality gates passed — goal completion, per-turn latency, and language fidelity. Use it in CI to catch regressions before they reach callers. # Versioning & Publishing Source: https://docs.flowyte.com/concepts/versioning Edit a draft freely, publish to freeze a version, and roll back instantly if you need to. An agent always has two states: the **draft** you're editing and the **published** version your callers actually reach. Every change — persona, knowledge, skills, guardrails, playbooks — lands in the draft. Phone and chat keep serving the last published version until you publish again. A change is invisible to live calls until you **publish**. "Works in the tester, fails on the phone" almost always means the draft has unpublished edits. Confirm with the pre-publish diff before you ship. ## Publishing freezes a version `POST /agents/{id}/publish` compiles the current draft and writes a new **frozen** version — an immutable snapshot of the compiled config, stamped with a version number, who published it, and an optional `note`. From that moment, every new call runs that frozen version. Because publishing is a money-and-callers moment, it accepts an `Idempotency-Key` so a retried request never double-ships. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/AGENT_ID/publish \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Idempotency-Key: 5f3c…" \ -H "Content-Type: application/json" \ -d '{"note":"Added after-hours transfer skill"}' ``` ## Look before you publish Two read-only endpoints make the trust moment safe: * **Diff** — `GET /agents/{id}/versions/{versionId}/diff` lists exactly what changed against the current draft (each entry is a `field` with `before`, `after`, and a `kind` of `added`, `removed`, or `changed`). * **Pre-publish report** — `GET /agents/{id}/prepublish-report` shows what the agent **will say** and **won't say**, what data leaves the system (which skills send which fields, and where), active guardrails, knowledge coverage, and any warnings. ## Rolling back If a published version misbehaves, `POST /agents/{id}/rollback` with a `versionId` re-points the live agent at a previous frozen version immediately — no recompile, no redeploy. Browse the history with `GET /agents/{id}/versions`. ## In the API | Action | Endpoint | Scope | | -------------------------- | -------------------------------------------- | -------------- | | Publish (freeze a version) | `POST /agents/{id}/publish` | `agents:write` | | Roll back to a version | `POST /agents/{id}/rollback` | `agents:write` | | List version history | `GET /agents/{id}/versions` | `agents:read` | | What-changed diff | `GET /agents/{id}/versions/{versionId}/diff` | `agents:read` | | Pre-publish report | `GET /agents/{id}/prepublish-report` | `agents:read` | A clean release loop: edit the draft → [test it](/concepts/testing) → read the pre-publish report → publish with a `note`. Keep the note descriptive — it's what you'll scan when deciding which version to roll back to. # Authentication Source: https://docs.flowyte.com/get-started/authentication API keys, scopes, and the two ways requests authenticate. The Flowyte API has two authentication modes that both resolve to the same tenant-scoped identity, so a key can never reach another organization's data. ## Secret API keys (server-to-server) For your backend and for agents building against the API, use a **secret key**: ``` Authorization: Bearer flowyte_sk_… ``` Your API key already determines the organization — no extra headers are needed. The first key is minted in the dashboard's **Developer** page (a chicken-and-egg bootstrap). Manage your keys — mint, rotate, and revoke — in the dashboard Developer page. ## Browser sessions The dashboard authenticates with a session token. You won't use this directly when building against the API — it's how the builder UI talks to the same endpoints your key does. ## Publishable keys (the chat widget) The embeddable chat widget authenticates in the browser with a **publishable key** (`flowyte_pk_…`). It is agent-pinned, origin-allowlisted, and limited to public chat — safe to ship in client-side code. ## Scopes Secret keys carry **scopes** that gate which operations they can call. Request only what you need. Common scopes: | Scope | Grants | | -------------------------------- | --------------------------------------------------------------------------------------------------- | | `agents:read` / `agents:write` | Read or modify agents, simulate, mint a talk token | | `knowledge:write` | Add and manage knowledge sources | | `skills:write` | Create and configure skills | | `pubkeys:write` | Mint publishable keys for the widget | | `numbers:write` | Search, reserve, and assign phone numbers | | `billing:read` / `billing:write` | Read the wallet / manage credits and plans | | `analytics:read` | List conversations and read post-call analytics — **every channel** | | `calls:read` | Read a conversation's detail, its **transcript text — voice, chat, and SMS**, and its event receipt | | `audit:read` | Read the organization's account activity trail (who changed what, plus Flowyte-staff access) | | `sms:read` / `sms:write` | A2P 10DLC registration, per-number SMS enablement, opt-out list, contacts — **not message content** | The authoritative scope list is the `Scope` enum in the [API Reference](/api-reference/introduction). **Message content lives under `calls:read`, not `sms:read`.** Despite the name, `calls:read` is channel-agnostic: an SMS conversation is stored as a conversation like any other, so `GET /conversations/{id}/transcript` returns the verbatim text of every message a customer sent or received. A key holding `calls:read` can read all of it. `sms:read` is the compliance surface — registration status, number toggles, the opt-out list and the contact roster. It grants **no** message bodies at all. Three consequences worth planning around: * A key granted "just call reads" can read your customers' text messages. * `calls:read` also grants the **event receipt** (`GET /conversations/{id}/receipt`) — the forensic record of everything the agent did on that call: tool calls, knowledge lookups, guardrail decisions and latency samples. Summary, transcript and receipt are one grant, not three. * There is currently **no way to grant SMS visibility without also granting voice and chat**; `calls:read` is all-or-nothing across channels. Reading a conversation also needs `analytics:read` to find it in the first place — the list endpoint and the transcript endpoint are separately scoped, so a key with only `analytics:read` sees conversations listed but every transcript comes back empty. That is a missing scope, not a bug. Some endpoints documented in the contract are **reserved** (planned ahead of release). Calling one returns a `403`, not a `404`. If a correctly-scoped call returns `403`, check whether the endpoint is marked reserved. # Draft vs Published vs Sandbox Source: https://docs.flowyte.com/get-started/draft-vs-published How edits, the tester, and live phone & chat traffic relate — and why you publish before going live. Every agent has two faces: the **draft** you are editing and the **published** version your customers reach. Understanding which one runs where is the difference between "it works in the tester but fails on the phone" and a clean launch. ## The three states Your live working copy. Every edit — knowledge, skills, guardrails, playbooks — changes the draft. It is never billed and never frozen. A frozen snapshot of the draft, taken at publish time. Phone calls and chat sessions serve the **last published version**, not your draft. A way to run the *draft* on demand — the tester and `draftMode: true` chat sessions compile the draft ephemerally so you can try changes before they go live. ## How traffic is routed * **The tester runs the draft.** It compiles your current draft on the fly, so it always reflects your latest unsaved-to-live changes. * **Phone & chat serve the last published version.** Until you publish, callers and the public chat endpoint keep getting the old behavior. * **Publishing freezes a version.** A publish takes your draft, compiles it, and stores it as an immutable, numbered version. That snapshot is what goes live. * **Rollback restores a version.** If a new publish misbehaves, repoint "published" at any earlier version — no re-editing required. This is the most common gotcha: you edit, the tester looks perfect, but the phone still does the old thing. **Editing changes the draft; only publishing moves it to live.** ## Why your first live call returns 409 A chat session (and any non-draft channel) needs a published version to serve. If an agent has **never been published**, a non-draft chat session responds `409` with the problem code `no_published_version`. The fix is always the same: **publish first**. ```jsonc theme={null} // POST /chat/sessions with draftMode:false on an unpublished agent { "type": "...", "title": "Conflict", "detail": "no_published_version" } ``` Testing the draft directly? Set `draftMode: true` when you create a chat session, or use the in-dashboard tester. Draft sessions compile your draft ephemerally and are **not billed**. ## A clean release flow Make your changes. Add knowledge, wire skills, tune guardrails. Use the tester or a `draftMode: true` chat session to confirm behavior. Pull the pre-publish diff and report to see exactly what is about to go live. Freeze the draft into a new numbered version. Phone & chat now serve it. Repoint published to a known-good version in one call. ## In the API | Action | Endpoint | Scope | | ----------------------------- | -------------------------------------------- | -------------- | | Publish (freeze a version) | `POST /agents/{id}/publish` | `agents:write` | | Roll back to a version | `POST /agents/{id}/rollback` | `agents:write` | | List version history | `GET /agents/{id}/versions` | `agents:read` | | Pre-publish what-changed diff | `GET /agents/{id}/versions/{versionId}/diff` | `agents:read` | | Pre-publish report | `GET /agents/{id}/prepublish-report` | `agents:read` | Authenticate every call with `Authorization: Bearer flowyte_sk_…`. A successful publish returns `201` with the new frozen version; `409` signals a publish race or version mismatch. See [Agents](/concepts/agents) and [Channels](/channels/voice) for what each version actually serves. # For AI agents Source: https://docs.flowyte.com/get-started/for-ai-agents How an LLM or coding agent builds and operates a Flowyte agent end to end, via the API. This page is written for an **AI agent** (or a developer's coding assistant) building against Flowyte programmatically. Everything a person can do in the dashboard is available over the API, so an agent can create, configure, publish, and test a Flowyte agent on its own. ## Machine-readable resources | Resource | URL | | ------------------------------ | --------------------------------------------------------------- | | Plain-text index of every page | [`/llms.txt`](https://flowyte.mintlify.site/llms.txt) | | Full docs as one file | [`/llms-full.txt`](https://flowyte.mintlify.site/llms-full.txt) | | Markdown of any page | append `.md` to its URL | | Docs MCP server | `https://flowyte.mintlify.site/mcp` | | OpenAPI contract | the **API Reference** tab | ## Authentication Every request uses a secret API key as a bearer token: ``` Authorization: Bearer flowyte_sk_… ``` Base URL: `https://builder.flowyte.com/api/v1`. The first key is created in the dashboard's Developer page; the key alone determines your organization (no other headers needed). Each endpoint lists the **scope** the key must hold — request only what you need. ## The build sequence `POST /agents` → capture `data.id`. `POST /agents/{id}/knowledge/sources`, then **poll** `GET …/sources/{id}` until `status` is `indexed` (ingestion is asynchronous). `POST /agents/{id}/skills` for actions like transfer or booking. `POST /agents/{id}/publish` — freezes the version that channels will serve. `POST /agents/{id}/simulate` — streams the conversation over SSE. `POST /agents/{id}/publishable-keys` for the chat widget, or assign a phone number. ## Rules that prevent the common mistakes * **Test with `POST /agents/{id}/simulate`** (SSE) — not `/chat/completions`. * **Phone and chat serve the last *published* version.** Edits change the *draft*. Always publish before going live; `409 no_published_version` means "publish first." * **Knowledge ingestion is async** — poll a source until `status: indexed` before relying on it. * **Responses** are the `ApiResponse` envelope (`{ success, data }`). **Lists** use cursor pagination — follow the returned cursor, not page numbers. * **Errors** are RFC 9457 problem+json — branch on the `type` / `status`. * **Streaming** endpoints are Server-Sent Events: read frames `event:\ndata:`, stop on `event: done`. Start with the [Quickstart](/quickstart), then browse the [API Reference](/api-reference/introduction). # MCP gateway Source: https://docs.flowyte.com/get-started/mcp-gateway Configure and operate your Flowyte tenant from any MCP client — Claude Code, Claude.ai, or your own agent runtime — over a remote MCP server. The **Control MCP gateway** is a remote [MCP](https://modelcontextprotocol.io) server that lets an MCP client — Claude Code, a Claude.ai connector, or any agent runtime — configure and operate your Flowyte tenant through a curated set of safe tools. It's a thin, scoped wrapper over the same `/api/v1` the rest of the platform uses: every tool call re-enters the API, carrying your key, so your organization scoping, scopes, and audit trail all apply exactly as they would over REST. Use it when you want an agent to *drive* Flowyte — list agents, create and configure one, add knowledge, wire skills and guardrails, deploy the chat widget, test, and publish — without writing REST glue. If you're calling the API directly instead, see [For AI agents](/get-started/for-ai-agents). * **Endpoint:** `https://mcp.flowyte.com/mcp` (`https://builder.flowyte.com/mcp` also works as an alias) * **Transport:** Streamable HTTP + JSON-RPC 2.0 (stateless, plain JSON) * **Protocol:** MCP `2025-11-25`, negotiated down for older clients * **Auth:** `Authorization: Bearer flowyte_sk_…` ## Connect ```bash Claude Code theme={null} claude mcp add --transport http flowyte https://mcp.flowyte.com/mcp \ --header "Authorization: Bearer flowyte_sk_…" ``` ```jsonc Generic MCP client theme={null} // Point any Streamable-HTTP MCP client at the endpoint with a Flowyte key. { "flowyte": { "transport": "http", "url": "https://mcp.flowyte.com/mcp", "headers": { "Authorization": "Bearer flowyte_sk_…" } } } ``` The client runs `initialize` to negotiate the protocol, then `tools/list` to discover the tools your key can call. The server is stateless — there is no session id, and it never pushes changes — so each request is authorized on its own. Mint a scoped key in the dashboard's **Developer** page (Settings → API keys) and grant only the scopes you need. Your key alone determines your organization — no other headers. See [Authentication](/get-started/authentication). ## Authentication The gateway accepts only **Flowyte-issued** bearers — a foreign token is rejected `401`. Two credentials work, both resolved by the same resolver `/api/v1` uses: | Credential | Notes | | --------------- | --------------------------------------------------------------------------------------- | | `flowyte_sk_…` | A secret API key from the dashboard; org-scoped, with the scopes you chose at creation. | | `flowyte_oat_…` | An OAuth access token — the same tokens `/api/v1` accepts. | The gateway holds no credentials of its own and never touches the database directly. Because each tool call re-runs your key's scopes and organization isolation, a revoked key stops working instantly and a scope your key lacks fails the same way it would over REST. Full OAuth (authorization-server metadata, dynamic client registration) is on the roadmap. Today, API-key auth is the supported path — a compliant MCP posture. OAuth-capable clients can already discover the seam: an unauthenticated request returns `401` with a `WWW-Authenticate: Bearer resource_metadata="…"` header pointing at `/.well-known/oauth-protected-resource`. ## Tools Tools split into **two capability domains**, and `tools/list` is **scope-filtered** — a key sees only the tools whose scopes it holds. A config-only key never sees the escalation tools, and a connector-only key never sees the control tools. ### Control tools Configure and run an agent. Reads are safe; writes re-run your scopes on the underlying endpoint. | Tool | Scope | What it does | | ---------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------- | | `list_agents` | `agents:read` | List agents (id, name, status). | | `get_agent` | `agents:read` | Full agent config — persona, goals, voice, languages. | | `create_agent` | `agents:write` | Create a new draft agent. | | `configure_agent` | `agents:write` | Partial update of the draft — persona, goals, tone, greeting, voice, languages. | | `add_knowledge` | `knowledge:write` | Add a knowledge source (`url`, `file`, `text`, or `faq`). | | `list_knowledge` | `knowledge:read` | List an agent's knowledge sources and their ingest status. | | `list_skills` | `skills:read` | List an agent's configured skills. | | `configure_skill` | `skills:write` | Create or update one skill. | | `configure_guardrails` | `guardrails:write` | Replace the agent's guardrail-policy set. | | `deploy_chat_widget` | `pubkeys:write` | Mint a publishable key for the [chat widget](/channels/widget). | | `test_agent` | `agents:read` | Run one test turn against the draft. | | `get_prepublish_report` | `agents:read` | Draft readiness report — plus the token `publish_agent` needs. | | `publish_agent` | `agents:write` | Freeze the draft as a new live version (see the confirm gate below). | | `list_conversations` · `get_conversation_receipts` · `get_analytics_summary` | `analytics:read` · `calls:read` | Read past conversations, their audit-event receipts, and analytics rollups. | `test_agent` is **not** a safe read. It runs the draft agent's LLM, and the agent's configured skills may take **real actions** (webhooks, emails, integration calls). Its annotations say so — prefer a test tenant. ### Escalation tools Manage where and when an agent hands a conversation to an external operator agent, and — for connector runtimes — work the live handoff. These tools carry their own scope family, so they only appear for a key that holds it, and they're enabled once you've connected an [AI Harness](/integrations/ai-harness). | Sub-domain | Scopes | Tools | | --------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Destinations (config) | `escalation_destinations:read` / `:write` | `list_escalation_destinations`, `create_escalation_destination`, `update_escalation_destination`, `test_escalation_destination` | | Policy (config) | `escalation_policies:read` / `:write` | `get_escalation_policy`, `configure_escalation_policy` | | Runtime (connector) | `escalations:read` / `:claim` / `:respond` / `:resolve` | `list_escalations`, `get_escalation`, `claim_escalation`, `post_escalation_message`, `resolve_escalation`, `return_escalation`, `request_human_takeover` | A **connector key** holding only `escalations:*` sees exactly the runtime tools — never a config or control tool. See [AI Harnesses](/integrations/ai-harness) for the full setup. ## The publish confirm gate `publish_agent` changes live traffic, so it's a **two-call** operation — never a one-shot: Call `get_prepublish_report`. It compiles the draft, returns what will and won't change, and mints a short-lived `prepublishToken` bound to the exact draft you reviewed. The token expires in 10 minutes and is absent if the draft doesn't compile. Call `publish_agent` with that `prepublish_token`. It verifies the token and that the draft hasn't changed since the report ran, then freezes a new version. Any edit in between (a `configure_agent` or `configure_skill`) invalidates the token — re-run the report and publish with the fresh one. This guarantees you publish exactly what you reviewed. ## A worked example The tool-call flow to list, build, and publish an agent. Each call is a JSON-RPC `tools/call` request to `POST /mcp`; the response carries both a structured `ApiResponse` envelope and the same JSON as text. ```jsonc theme={null} // 1. See what's there { "method": "tools/call", "params": { "name": "list_agents", "arguments": { "status": "published" } } } // 2. Create a draft — capture data.id from the result { "method": "tools/call", "params": { "name": "create_agent", "arguments": { "name": "Front Desk" } } } // 3. Configure it { "method": "tools/call", "params": { "name": "configure_agent", "arguments": { "agent_id": "agt_123", "persona": "A warm, concise front-desk assistant.", "greeting": "Thanks for calling — how can I help?" } } } // 4. Read the pre-publish report to get a token { "method": "tools/call", "params": { "name": "get_prepublish_report", "arguments": { "agent_id": "agt_123" } } } // ← result carries data.prepublishToken // 5. Publish with that exact token { "method": "tools/call", "params": { "name": "publish_agent", "arguments": { "agent_id": "agt_123", "prepublish_token": "eyJvcmdf…" } } } ``` Make a retried create or publish safe by passing a client-generated `_meta.idempotencyKey` on the `tools/call` request — a retry with the same key and arguments replays the original result instead of creating a second agent or version. ## Safety * **Curated, task-level tools** — not a raw endpoint mirror. Each tool has a closed input schema and is written for an operator agent. * **Writes re-run your scopes.** Every call re-enters `/api/v1` with your key, so scope enforcement, organization isolation, and rate limits all apply per call. * **Publish needs the confirm token** from a fresh `get_prepublish_report` — you can't publish blind. * **Everything is audited.** Each tool call lands in your audit trail exactly as the equivalent REST call would. ## Errors Business, validation, and permission failures come back as a `tools/call` **result** with `isError: true` and actionable text (the REST error body) so the model can self-correct — for example, a downstream `403` when your key lacks the underlying scope. JSON-RPC **error objects** are reserved for protocol faults (unknown tool, malformed request, auth). Next: build against the REST API directly in [For AI agents](/get-started/for-ai-agents), or set up an [AI Harness](/integrations/ai-harness) to work escalated conversations. # Add a knowledge base & verify coverage Source: https://docs.flowyte.com/guides/add-knowledge Add a URL or FAQ source, wait for it to index, preview what the agent retrieves, and close the gaps callers expose. **Knowledge** is what your agent answers from. You add sources — a help-center URL, an uploaded document, plain text, or structured FAQs — and the platform indexes them so the agent retrieves the right passage at answer time instead of guessing. This guide adds a source, confirms it indexed, previews retrieval, and reviews the questions callers ask that you don't yet cover. Authenticate with `Authorization: Bearer flowyte_sk_…`. All paths below are relative to `https://builder.flowyte.com/api/v1`. ## What you'll use | Action | Endpoint | Scope | | ------------------ | ------------------------------------------------ | ----------------- | | Add a source | `POST /agents/{agentId}/knowledge/sources` | `knowledge:write` | | Poll ingest status | `GET /agents/{agentId}/knowledge/sources/{id}` | `knowledge:read` | | Preview retrieval | `POST /agents/{agentId}/knowledge/preview` | `knowledge:read` | | Review gaps | `GET /agents/{agentId}/knowledge-gaps` | `analytics:read` | | Curate a gap | `PATCH /agents/{agentId}/knowledge-gaps/{gapId}` | `analytics:write` | A source has a `kind` (`url`, `file`, `text`, or `faq`) and a `label`. Use `url` to crawl a page, `text` to paste content inline, or `file` with a `file_id` from an upload. ```bash curl theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge/sources \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "url", "label": "Help center", "url": "https://acme.com/help" }' ``` ```ts Node theme={null} const res = await fetch( `https://builder.flowyte.com/api/v1/agents/${agentId}/knowledge/sources`, { method: "POST", headers: { Authorization: `Bearer ${process.env.FLOWYTE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ kind: "url", label: "Help center", url: "https://acme.com/help" }), }, ); const { data: source } = await res.json(); ``` ```python Python theme={null} import os, requests r = requests.post( f"https://builder.flowyte.com/api/v1/agents/{agent_id}/knowledge/sources", headers={"Authorization": f"Bearer {os.environ['FLOWYTE_API_KEY']}"}, json={"kind": "url", "label": "Help center", "url": "https://acme.com/help"}, ) source = r.json()["data"] ``` The source is created with `status: "pending"`. Capture `data.id` as `SOURCE_ID`. Ingestion is **asynchronous** — the source moves `pending` → `indexed`. Poll the source every few seconds until `data.status` is `indexed`. Don't rely on it before then. ```bash theme={null} curl https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge/sources/$SOURCE_ID \ -H "Authorization: Bearer $FLOWYTE_API_KEY" # { "data": { "id": "...", "status": "indexed", ... } } ``` Before you trust it, ask what the agent would retrieve for a real question. `preview` returns the matching chunks and a `topScore` — a low score or empty `chunks` means the answer isn't covered yet. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge/preview \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "Do you offer same-day appointments?" }' ``` Pass `sourceIds` to scope the preview to specific sources. Use this to confirm a newly added source actually answers the question you added it for. Once callers start talking to the agent, the questions it couldn't answer surface as **knowledge gaps** — deduped and, above a small volume floor, ranked by how often and how recently they hit. This is the loop that makes the agent better over time. ```bash theme={null} curl "https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge-gaps?status=open" \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` The response's `dataState` is `collecting` until enough calls land, then `ready`. When `ranked` is `true`, each gap carries a `gapId`, `rankScore`, and `classification`. Fix a real gap by adding a source that covers it (back to step 1), then mark the gap handled. You can set a ranked gap to `in_progress`, `dismissed`, or back to `open` — `covered` is set automatically once the live knowledge base answers it. ```bash theme={null} curl -X PATCH https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge-gaps/$GAP_ID \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "in_progress" }' ``` Learn how retrieval fits the rest of the agent in [Knowledge](/concepts/knowledge). # Build an HVAC answering agent Source: https://docs.flowyte.com/guides/build-hvac-agent An answering agent backed by your field-service software — recognizes customers, captures requests, checks job status, and escalates emergencies. This guide builds a working answering agent for a home-services business (HVAC, plumbing, electrical — same shape), backed by your **field-service software**. It recognizes returning customers, captures new jobs straight into your system, tells callers when their tech is coming, and escalates emergencies to a person. Every step is one API call against `https://builder.flowyte.com/api/v1`. Authenticate every request with a secret API key: `Authorization: Bearer flowyte_sk_…`. See [Authentication](/get-started/authentication) to mint one. ## What you'll wire up | Action | Endpoint | Scope | | ----------------------------------- | ------------------------------------------------- | -------------------- | | Create the agent | `POST /agents` | `agents:write` | | Add knowledge | `POST /agents/{id}/knowledge/sources` | `knowledge:write` | | Connect your field-service software | `POST /integrations/{kind}/connect` | `integrations:write` | | Provision its skills | `POST /agents/{id}/integrations/{kind}/provision` | `skills:write` | | Add a transfer skill | `POST /agents/{id}/skills` | `skills:write` | | Publish | `POST /agents/{id}/publish` | `agents:write` | | Simulate | `POST /agents/{id}/simulate` | `agents:read` | | Assign a number | `POST /numbers/{id}/assign` | `numbers:write` | Give it a name and a primary language. Capture `data.id` as your `AGENT_ID`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "name": "Capital Comfort Heating & Air", "primaryLanguage": "en" }' ``` Add your service area, pricing, and FAQs. Ingestion is **asynchronous** — poll the source until `status` is `indexed`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge/sources \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "kind": "url", "label": "Services & pricing", "url": "https://capitalcomfort.com/services" }' ``` Find your provider's `kind` in `GET /integrations/catalog`, then start the connection. For an OAuth provider this returns an authorization link — open it once to approve access to your account. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/$KIND/connect \ -H "Authorization: Bearer $FLOWYTE_API_KEY" # → { "data": { "oauthUrl": "https://provider.example.com/oauth/authorize?…" } } ``` Turn the connected account into skills on the agent in one call. By default it provisions the key actions — recognize a customer (`find_client`), capture a request (`create_lead`), and check a job's status (`get_job_status`). ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/integrations/$KIND/provision \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` Now the agent can greet a returning caller by name, open a request for a new job, and read back a customer's next scheduled visit. For a burst pipe or a no-heat call, add a `transfer` skill that routes to your on-call person. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/skills \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "name": "Transfer to on-call", "description": "Transfer the caller to a person for emergencies like no heat, a gas smell, or a flooding leak.", "skillType": "transfer", "executionConfig": { "destination": "+14155550199" } }' ``` Editing changes a **draft**; phone and chat serve the last **published** version. Publish to freeze a version your channels can serve. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publish \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` Test before a real caller hits it. `simulate` streams over SSE — read it with `fetch()` streaming and stop on `event: done`. ```bash theme={null} curl -N -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/simulate \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "message": "Hi, my furnace stopped working and I need someone out today.", "draftMode": false }' ``` Buy a number and point it at the agent (search → reserve → purchase → assign). See [Buy a number & go live](/guides/buy-a-number). No field-service integration? You can still capture requests by email or webhook and book to a connected calendar — the same shape, different skills. # Buy a number & go live Source: https://docs.flowyte.com/guides/buy-a-number Search available numbers, reserve one, purchase it from your wallet, assign it to an agent, and route real calls. To take real phone calls, your agent needs a phone number. This guide searches the carrier's inventory, holds a number so it can't be sniped, purchases it from your prepaid wallet, and points it at a published agent. All paths are relative to `https://builder.flowyte.com/api/v1`. Authenticate with `Authorization: Bearer flowyte_sk_…`. Purchasing **debits your prepaid wallet** — top it up first if needed. ## What you'll use | Action | Endpoint | Scope | | ------------------ | --------------------------- | --------------- | | Search inventory | `GET /numbers/search` | `numbers:read` | | Reserve (hold) | `POST /numbers/reserve` | `numbers:write` | | Purchase | `POST /numbers/purchase` | `numbers:write` | | Assign to an agent | `POST /numbers/{id}/assign` | `numbers:write` | | Check wallet | `GET /billing/wallet` | `billing:read` | Filter by area code, city, state, capabilities, or a vanity pattern. `bestEffort` (default `true`) widens a too-narrow filter to nearby matches — set it `false` for strict last-four or vanity matching. Each result carries an `e164` and a price. ```bash curl theme={null} curl "https://builder.flowyte.com/api/v1/numbers/search?areaCode=415&numberType=local&features=voice" \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` ```ts Node theme={null} const params = new URLSearchParams({ areaCode: "415", numberType: "local", features: "voice" }); const res = await fetch(`https://builder.flowyte.com/api/v1/numbers/search?${params}`, { headers: { Authorization: `Bearer ${process.env.FLOWYTE_API_KEY}` }, }); const { data: numbers } = await res.json(); ``` ```python Python theme={null} import os, requests r = requests.get( "https://builder.flowyte.com/api/v1/numbers/search", headers={"Authorization": f"Bearer {os.environ['FLOWYTE_API_KEY']}"}, params={"areaCode": "415", "numberType": "local", "features": "voice"}, ) numbers = r.json()["data"] ``` Pick an `e164` from the results, e.g. `+14155551234`. Hold the number (\~30 minutes) so it can't be purchased out from under you while you confirm. Reserving does **not** charge your wallet. Capture the returned reservation `id`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/numbers/reserve \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "e164": "+14155551234" }' ``` Reserving is optional but recommended for interactive flows. A `409` means the number can no longer be held — search again and pick another. Purchase debits your prepaid wallet. Pass the `reservationId` from the previous step so the order can't be sniped. A `402` means insufficient balance — top up with `POST /billing/wallet/topup`, then retry. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/numbers/purchase \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "e164": "+14155551234", "reservationId": "'$RESERVATION_ID'" }' ``` The response is the purchased number. Capture `data.id` as `NUMBER_ID`. Already own a number elsewhere? Use `POST /numbers/import` instead — it routes a number on your carrier account to this deployment with no wallet charge. Point the number at your agent. Inbound calls now route to the agent's **published** version. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/numbers/$NUMBER_ID/assign \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "agentId": "'$AGENT_ID'" }' ``` Calls serve the last **published** version of the agent — not your draft. If you haven't published since your latest edits, publish now, then dial the number. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publish \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` An assigned number routes to the agent's published version. If the agent has never been published, callers reach nothing — publish before you dial. ## Managing numbers later To stop using a number without losing it, `DELETE /numbers/{id}/assign` moves it back to your pool (you keep owning and paying for it). `DELETE /numbers/{id}` permanently releases it to the carrier. SMS on a number requires per-organization 10DLC brand and campaign registration before sends are allowed. # Verify a caller before disclosure Source: https://docs.flowyte.com/guides/caller-verification Make the agent prove a caller's identity before it reads back account details or takes a sensitive action. When an agent can look up orders, balances, or appointments, you don't want it reading those back to *anyone* who calls. **Caller verification** is a deterministic guardrail: the agent must verify identity before it discloses sensitive fields or runs a write action. Unlike a prompt instruction, this is enforced, not suggested. All paths are relative to `https://builder.flowyte.com/api/v1`. Authenticate with `Authorization: Bearer flowyte_sk_…`. Both endpoints below are **replace** (`PUT`) — send the full list each time. It comes together in three parts: 1. A **verifier skill** that checks the caller's answers against your system of record. 2. A **caller-verification config** that ties a verification method to that verifier. 3. A **guardrail** plus per-skill flags that block disclosure until the caller is verified. ## What you'll use | Action | Endpoint | Scope | | ---------------------------------- | ---------------------------------------------- | -------------------------------------- | | Create the verifier skill | `POST /agents/{id}/skills` | `skills:write` | | Read / replace verification config | `GET` · `PUT /agents/{id}/caller-verification` | `guardrails:read` · `guardrails:write` | | Read / replace guardrail policies | `GET` · `PUT /agents/{id}/guardrails` | `guardrails:read` · `guardrails:write` | | Gate a disclosure skill | `PATCH /agents/{id}/skills/{id}` | `skills:write` | ## Verification methods | `method` | The caller proves identity with | | ----------------- | ------------------------------------------ | | `dob_postcode` | date of birth + postcode | | `account_pin` | an account PIN | | `otp_sms` | a one-time code sent by text | | `knowledge_based` | answers only the account holder would know | `otp_sms` sends a text, which requires per-organization 10DLC brand and campaign registration before sends are allowed. The verifier is a skill (commonly `db_query` or `http_webhook`) that takes the caller's claimed identity plus a secret and returns whether they match. Capture its `id` — you'll reference it as `verifierSkillId`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/skills \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Verify identity", "description": "Confirm the caller by date of birth and postcode.", "skillType": "http_webhook", "executionConfig": { "url": "https://api.example.com/verify", "method": "POST" } }' ``` Tie a method to the verifier. `tokenTtlSeconds` controls how long a verification stays valid on the call; `maxAttempts` caps retries; `stepUpFor` names actions that demand a re-check even after basic verification. ```bash theme={null} curl -X PUT https://builder.flowyte.com/api/v1/agents/$AGENT_ID/caller-verification \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '[ { "method": "dob_postcode", "requiredFields": ["date_of_birth", "postcode"], "verifierSkillId": "'$VERIFIER_SKILL_ID'", "tokenTtlSeconds": 600, "maxAttempts": 3, "stepUpFor": ["refund"] } ]' ``` Add a `verify_before_disclose` policy, then mark every skill that returns sensitive data with `requiresVerifiedIdentity: true`. Use `verificationLevel` (`basic` or `step_up`) for higher-risk actions, and `allowedFields` to cap what may be read back. ```bash Guardrail theme={null} curl -X PUT https://builder.flowyte.com/api/v1/agents/$AGENT_ID/guardrails \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '[ { "policyType": "verify_before_disclose", "appliesToSkillType": "shopify", "isEnabled": true } ]' ``` ```bash Gate a skill theme={null} curl -X PATCH https://builder.flowyte.com/api/v1/agents/$AGENT_ID/skills/$SKILL_ID \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "requiresVerifiedIdentity": true, "verificationLevel": "basic" }' ``` Verification is enforced at runtime, but it only protects skills you actually gate. Audit every skill that returns account data and set `requiresVerifiedIdentity`. Changes apply on **publish**. Related: [Guardrails](/concepts/guardrails) · [Skills](/concepts/skills) · [Draft vs published](/get-started/draft-vs-published) # Connect your AI harness Source: https://docs.flowyte.com/guides/connect-ai-harness Wire an external agent runtime — Hermes, OpenClaw, or your own — to Flowyte end to end: endpoint URL in, connector key and signing secret out, then claim, reply, resolve, and start outbound SMS threads. An **AI Harness** is your own agent runtime taking over a live Flowyte conversation. This guide is the one-time setup, start to finish: what you hand Flowyte, what you get back, how to prove the connection works, and how to run the loop afterwards. Follow it once and your harness is live. Two things move in opposite directions, and it helps to hold them apart from the start: **Signed webhooks** to your `endpoint_url`. You verify them with the **signing secret** (`whsec_…`). This is the doorbell. **REST calls** to the Flowyte API. You authenticate with a scoped **connector key** (`flowyte_sk_…`). This is where the truth lives. Two different secrets, two different jobs. The signing secret only ever **verifies** inbound webhooks — it is not an API credential. The connector key only ever **authenticates** your outbound REST calls — it never appears in a webhook. Never swap them. ## Before you start * **The API base URL.** Every path in this guide — `/escalations`, `/escalations/{id}/messages` — is relative to `https://builder.flowyte.com/api/v1`. So `POST /escalations` is `POST https://builder.flowyte.com/api/v1/escalations`. The MCP gateway, if you drive Flowyte that way instead, is a separate host: `https://mcp.flowyte.com/mcp`. * An **AI Harness runtime** you control, reachable at an **absolute `https://` URL**. Flowyte SSRF-checks it: private, loopback and link-local targets are refused at registration *and* re-checked at send time. * A Flowyte organization with **at least one agent**. * For the `sms` channel: the organization's **A2P 10DLC (TCR) registration** must be complete — the same registration its inbound number already needs. See [SMS](/channels/sms). * **Starter or higher** to run real conversations. Setup and the handshake test work on any plan, so you can wire everything up before upgrading. ## 1. Connect the harness integration In the dashboard's **Integrations** page, connect your AI Harness provider — `hermes` or `openclaw` (or `POST /integrations/{kind}/connect`). This is enablement only: it stores no credential, it just marks the provider connected for the organization. Nothing else on this page works until you do. Every escalation surface re-checks this server-side, so disconnecting the integration later immediately stops new conversations reaching your harness — it does not merely hide a button. ## 2. Give Flowyte your endpoint URL This is the one thing **you** hand Flowyte. Register a **destination** — the record that says *where* conversations go and *which channels* you handle. In **Integrations → your harness → Add destination**, give it a name, your `endpoint_url`, and tick the channels you handle (**Chat**, **SMS**, or both). Over the API this is `POST /escalation-destinations` with `escalation_destinations:write`. Optional knobs: **context profile** (which handoff bundle you receive), **transcript window** (how many recent turns come with it, default 20), **claim window** (how long Flowyte waits for you to claim before falling back — default 120 seconds), and a fallback route. The create response carries `signingSecret` (`whsec_…`) **exactly once**. It is never returned by any later read. Store it in your secret manager before you close the drawer; if you lose it, rotate it (`POST /escalation-destinations/{id}/rotate-secret`) rather than hunting for it. The destination starts `unverified`. The handshake test in step 5 is the **only** path to `active` — no API call can set it directly. Ticking **SMS** requires the organization's 10DLC/TCR campaign to be active, or the destination refuses with `422 sms_registration_required`. Chat has no such requirement. ## 3. Mint a connector key In the dashboard's **Developer** page, mint a secret key (`flowyte_sk_…`) for your harness. It is shown once. Every REST call carries it: ``` Authorization: Bearer flowyte_sk_… ``` Grant only the scopes you need: | Scope | Grants | | ---------------------- | --------------------------------------------------------------------------------------------------------------- | | `escalations:read` | `GET /escalations`, `GET /escalations/{id}` (the context package, owner only), `GET /escalations/{id}/messages` | | `escalations:claim` | `POST /escalations/{id}/claim`, `/heartbeat` | | `escalations:respond` | `POST /escalations/{id}/messages`, `/typing` | | `escalations:resolve` | `POST /escalations/{id}/resolve`, `/return`, `/request-human` | | `escalations:initiate` | `POST /escalations` — start an outbound SMS thread (step 7). Only take this one if you need it. | The key is organization-scoped: it only ever sees its own tenant's sessions, and a cross-tenant call fails closed as `404`. These scopes are deliberately separate from `escalation_destinations:*` — a connector key can work conversations but cannot edit the destinations or agents it works for. ## 4. Verify webhook signatures Flowyte signs every delivery. **Verify before you do any work** — parse nothing, queue nothing, and above all claim nothing on an unverified body. | Header | Meaning | | ------------------------ | ------------------------------------------------------------------------------- | | `Flowyte-Signature` | `hex(HMAC_SHA256(secret, "{Flowyte-Delivery}.{Flowyte-Timestamp}.{rawBody}"))` | | `Flowyte-Signature-Prev` | Present **only** during a 24-hour secret rotation; same scheme, previous secret | | `Flowyte-Timestamp` | Unix seconds, as a decimal string | | `Flowyte-Delivery` | The delivery id (also `id` in the body) — your **dedupe key** | | `Flowyte-Event` | The event type | ```js theme={null} import crypto from "node:crypto"; // rawBody MUST be the exact bytes received. Re-serializing the JSON changes // whitespace and key order, and the signature will never match again. function verify(headers, rawBody, secrets) { const ts = headers["flowyte-timestamp"]; const delivery = headers["flowyte-delivery"]; if (!ts || !delivery) return false; // Replay window: reject anything more than 300s away from now. const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(ts)); if (!Number.isFinite(skew) || skew > 300) return false; const signed = `${delivery}.${ts}.${rawBody}`; const sent = [headers["flowyte-signature"], headers["flowyte-signature-prev"]].filter(Boolean); // Accept if ANY secret you hold verifies EITHER header. That pairing is what // keeps you working through a rotation you have not picked up yet. return secrets.some((secret) => { const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex"); return sent.some((sig) => { const a = Buffer.from(expected, "utf8"); const b = Buffer.from(sig, "utf8"); return a.length === b.length && crypto.timingSafeEqual(a, b); // never === }); }); } ``` A bad signature or a stale timestamp is a `401` from you, and no work. **Rotation, concretely.** After `rotate-secret`, Flowyte sends `Flowyte-Signature` (new secret) **and** `Flowyte-Signature-Prev` (previous secret) for 24 hours. A connector still holding only the old secret keeps verifying, because its secret matches the `-Prev` header. Pick up the new secret inside that window; after it, only the new secret verifies. **Answer fast, then work.** Flowyte counts any `2xx` as delivered and gives you **10 seconds** total. Acknowledge immediately and process asynchronously. A non-`2xx` or a timeout is retried up to **6 attempts** (immediately, then 30s, 1m, 2m, 4m, 8m) and then dead-lettered. Delivery is **at-least-once**, so **dedupe on `Flowyte-Delivery`**. Webhooks are the **doorbell, not the mailbox.** Never reconstruct a conversation from webhook bodies — read it from `GET /escalations/{id}/messages`, which is ordered and authoritative. ## 5. Pass the handshake test The go-live gate. In **Integrations → your harness → Handshake test**, hit **Run test** (or `POST /escalation-destinations/{id}/test`). Flowyte opens a **sandbox** escalation on a synthetic conversation, delivers a signed `escalation.test` webhook, and then waits for your connector to complete the loop: Exactly as in step 4. Treat `escalation.test` as **identical** to `escalation.requested` — same code path, no special case. `POST /escalations/{id}/claim`. Retry on `409` and `404`: a redelivered webhook can transiently conflict, and the session may not be visible for a beat right after it is created. Claiming is idempotent for your own key. `POST /escalations/{id}/messages` with a `client_message_id` and `text`. `POST /escalations/{id}/resolve`. The response is a step ledger — `{ webhook_delivered, claimed, message_posted, resolved }` — plus a verdict and, on failure, a hint naming the first step you missed (for example *"Connector claimed the session but posted no message within 120s. Check the escalations:respond scope on the connector key."*). **Both a pass and a fail come back as HTTP 200** — read `result`, not the status code. Only a green run flips the destination to `active`. A test message reaches no customer and is never billed; your connector does not need to know that, and should not branch on `test`. Flowyte ships `tools/mock-connector` (Go, stdlib only) as a working reference for this whole loop, with a `-fail-step` flag to reproduce each failure hint. ## 6. Route real conversations to it Set the agent's escalation policy — `PUT /agents/{id}/escalation-policy`, or the **Escalation policy** page — as an ordered list of rules: *when* to escalate and *where* to route. Conditions are deterministic (an explicit request for a person, a human request, business hours, channel, a failed tool or knowledge lookup); routes are `external:`, an email fallback, or a queue. First match wins. The policy edits the agent **draft** — [publish](/get-started/draft-vs-published) it so live traffic uses it. From then on, the loop is: 1. **`escalation.requested`** arrives. Verify, dedupe, claim. 2. **Claim** returns the context package: the recent transcript turns (each with its spine `seq`), the escalation reason, verified identifiers (on SMS, the customer's phone), and completed or failed tool actions. It is rebuilt at claim time, so you start current. 3. **Reply** with `POST /escalations/{id}/messages`. `client_message_id` is your idempotency key — a replay returns the original receipt instead of double-sending. 4. **Sync** with `GET /escalations/{id}/messages?after_seq=N` and process strictly increasing `seq`. 5. **Finish** with `/resolve`, `/return` (hand back to the Flowyte AI), or `/request-human`. Claiming starts an **activity-based lease** — about 2 minutes on chat, 4 hours on SMS — and *any* owner action extends it, including a `/messages` read. Send `/heartbeat` if you go quiet but want to keep the thread. If the lease expires the thread returns to the AI and you get `409 lease_expired`; re-claim to continue. ## 7. Start an outbound SMS thread (optional) If your harness needs to open the conversation rather than wait for one, `POST /escalations` starts a new SMS thread and sends its first message. The session comes back **already owned by your key** — no claim, no SLA race — and the customer's replies route to you exactly like any other escalation. ```jsonc theme={null} POST /escalations // scope: escalations:initiate { "agentId": "agt_…", "destinationId": "dest_…", "to": "+14155551234", "text": "Hi Dana — following up on the quote you asked about yesterday.", "client_message_id": "6f1c2d3e-4a5b-6789-abcd-ef0123456789" } ``` Four things decide whether this works, and they catch most first attempts: * **Consent.** The recipient must have a recorded basis. **An inbound text is consent to text back** — a customer who has ever texted the business is already `conversational` and needs nothing extra. A number that has never texted in is refused `422 consent_required`. * **It must be switched on.** Outbound initiation is off by default and enabled per organization by Flowyte, on top of the deployment-level Agent Bridge switch. Otherwise `403 harness_outbound_disabled` or `503 bridge_disabled`. * **There is no `from`.** The sending number is derived from the agent's SMS-active number, and the agent must have one (`422 no_sms_number`). * **Send the message only.** The brand identity and the `Msg & data rates may apply. Reply HELP for help, STOP to opt out.` block are appended for you on a first text. Writing your own ships it twice. Quiet hours also apply differently here than to a reply: a first text is business-initiated, so outside the recipient's legal window it is **refused** `409 quiet_hours` with a `nextOpen` instant to retry after — never silently queued. **`409 send_state_unknown` is not a retry signal.** It means the send outcome is genuinely unknown and the message **may already have been delivered**. Replay the **same** `client_message_id` (which resolves to the original answer and never sends a second text) or re-read the thread. A fresh id there texts somebody twice. The full contract — every error code, the volume cap, the audited consent bypass, a worked end-to-end example — is in the [connector reference](/integrations/ai-harness-connector#5-start-an-outbound-sms-thread). ## When it does not work | Symptom | Cause | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Destination will not leave `unverified` | The handshake never went green. Read the `hint` on the last run — it names the first missed step. | | `401` on your endpoint, nothing else happens | Your verification is failing. Almost always a re-serialized body instead of the raw bytes, or clock skew past the 300-second window. | | `403 insufficient_scope` | The connector key is missing the scope for that call. Scopes are per-call, not per-resource. | | `403 plan_required` | The organization is on PAYG. Harness conversations need Starter or higher; only the handshake test is exempt. | | `409 claim_conflict` right after a webhook | A redelivery or a race. Back off and retry — claiming is idempotent for your key. | | `409 destination_unavailable` | The AI Harness integration was disconnected, or the destination is no longer active. | | `409 lease_expired` | You went quiet past the lease. Re-claim, and heartbeat next time. | | Conversation goes quiet and the AI answers | Your lease expired and the thread returned to the AI, or you called `/return`. | | `422 sms_registration_required` on the destination | The organization's 10DLC/TCR campaign is not active yet. | ## Next * [Connector reference](/integrations/ai-harness-connector) — the full contract you keep open while building. * [AI Harnesses](/integrations/ai-harness) — the operator-side view of the same setup. * [MCP gateway](/get-started/mcp-gateway) — the same surface as agent tools, if your harness speaks MCP rather than REST. # Connect a SQL database Source: https://docs.flowyte.com/guides/connect-sql-database Give an agent live read (and scoped write) access to your own Postgres or MySQL database — safely, one column at a time. This guide connects a **PostgreSQL** or **MySQL** database to an agent so it can answer from live records on a call — for example "when's my appointment?" — using a least-privilege user, data scoping, and a bound read query. Every step is one API call against `https://builder.flowyte.com/api/v1`. Authenticate every request with a secret API key: `Authorization: Bearer flowyte_sk_…`. See [Authentication](/get-started/authentication) to mint one. Below, `KIND` is `postgres` or `mysql`. ## What you'll wire up | Action | Endpoint | Scope | | ------------------------- | ------------------------------------------------ | -------------------- | | Generate a scoped DB user | `GET /integrations/{kind}/sql/scripts` | `integrations:read` | | Test the connection | `POST /integrations/{kind}/sql/test` | `integrations:write` | | Connect | `POST /integrations/{kind}/connect` | `integrations:write` | | Discover the schema | `POST /integrations/{kind}/discover` | `integrations:write` | | Scope out sensitive data | `PATCH /integrations/{kind}/scoping` | `integrations:write` | | Bind a read as a skill | `POST /agents/{id}/integrations/{kind}/bindings` | `skills:write` | | Publish | `POST /agents/{id}/publish` | `agents:write` | Generate the setup SQL, then run it on your database as an admin. Never connect Flowyte with your admin credentials — create a scoped user instead. ```bash theme={null} curl "https://builder.flowyte.com/api/v1/integrations/postgres/sql/scripts?database=appdb" \ -H "Authorization: Bearer $FLOWYTE_API_KEY" # → { "data": { "readOnly": { "sql": "CREATE ROLE flowyte_ro …" }, "generatedPassword": "…" } } ``` For read-only lookups, run the `readOnly` script. Only add the scoped `write` script (pass `?tables=appointments,requests`) if the agent needs to insert or update rows. Validate before you store anything. The check runs reachability, TLS, auth, a read probe, a read-only-session proof, and a latency verdict, and returns an overall `ok`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/postgres/sql/test \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "credentials": { "host": "db.example.com", "port": "5432", "database": "appdb", "user": "flowyte_ro", "password": "…", "sslmode": "verify-full" } }' # → { "data": { "ok": true, "checks": [ … ] } } ``` `sslmode` must be `require`, `verify-ca`, or `verify-full` (`disable` is refused). A host that resolves to a private, loopback, or metadata address is refused too. Pass the same credentials to the connect endpoint. They're encrypted at rest and never returned. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/postgres/connect \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "credentials": { "host": "db.example.com", "port": "5432", "database": "appdb", "user": "flowyte_ro", "password": "…", "sslmode": "verify-full" } }' # → { "data": { "status": "connected" } } ``` Introspect the database, then block tables or columns the agent should never see — they vanish from the schema browser and are refused at bind time. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/postgres/discover \ -H "Authorization: Bearer $FLOWYTE_API_KEY" curl -X PATCH https://builder.flowyte.com/api/v1/integrations/postgres/scoping \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "blockedColumns": { "customers": ["ssn", "card_last4"] } }' ``` Map a read onto a skill: pick the operation, map the caller's input to its filter, and project the columns you want back. It compiles to a frozen, parameterized query — the agent supplies the parameter and never sees raw SQL. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/integrations/postgres/bindings \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "toolName": "Look up an appointment", "description": "Find the caller'\''s appointment by phone and read back the time and status.", "operation": "appointments.read", "inputs": [ { "param": "caller_phone", "arg": "phone", "required": true } ], "projection": [ { "path": ["appointment", "starts_at"], "leaf": "appointment_time" }, { "path": ["appointment", "status"], "leaf": "appointment_status" } ], "enabled": true }' ``` Don't want to hand-map? `POST /agents/{id}/integrations/postgres/bindings/auto` takes a plain-language goal ("look up the caller's next appointment by phone") and the AI assistant proposes the binding, saved as a draft to review. Binding edits the **draft**. Publish so live callers get real data, then try it in the simulator. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publish \ -H "Authorization: Bearer $FLOWYTE_API_KEY" curl -N -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/simulate \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "message": "Hi, can you tell me when my appointment is?", "draftMode": false }' ``` ## Safety model * **Least privilege** — you connect a scoped user, not an admin. Reads are read-only; writes are limited to the tables you granted. * **Data scoping** — blocked tables and columns never reach the schema browser, the AI assistant, or a binding. * **Frozen queries** — a binding compiles to one parameterized statement. The agent passes parameters; it can't run arbitrary SQL, and writes land disabled until you review them. See the [SQL database connector](/integrations/sql-database) for the full endpoint reference. # Connect Zapier Source: https://docs.flowyte.com/guides/connect-zapier Bridge the 8,000+ apps Zapier supports to your agent — recognize callers, take live actions on a call, and log calls back to your tools, all on your own Zapier account. Zapier is Flowyte's **universal bridge** to the thousands of apps that don't have a built-in Flowyte connector. If your app is HubSpot, Jobber, Shopify, Google Sheets, or a SQL database, use its [built-in connector](/integrations/overview) instead — it's faster and deeper. For everything else, Zapier lets your agent **recognize your customers**, **take live actions during a call**, and **log calls back into your tools** — all through your own Zapier account. **You pay Zapier for tasks; Flowyte doesn't charge extra.** Everything here runs on your Zapier plan at Zapier's normal task pricing. Flowyte adds no fee for Zapier calls or for storing your synced records. There are three things you can set up — do the one(s) you need: Sync your customers in so the agent greets them by name and looks them up instantly — no per-call cost. **Start here.** Book an appointment or update a ticket mid-call through a Zapier MCP connection. Log the call or notify your team the moment a call ends. ## Before you start * A **Zapier account** — any paid plan for real volume; the free plan is fine to test. * Your Flowyte account with at least one agent. * For the triggers/actions in sections 1 and 3, the **Flowyte Zapier app**. During the beta it's invite-only — use the invite link your Flowyte contact sent you to add "Flowyte" to your Zapier account. (Once it's public it'll be in Zapier's app directory.) Section 2 (live actions) needs no app — it uses your own MCP endpoint. ## 1. Recognize callers (sync your customers in) This is the most valuable setup and costs the least. A Zap copies your customer records into Flowyte; the agent reads them **locally** at call time — instant, and **no Zapier task per call**. In Zapier, create a Zap. **Trigger:** your app (e.g. "New or Updated Contact" in your CRM). **Action:** **Flowyte → Create or Update Record**. Connect your Flowyte account when prompted. Pick the **record type** (e.g. `customer`) and map your app's fields onto it — at minimum a **phone number** (how the agent matches a caller) and a **name**. Add anything the agent should know (account status, last order, …). Turn the Zap on. A fresh Flowyte store is **empty**, so until you load your *existing* records the agent recognizes nobody and looks broken. Load them once with **Zapier Transfer** (bulk-run your existing records through the same Create/Update Record action) or a **CSV import** into Flowyte. **How the agent uses it:** on a call it matches the caller's number and greets them by name. Sensitive details (balance, address, order history) are only read out **after the caller verifies their identity** — caller ID alone is spoofable, so Flowyte protects those fields by design. Leave the Zap on; if you turn it off, the agent works from stale data. ## 2. Take live actions during a call (Zapier MCP) For things the agent must do **live** — "book me in now", "update my open ticket", "check today's availability" — connect a **Zapier MCP** endpoint. Flowyte calls your Zapier action mid-call and waits for the result: a couple of seconds for a **classic** server's direct action call, more like **10+ seconds** for an **agentic** server (Zapier shapes the response with its own AI layer) — the agent covers the pause naturally either way. For a pure **write** that nothing needs to read back — logging the call, creating a lead, tagging a contact — you can send it **in the background** instead, so the caller isn't kept waiting on it at all; see [Send writes in the background](#send-writes-in-the-background) below. ### Set up your Zapier MCP server At **[mcp.zapier.com](https://mcp.zapier.com)**, create an MCP server and enable the exact actions you want the agent to run (e.g. "Create Appointment"). See [Zapier's MCP docs](https://docs.zapier.com/mcp) for the current setup steps — their UI evolves. Any server works, including the **dynamic-discovery ("agentic")** servers Zapier now creates by default — you pick the specific action in Flowyte next, and Flowyte resolves it into a concrete, pinnable schema. On the server's **Connect** step, copy the **endpoint URL** (it contains a token). Keep it secret — it's the credential Flowyte uses; rotating it in Zapier revokes Flowyte's access. ### Connect it in Flowyte, resolve the action, and freeze a skill In Flowyte, open **Integrations → Zapier → Connect** and paste your MCP endpoint URL. Flowyte lists the actions you equipped. (API: `POST /integrations/zapier/connect`, then `GET /integrations/zapier/tools`.) Pick the specific action you want (e.g. "Create Spreadsheet Row"). Flowyte **resolves** it to a concrete, closed schema of typed fields — this works the same whether your server is classic or agentic; you never freeze a generic "run any action" tool. **Actions with dynamic fields** (a spreadsheet's columns, a CRM's custom fields) only expose their concrete fields once their parent is pinned — the specific spreadsheet, then its worksheet, then its columns resolve. Flowyte prompts you to pin each parent in turn (spreadsheet → worksheet → column) — pick a value and it re-resolves the next level down. Leave a parent unpinned and its dynamic children can't be frozen. For a read action, **Probe** it — Flowyte runs it once against a real record so you can choose exactly which response fields the agent may read back (deny-by-default). Write actions skip live probing — Flowyte already knows their response shape. Probing **runs the real action** — it may create or send something. Only probe an action you're comfortable executing once. Write the **Instructions** (what the action should do) and the **Output** — the Output must ask for the result **as strict JSON**, since that's what Flowyte parses to hand fields back to the agent. Then **freeze** the action into a skill. **Every action is treated as a write by default**: the agent **confirms with the caller before running it** and runs one at a time. If an action genuinely changes nothing, mark it a read — you'll write a short reason, recorded for audit. If it's a write the agent doesn't need to hear the result of, turn on **background sending** (below) so the call doesn't wait on it. Or let **Flowyte Assist** do the resolve-and-freeze for you — just describe what you want. ### Send writes in the background Not every write needs the agent to wait on it. If the action is a pure **send** — log the call in your CRM, create a lead, tag a contact, kick off a downstream Zap — nothing on the call reads its result back, so there's no reason to hold the turn open for it. Turn on **`fireAndForget`** when you freeze that skill and: * the agent still **confirms with the caller first**, exactly as before — background sending only changes what happens *after* the caller says yes; * on the confirmed call, Flowyte hands the same pinned arguments to a background queue and tells the caller it's **done** right away, instead of blocking the turn on the live Zapier call; * a background worker completes the real send moments later, off the call, with automatic retries and de-duplication (a retried confirmation is never sent twice); * if the send eventually fails, the caller was already told "done" and is gone by then — the failure surfaces to **you**, not them: it shows up as a failed step on the call's Observe receipt and triggers an internal alert so you can follow up (reconnect, re-freeze, or resend manually in Zapier). Only turn this on for actions where a later failure is genuinely fine to catch after the fact — not for anything the caller is relying on hearing confirmed in the moment. It's **write-only**: Flowyte won't apply it to anything marked or resolved as a read, since a read has nothing to send in the background. It's off by default; set `"fireAndForget": true` per skill when you freeze it. Full field reference: [Zapier connector reference](/integrations/zapier). **Cost:** each live action is about **2 Zapier tasks** on your plan, whether it runs live or in the background — background sending changes only *when* the action runs, not what it costs. Use live actions (with or without background sending) for things that must happen on the call; use the **records sync** (section 1) for anything you can only look up. ## 3. Send call events to your apps Log calls or notify your team automatically when a call finishes. Choose an event: **Call completed** (fires when the call ends — caller, duration, outcome), **Call analyzed** (a few minutes later — outcome, sentiment, and a **summary**), or **Call transferred**. A CRM note, a Slack message, a spreadsheet row. Turn the Zap on. **Privacy:** these events include the caller's **phone number**, and "Call analyzed" includes a **call summary** — personal data. Send it only to apps and people who should see it. ## Which Zapier plan do I need? Everything runs on your Zapier plan, billed in **tasks**. The driver is usually the live actions: ``` Monthly live-action tasks ≈ (calls/month) × (live actions/call) × 2 ``` **Example:** 1,000 calls/month with \~1 live action each → `~2,000 tasks/month` for live actions; add your record syncs (\~1 task each) and call-log Zaps (\~1 task per call) → pick a Zapier plan comfortably above your total. Prefer the **records sync** for lookups (no per-call task) and reserve **live actions** for what must happen on the call. Zapier's tiers change — check [zapier.com/pricing](https://zapier.com/pricing). ## Troubleshooting Did you **backfill**? A new store is empty until you load existing customers (Zapier Transfer or CSV). Is the sync Zap **on**? Check "last synced" in Flowyte — if it's old, the Zap is off or out of tasks. It greets by name but won't read a balance/address → *by design*: sensitive fields need the caller to verify first. Pin its parent first — the specific spreadsheet, then its worksheet — and re-resolve the action; its columns/fields materialize once the value above them is pinned. The integration card shows a **degraded/down** reason: **out of tasks** (your Zapier plan ran out — the agent says a graceful line; upgrade Zapier), **needs reconnect** (your MCP token rotated — reconnect with the fresh URL), or **timed out** (usually transient). Bulk/"send to everyone" actions are blocked on purpose — pin the exact recipient in Zapier. If you **rebuilt** the Zap, relink from the new one. If it "just stopped," your catch URL was failing and Flowyte disabled it after retries — fix the Zap and re-enable it in Flowyte. If Flowyte shows the event **delivered** but nothing happened, check the Zap's history in Zapier. The caller already heard "done," so Flowyte doesn't call them back to say otherwise. Check the call's Observe receipt for a failed step, and look for the internal alert email — both name the skill and the reason. Common causes: your Zapier token rotated (reconnect), the account ran out of tasks, or (for a dynamic-field action) its columns changed shape since you froze it — re-freeze after a column change. The write is never silently retried into a second execution; a failed send stays failed until you act. ## Safety model * **Scoped access** — Flowyte only sees the actions *you* equipped on your server. Your token is the credential; revoke it in Zapier and access ends. * **Frozen skills** — a skill pins its arguments, allow-lists exactly which output fields the agent may read (deny-by-default), and treats every action as a **confirm-gated write** unless a human attests it's read-only. * **Background sending never skips the confirm gate** — `fireAndForget` only changes what happens *after* the caller says yes to a write; it still confirms first, and a send that ultimately fails is always reported to you (Observe + an internal alert), never silently dropped. * **Your bill, your data** — Zapier calls run on your Zapier plan; Flowyte adds no markup. See the [Zapier connector reference](/integrations/zapier) for the full endpoint list. # Embed the chat widget Source: https://docs.flowyte.com/guides/embed-chat-widget Mint a browser-safe publishable key, drop in the loader snippet, and lock it to the origins you control. The chat widget puts a published agent on your website. It runs from a **publishable key** (`flowyte_pk_…`) — a browser-safe key scoped to a single agent, locked to an origin allowlist, and limited to the `chat:public` capability so it can never read tenant data. You mint the key with your secret key, then paste a loader snippet. All paths are relative to `https://builder.flowyte.com/api/v1`. Mint and manage keys server-side with your secret key (`flowyte_sk_…`). The publishable key is safe to ship in a web page; **never** put a secret key in client-side code. ## What you'll use | Action | Endpoint | Scope | | ---------------------- | ------------------------------------------------ | --------------- | | Mint a publishable key | `POST /agents/{id}/publishable-keys` | `pubkeys:write` | | Get the embed snippet | `GET /agents/{id}/widget/embed` | `widgets:read` | | Customize theme / copy | `PUT /agents/{id}/widget` | `widgets:write` | | Rotate the key | `POST /agents/{id}/publishable-keys/{id}/rotate` | `pubkeys:write` | | Revoke the key | `DELETE /agents/{id}/publishable-keys/{id}` | `pubkeys:write` | `allowedOrigins` is the allowlist of sites that may use this key. Match each origin exactly — scheme + host (+ port). Include both the apex and `www` if you serve both. Set `rateLimitRpm` to bound abuse. The full key is returned in `keyPublic` (it's public, so it isn't show-once). ```bash curl theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publishable-keys \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Marketing site", "allowedOrigins": ["https://www.example.com", "https://example.com"], "env": "live", "rateLimitRpm": 60 }' ``` ```ts Node theme={null} const res = await fetch( `https://builder.flowyte.com/api/v1/agents/${agentId}/publishable-keys`, { method: "POST", headers: { Authorization: `Bearer ${process.env.FLOWYTE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Marketing site", allowedOrigins: ["https://www.example.com", "https://example.com"], env: "live", rateLimitRpm: 60, }), }, ); const { data: key } = await res.json(); // key.keyPublic = flowyte_pk_live_… ``` Pass the key's `publishableKeyId` to get the loader. The response carries `scriptSnippet` and the `publishableKey` to drop into your page. ```bash theme={null} curl "https://builder.flowyte.com/api/v1/agents/$AGENT_ID/widget/embed?publishableKeyId=$PUBLISHABLE_KEY_ID" \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` Copy the exact `scriptSnippet` returned above into your page, just before ``. It looks like a single async loader tag carrying your publishable key: ```html theme={null} ``` The widget loads its theme and copy from a public bootstrap call — no secrets in the browser. Tune the look and wording without touching the snippet. `PUT /agents/{id}/widget` accepts `theme`, `copy`, and `behavior` objects. ```bash theme={null} curl -X PUT https://builder.flowyte.com/api/v1/agents/$AGENT_ID/widget \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "theme": { "accent": "#2563eb" }, "copy": { "greeting": "Hi! How can I help?" } }' ``` A request from an origin that isn't on the allowlist is rejected with **403** — add the exact origin and try again. To roll a key without changing the agent or origins, use **rotate** (it keeps a short grace window); use **revoke** to kill it immediately. The widget always serves the agent's **published** version. Publish after any edits, and make sure the agent has at least one enabled chat channel. Related: [Agents](/concepts/agents) · [Draft vs published](/get-started/draft-vs-published) · [OpenAI-compatible chat](/guides/openai-compatible-chat) # Map an integration's fields Source: https://docs.flowyte.com/guides/map-integration-fields Connect any REST or GraphQL system, discover its schema, and map its fields onto your agent — no per-provider code. When a provider's schema can be introspected, you don't need pre-built actions. You **discover** its entire data model, then **map** the fields you want onto your agent's parameters. The mapping *is* the integration. This guide builds a "look up the caller" skill against a connected CRM. Authenticate every request with a secret API key: `Authorization: Bearer flowyte_sk_…`. Base URL `https://builder.flowyte.com/api/v1`. Connect it once for your organization (OAuth approval, or API-key credentials). ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/acme_crm/connect \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` Introspect the provider into a normalized schema. The response summarizes what was found. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/acme_crm/discover \ -H "Authorization: Bearer $FLOWYTE_API_KEY" # → { "data": { "objectCount": 38, "fieldCount": 612, "edgeCount": 74, ... } } ``` Read the discovered schema to see the objects, their fields, the relationships between them, and the operations you can run. This is the catalog you map from. ```bash theme={null} curl https://builder.flowyte.com/api/v1/integrations/acme_crm/schema \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` Find the operation you want (here, a `findContact` query) and the fields you want back (`contact.name`, `contact.email`). Map the operation onto a skill. **`inputs`** map your agent's parameters to the operation's arguments; **`projection`** picks which provider fields come back, each renamed to one of your own output names (`leaf`). The mapping is validated against the schema before it's created. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/integrations/acme_crm/bindings \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "toolName": "Look up the caller", "description": "Find the calling customer in the CRM by phone and return their name and email.", "operation": "findContact", "inputs": [ { "param": "caller_phone", "arg": "phone", "required": true, "description": "The caller'\''s phone number" } ], "projection": [ { "path": ["contact", "name"], "leaf": "customer_name" }, { "path": ["contact", "email"], "leaf": "customer_email" } ], "enabled": true }' ``` This compiles to a skill on the agent. The agent now calls `Look up the caller` with the caller's phone and gets back `customer_name` and `customer_email` — without ever seeing the provider's raw field tree. The new skill takes effect on the **published** agent. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publish \ -H "Authorization: Bearer $FLOWYTE_API_KEY" curl -N -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/simulate \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "message": "Hi, it'\''s me calling about my account.", "draftMode": false }' ``` ## How it fits together * You map onto **canonical parameters** (`caller_phone`, `caller_name`, `service_address`, …), so one mapping concept is reusable across any provider and your agent's prompt never changes. * The mapping is **validated against the discovered schema** when you save it — a field that doesn't exist, or one the operation can't return, is caught up front, not on a live call. * Set `enabled: false` to save a binding as a **draft** to review before it ships. **Don't want to map by hand?** `POST /agents/{agentId}/integrations/{kind}/bindings/auto` takes a plain-language goal — "look up a caller by phone and tell me their open tickets" — and the AI assistant proposes the whole binding (operation, input mapping, and projection) over the discovered schema. It's validated and saved as a **disabled draft** for you to review, then enable. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/integrations/acme_crm/bindings/auto \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "goal": "Look up the caller by phone and return their name and open tickets" }' ``` Some providers also ship a **Connector Pack** of ready-made preset skills — install them in one call with `POST /agents/{agentId}/integrations/{kind}/pack/install` (read it first with `GET /integrations/{kind}/pack`). # Use the OpenAI-compatible chat endpoint Source: https://docs.flowyte.com/guides/openai-compatible-chat Point an OpenAI SDK at Flowyte with a base-URL swap and talk to your agent — no new client to learn. Flowyte exposes an **OpenAI-compatible** chat completions endpoint. If your code already uses an OpenAI client library, you can talk to a Flowyte agent by changing two things: the base URL and the model. The `model` is your **agent id**, and the agent's knowledge, skills, and guardrails all apply to the response. All paths are relative to `https://builder.flowyte.com/api/v1`. Authenticate with your secret key `Authorization: Bearer flowyte_sk_…` (scope `chat:write`). This endpoint serves the agent's **published** version. ## What you'll use | Action | Endpoint | Scope | | ------------------- | ------------------------ | ------------ | | Create a completion | `POST /chat/completions` | `chat:write` | ## The base-URL swap Set the client's base URL to `https://builder.flowyte.com/api/v1`, the API key to your `flowyte_sk_…`, and pass the agent id as `model`. The SDK appends `/chat/completions` for you. ```ts Node theme={null} import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.FLOWYTE_API_KEY, // flowyte_sk_… baseURL: "https://builder.flowyte.com/api/v1", // ← the only swap }); const completion = await client.chat.completions.create({ model: "agt_123", // ← your agent id messages: [{ role: "user", content: "Do you ship to Canada?" }], }); console.log(completion.choices[0].message.content); ``` ```python Python theme={null} from openai import OpenAI client = OpenAI( api_key="flowyte_sk_…", base_url="https://builder.flowyte.com/api/v1", # ← the only swap ) resp = client.chat.completions.create( model="agt_123", # ← your agent id messages=[{"role": "user", "content": "Do you ship to Canada?"}], ) print(resp.choices[0].message.content) ``` ```bash curl theme={null} curl https://builder.flowyte.com/api/v1/chat/completions \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "agt_123", "messages": [{ "role": "user", "content": "Do you ship to Canada?" }] }' ``` The response is the standard completion shape: read the reply from `choices[0].message.content`. ## Streaming Set `stream: true` and the endpoint returns OpenAI-style SSE chunks — `data: {choices:[{delta:{content}}]}` … terminated by `data: [DONE]` — so an OpenAI SDK's streaming mode works unchanged. ```ts theme={null} const stream = await client.chat.completions.create({ model: "agt_123", messages: [{ role: "user", content: "Summarize your return policy." }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); } ``` ## What's read, and what isn't * Only `model`, `messages`, `stream`, and `user` are used. The agent's own configuration governs its behavior — sampling, tools, and system prompt come from the published agent, so request-level overrides for those are ignored. * `model` must be a real agent id, not an OpenAI model name. An unknown id returns a `400`. * Roles `system`, `user`, `assistant`, and `tool` are accepted in `messages`. Need session state, tool-call events, or quick replies? Use the native chat API instead: `POST /chat/sessions` then `POST /chat/sessions/{id}/messages` with `stream: true`. For an in-browser widget, mint a publishable key — see [Embed the chat widget](/guides/embed-chat-widget). Related: [Agents](/concepts/agents) · [Authentication](/get-started/authentication) # Take an order with a delivery check Source: https://docs.flowyte.com/guides/take-orders A phone agent that knows your menu, confirms the delivery address is in range, and sends the order to your kitchen. This guide builds a phone agent for a restaurant that answers menu questions, takes an order, checks that the delivery address is in your zone, and posts the confirmed order to your kitchen. Every step is one API call against `https://builder.flowyte.com/api/v1`. Authenticate every request with a secret API key: `Authorization: Bearer flowyte_sk_…`. See [Authentication](/get-started/authentication) to mint one. Capture `data.id` as your `AGENT_ID`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "name": "Tony's Pizza", "primaryLanguage": "en" }' ``` Add your menu so the agent answers "what's on the supreme?" and "how much is a large?" from the real thing. Ingestion is **asynchronous** — poll the source until `status` is `indexed`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge/sources \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "kind": "url", "label": "Menu", "url": "https://tonyspizza.com/menu" }' ``` A free `geo` skill with the `check_service_area` action. Set your shop address as the origin and your delivery radius. The agent asks for the address and confirms coverage before taking a delivery order — and offers pickup if it's out of range. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/skills \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "name": "Check delivery area", "description": "Check whether the caller's delivery address is within our delivery zone before taking a delivery order.", "skillType": "geo", "executionConfig": { "action": "check_service_area", "originAddress": "120 Main St, Boulder, CO", "radiusMiles": 5 } }' ``` An `http_webhook` skill posts the confirmed order to your kitchen display or POS. The agent fills in the parameters it collected on the call. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/skills \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "name": "Submit order", "description": "Send the confirmed order to the kitchen. Use after the caller confirms items and (for delivery) the address is in range.", "skillType": "http_webhook", "executionConfig": { "method": "POST", "url": "https://tonyspizza.com/api/orders", "parameters": [ { "name": "items", "description": "The ordered items with size and quantity" }, { "name": "fulfillment", "description": "delivery or pickup" }, { "name": "address", "description": "Delivery address, if delivery" }, { "name": "phone", "description": "Callback number" } ] } }' ``` Publish, then simulate an order. `simulate` streams over SSE — read it with `fetch()` streaming and stop on `event: done`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publish \ -H "Authorization: Bearer $FLOWYTE_API_KEY" curl -N -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/simulate \ -H "Authorization: Bearer $FLOWYTE_API_KEY" -H "Content-Type: application/json" \ -d '{ "message": "Can I get a large supreme delivered to 200 Pearl St?", "draftMode": false }' ``` Buy a number and point it at the agent — see [Buy a number & go live](/guides/buy-a-number). The agent only spends on a geocode when it has to look up a caller's address; the in/out-of-zone math itself is free. # Warm transfer to a human Source: https://docs.flowyte.com/guides/warm-transfer Hand a live caller off to a person — with context, and only under the conditions you choose. A **warm transfer** moves a live caller from the agent to a real person, after the agent has gathered who is calling and why. Use it when a request is out of scope, the caller is frustrated, or a rule says a human must take over. The agent stays in control right up to the moment it connects the call. All paths are relative to `https://builder.flowyte.com/api/v1`. Authenticate with `Authorization: Bearer flowyte_sk_…`. Destination numbers are saved in E.164 (e.g. `+14155550100`); a 10-digit US number is normalized on save. There are two pieces, and most agents use both: * **Handoff config** on the agent — a default destination plus a "transfer by context" table that routes described situations to specific numbers. * **A transfer skill** — a tool the agent calls mid-conversation to move the caller to a fixed number, which you can gate with a spoken confirmation. ## What you'll use | Action | Endpoint | Scope | | -------------------------------- | --------------------------- | -------------- | | Set default + by-context handoff | `PATCH /agents/{id}` | `agents:write` | | Add a transfer skill | `POST /agents/{id}/skills` | `skills:write` | | List an agent's skills | `GET /agents/{id}/skills` | `skills:read` | | Publish the change | `POST /agents/{id}/publish` | `agents:write` | The agent's `handoffConfig` holds a default `transferDestination` and a `transferRules` table. Each rule has a plain-language `when` the agent matches against, plus a `destination`. Set `warm: true` so the agent summarizes the caller and the reason before connecting, rather than a silent hand-off. ```bash curl theme={null} curl -X PATCH https://builder.flowyte.com/api/v1/agents/$AGENT_ID \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "handoffConfig": { "warm": true, "transferDestination": "+14155550100", "transferRules": [ { "label": "Billing", "when": "caller asks about an invoice, charge, or refund", "destination": "+14155550111" }, { "label": "Spanish line", "when": "caller prefers Spanish", "destination": "+14155550122" } ] } }' ``` ```ts Node theme={null} await fetch(`https://builder.flowyte.com/api/v1/agents/${agentId}`, { method: "PATCH", headers: { Authorization: `Bearer ${process.env.FLOWYTE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ handoffConfig: { warm: true, transferDestination: "+14155550100", transferRules: [ { label: "Billing", when: "caller asks about an invoice, charge, or refund", destination: "+14155550111" }, ], }, }), }); ``` The default `transferDestination` is used whenever a transfer is needed and no rule matches. A `transfer` skill is a tool the agent can call on demand. Its `description` is what the agent reads to decide *when* to use it — write it as the condition. Set `requiresConfirmation` so the agent confirms before transferring, and `stakes` to tune that gate. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/skills \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Transfer to billing", "description": "Transfer the caller to a billing specialist when they ask about an invoice, charge, or refund.", "skillType": "transfer", "executionConfig": { "destination": "+14155550111" }, "requiresConfirmation": true, "stakes": "medium" }' ``` A data-lookup skill can pass a **dynamic** destination at call time (e.g. route to the rep who owns the account), instead of a fixed number — useful for franchises and large rosters. Edits change the **draft**. Phone and chat serve the last **published** version, so publish to make the transfer behavior live. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publish \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` ## Choosing when a transfer fires * **By context** — the `when` text on each rule and the `description` on a transfer skill are how the agent decides; keep them specific. * **On silence** — set `callControl.onNoResponseAction` to `transfer` so an unresponsive caller is routed to a person instead of being ended. * **After hours** — by default the agent does *not* transfer to a live person when closed. Set `afterHours.allowHumanTransfer` and a `number` to allow it. See [After hours](/solutions/after-hours). The call is transferred off-net over the carrier (a blind SIP REFER), with the original caller ID passed through. Verify the destination number is one you control and is staffed during the hours the rule can fire. Related: [Skills](/concepts/skills) · [Draft vs published](/get-started/draft-vs-published) # Flowyte Source: https://docs.flowyte.com/index Build AI voice and chat agents that answer calls, book jobs, and resolve customer questions — all via API. Flowyte is an **API-first platform for building AI voice and chat agents**. Create an agent, give it knowledge and skills, publish it, and put it on a phone number or an embeddable chat widget — every step is available through the REST API, so an agent (or your code) can build and operate another agent end to end. Mint a key and build a working agent via the API in about five minutes. API keys, scopes, and how the dual browser / key auth model works. Agents, skills, knowledge, guardrails, and playbooks — what each one does. The full REST contract, generated from the OpenAPI spec. ## What you can build * **An AI answering service** that handles calls 24/7, answers questions from your knowledge base, and transfers to a human when it should. * **Field-service automation** — book a visit, look up a job's status, or capture a new request on the phone. * **Order status & support** — resolve "where is my order" and common questions across voice and chat. ## How it fits together An **agent** is the single thing you configure: its persona, language, and voice. Add **knowledge** (your FAQs, docs, and URLs) so it can answer accurately, and **skills** (tools) so it can take actions like booking or transferring. **Guardrails** keep it on-policy; **playbooks** script multi-step flows for repeatable tasks. **Publish** a frozen version, then connect a phone number or embed the chat widget. Ready? Head to the [Quickstart](/quickstart). # AI Harnesses Source: https://docs.flowyte.com/integrations/ai-harness Hand a live chat or SMS conversation from your Flowyte agent to your own operator agent — Hermes, OpenClaw, or any runtime — which talks back and forth with the customer, then resolves or returns to the AI. An **AI Harness** lets your Flowyte agent hand a live conversation to your **own** operator agent. When a chat or SMS conversation meets a rule you set, Flowyte opens a durable **escalation** and notifies your harness (Hermes, OpenClaw, or any runtime you build). Your harness **claims** the conversation and talks back and forth with the customer **in the same thread**, then **resolves** it, **returns** control to the Flowyte AI, or **asks for a human**. Flowyte owns the conversation record and keeps enforcing compliance; your harness is a participant. It reaches the customer in their original thread — a chat widget shows a "specialist has joined" divider; an SMS thread continues as a normal text. ## Set it up In **Integrations**, connect your AI Harness provider (`hermes` or `openclaw`). The escalation surface stays hidden until at least one harness is connected. Mint a scoped API key for your harness in the dashboard's **Developer** page, granting only the `escalations:*` scopes it needs (see the [connector reference](/integrations/ai-harness-connector)). A connector key can work conversations but can't edit your destinations or agents. Register where conversations go: `POST /escalation-destinations` with your harness's `endpoint_url` and the channels it handles (`chat`, `sms`, or both). The response returns a **signing secret** (`whsec_…`) **once** — store it; your harness verifies webhook signatures with it. The destination starts `unverified`. `POST /escalation-destinations/{id}/test` opens a sandbox escalation and calls your endpoint. Your harness must **receive → claim → reply → resolve** within the window. Only a green run flips the destination to `active`; the response reports a step-by-step ledger with a hint on failure. `PUT /agents/{id}/escalation-policy` sets an ordered list of rules — *when* to escalate and *where* to route. Rules match on deterministic conditions (an explicit request for a person, a human request, business hours, channel, a failed tool or knowledge lookup) and route to an `active` destination, an email fallback, or a queue. The policy edits the agent **draft**. [Publish](/get-started/draft-vs-published) so live phone and chat traffic use it. The policy is **per agent** — two agents in one organization can route to entirely different harness destinations. Destinations are shared organization-level infrastructure; the policy decides which agent uses which. ## What runs a conversation Once a destination is `active` and a policy rule matches, a live conversation follows this loop: 1. Flowyte opens an escalation and sends your harness a **signed webhook** naming the `channel` and a short reason. 2. Your harness **claims** the escalation, reads the handoff context (a transcript window plus the escalation reason and any verified identifiers), and starts replying. 3. Each reply reaches the customer in their original thread; each customer message is forwarded to your harness. While your harness owns the thread, the Flowyte AI stays silent. 4. Your harness ends the conversation — **resolve** (done), **return** (hand back to the AI, which resumes on the next customer message), or **request a human** (routes to your human fallback). Your harness can also **start** the conversation instead of waiting to be handed one: `POST /escalations` opens a new SMS thread with a customer and sends the first message. The session comes back **already owned by your key**, so step 2's claim is unnecessary — everything after that is the same loop. Building the harness side? Start with [Connect your AI harness](/guides/connect-ai-harness) for the end-to-end setup, then see the [connector reference](/integrations/ai-harness-connector) for the signed-webhook, claim, reply, resolve and initiate contract. ## Rules to know **Your harness can also start an SMS thread — with consent.** As well as replying inside a conversation the customer began, your harness can open one with `POST /escalations`. This is **not cold outreach**: the recipient must already have a recorded consent basis, and the text passes the same opt-out, quiet-hours and 10DLC gates as every other message Flowyte sends. **A customer who has texted you is already consented** — an inbound text is consent to text back. A number that has never texted you is refused. Outbound initiation is off by default and switched on per organization; see [Start an outbound SMS thread](/integrations/ai-harness-connector#5-start-an-outbound-sms-thread). * **SMS requires registration.** Enabling the `sms` channel on a destination requires your organization's completed **A2P 10DLC (TCR) registration** — the same registration your inbound SMS number already needs. See [SMS](/channels/sms). * **Plan.** Harness conversations are included on **Starter and above**. Setting up destinations and running the handshake test work on any plan, so you can wire everything up before you upgrade. * **Compliance stays with Flowyte.** Opt-out, quiet hours and consent are enforced on your harness's messages exactly as they are on the AI's — a message to a recipient who replied STOP is refused. Quiet hours depend on *who started the conversation*: a **reply** inside a thread the customer began is exempt from the recipient's 8am–9pm window (unless you turn on the strict posture in **Settings → SMS**, which holds replies too), while a harness-**initiated** first text is always held to that window and refused outside it, with the instant it re-opens. * **One owner per thread.** A conversation has at most one non-AI owner at a time, so a harness and a human seat can never both hold the same thread — including a thread your harness tries to start on a conversation a teammate is already working. ## In the API The escalation endpoints are in the [API Reference](/api-reference/introduction). Configuration endpoints (destinations, policy) work on any plan; the runtime endpoints are what your connector uses. | Action | Endpoint | Scope | | ------------------------------------- | --------------------------------------------------------------- | ----------------------------------------- | | List / register destinations | `GET` · `POST /escalation-destinations` | `escalation_destinations:read` / `:write` | | Update a destination | `PATCH /escalation-destinations/{id}` | `escalation_destinations:write` | | Run the handshake test | `POST /escalation-destinations/{id}/test` | `escalation_destinations:write` | | Rotate a destination's signing secret | `POST /escalation-destinations/{id}/rotate-secret` | `escalation_destinations:write` | | Read / set an agent's policy | `GET` · `PUT /agents/{id}/escalation-policy` | `escalation_policies:read` / `:write` | | List / read escalations | `GET /escalations` · `GET /escalations/{id}` | `escalations:read` | | Claim an escalation | `POST /escalations/{id}/claim` | `escalations:claim` | | Reply to the customer | `POST /escalations/{id}/messages` | `escalations:respond` | | Resolve / return / request human | `POST /escalations/{id}/resolve` · `/return` · `/request-human` | `escalations:resolve` | | **Start an outbound SMS thread** | `POST /escalations` | `escalations:initiate` | Authenticate every call with `Authorization: Bearer flowyte_sk_…`. You can also drive the same surface over the [MCP gateway](/get-started/mcp-gateway) — the escalation tools map one-to-one onto these endpoints. Next: the [connector reference](/integrations/ai-harness-connector) for building the harness side. # AI Harness connector reference Source: https://docs.flowyte.com/integrations/ai-harness-connector Build the harness side — authenticate with a scoped key, verify the signed webhook, then claim, reply, and resolve an escalated Flowyte conversation, or start an outbound SMS thread yourself. This page is for the team building the **connector** — the Hermes, OpenClaw, or custom runtime that receives escalated Flowyte conversations and talks back and forth with the customer. It's the technical companion to [AI Harnesses](/integrations/ai-harness), which covers the operator-side setup. The flow: Flowyte opens a durable **escalation session**, notifies you with a **signed webhook**, and you **claim** the session and drive it over REST — read the context, post replies (which reach the customer in their original thread), and finally **resolve** or **return** control to the AI. Flowyte owns the conversation record; your connector is a participant, never the source of truth. You can also drive that loop from the other end: [§5](#5-start-an-outbound-sms-thread) covers `POST /escalations`, which **starts** an SMS thread with a customer and hands you the session already owned. New to this? [Connect your AI harness](/guides/connect-ai-harness) walks the whole setup once, end to end; this page is the reference you keep open while building. ## 1. Authenticate with a scoped key Every REST call uses a **scoped secret key** (`flowyte_sk_…`), minted by the operator for the destination and shown once: ``` Authorization: Bearer flowyte_sk_… ``` The key's scopes gate exactly what the connector may do — request only what you need. These scopes are separate from `escalation_destinations:*`: a connector key works conversations but can't edit destinations. | Scope | Grants | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `escalations:read` | `GET /escalations`, `GET /escalations/{id}` (context, owner only), `GET /escalations/{id}/messages` | | `escalations:claim` | `POST /escalations/{id}/claim`, `/heartbeat` | | `escalations:respond` | `POST /escalations/{id}/messages`, `/typing` | | `escalations:resolve` | `POST /escalations/{id}/resolve`, `/return`, `/request-human` | | `escalations:initiate` | `POST /escalations` — **start** an outbound SMS thread ([§5](#5-start-an-outbound-sms-thread)). Deliberately separate from `escalations:respond`: the scopes above work a conversation Flowyte handed you, this one texts somebody who never messaged you on that thread. | Keys are **organization- and destination-scoped**: a key only ever sees its own tenant's sessions. Cross-tenant calls fail closed as `404`. Never expose local file paths, prompts, model names, or secrets in a reply, a resolve note, or a shipped log — the customer sees message text verbatim. ## 2. Verify the signature first Flowyte delivers webhooks to your `endpoint_url` over its signed pipeline. The body is a stable envelope — **route on the `channel` field in every notification**: ```jsonc theme={null} { "id": "", // also the Flowyte-Delivery header; dedupe on this "type": "escalation.requested", // or escalation.test | escalation.message.created | … "createdAt": "2026-07-13T18:03:00Z", "organizationId": "org_…", "data": { "escalationId": "esc_…", "conversationId": "…", "channel": "chat", // chat | sms — pick the right profile before claiming "reason": { "type": "sales_request", "summary": "…" }, "responseDeadlineAt": "2026-07-13T18:05:00Z", "test": false // true for a handshake escalation.test envelope } } ``` ### Signature headers | Header | Meaning | | ------------------------ | ------------------------------------------------------------------------------- | | `Flowyte-Signature` | `hex(HMAC_SHA256(secret, "{Flowyte-Delivery}.{Flowyte-Timestamp}.{rawBody}"))` | | `Flowyte-Signature-Prev` | Present **only** during a 24-hour secret rotation; same scheme, previous secret | | `Flowyte-Timestamp` | Unix seconds (decimal string) | | `Flowyte-Delivery` | The delivery id (also `id` in the body) — your **dedupe key** | | `Flowyte-Event` | The event type | ### Verification recipe Reject if `|now − Flowyte-Timestamp| > 300s` (replay window). Compute `hex(HMAC_SHA256(secret, Flowyte-Delivery + "." + Flowyte-Timestamp + "." + rawBody))` using the **raw** request bytes — re-serializing the JSON changes whitespace and breaks the match. Accept only if it equals `Flowyte-Signature`, using a constant-time compare (never `==`). During a rotation, also accept if any secret you hold verifies `Flowyte-Signature-Prev`. A bad signature or a stale timestamp is a `401` — do no work. Webhooks are the **doorbell, not the mailbox** — they tell you something happened; the REST API is the source of truth. Delivery is at-least-once, so a webhook may be redelivered — dedupe on `Flowyte-Delivery`. Don't parse conversation content out of webhooks; read it from `/messages`. ## 3. Claim, reply, resolve ### Claim `POST /escalations/{id}/claim` takes ownership and starts an activity-based lease (chat ≈ 2 min, SMS ≈ 4 h). Claiming is **idempotent for your key** — re-claiming a session you hold just re-extends the lease — and guarded, so a second connector gets `409 claim_conflict`. **Claim with retry on `409` / `404`.** A redelivered webhook or a race can transiently `409`, and the session may not be visible for a beat (`404`) right after it's created — back off briefly and retry a few times. The claim response (and `GET /escalations/{id}` as the owner) returns the **context package**: the last N transcript turns (each with its sequence number), the escalation reason, any verified identifiers (SMS: the caller's phone), and completed or failed tool actions (names and status only). It's returned **only** to the claiming owner and is rebuilt at claim time, so you start current with any turns that arrived while the notification was in flight. Claim can also fail with `409 destination_unavailable` (the harness was disconnected or the destination is inactive) or `403 plan_required` (the organization is below Starter — the handshake test is exempt). ### Read and sync Every turn — customer **and** your own replies — carries the conversation's sequence number (`seq`), the single ordering and dedup key. To catch up, or after a reconnect, poll `GET /escalations/{id}/messages?after_seq=N` and process strictly increasing `seq` values. Never assume a webhook told you everything; the `/messages` mailbox is authoritative and ordered. ### Reply ```jsonc theme={null} POST /escalations/{id}/messages { "client_message_id": "your-stable-id", "text": "Hi, this is Dana from Sales." } ``` * **`client_message_id` is your idempotency key.** A replay with the same id returns the **original** receipt (`{ messageId, seq, delivered }`) instead of double-sending. Use one stable id per logical message and reuse it across retries. * The reply is delivered to the customer in their original thread — chat shows a "specialist has joined" divider; SMS sends a text. * **Media** (`mediaUrls`) is chat-only; on SMS it returns `422 media_unsupported_channel`. * **SMS compliance:** a reply to an opted-out recipient returns `409 sms_suppressed`. A reply is **exempt from quiet hours** — the customer started this conversation — unless the operator has turned on the strict posture for their organization, in which case an out-of-window reply returns `200` with `delivered: false`. Both are terminal — don't retry either. (A thread your harness *started* is different: see [§5](#5-start-an-outbound-sms-thread).) Any owner action (a message, typing, heartbeat, even a `/messages` read) extends your lease. Send `POST /escalations/{id}/heartbeat` if you go quiet but want to keep the thread; `POST /escalations/{id}/typing` shows a typing indicator (chat only). If the lease expires, the thread returns to the AI and you get `409 lease_expired` — re-claim to continue. ### Finish | Call | Effect | | ------------------------------------------------------------------ | ------------------------------------------------------------ | | `POST /escalations/{id}/resolve` `{ "outcome": "…", "note": "…" }` | Done — the conversation closes. | | `POST /escalations/{id}/return` | Hand control back to the AI, which resumes the conversation. | | `POST /escalations/{id}/request-human` | Route to the operator's human fallback chain. | ## 4. Channels: chat vs SMS The lifecycle is identical across channels — **route on the `channel` field** — but: * **Chat** replies appear live in the website widget; typing indicators and media attachments are supported. Chat is **customer-initiated only** — there is no visitor to push to until one arrives, so `POST /escalations` refuses any channel but `sms` with `422 channel_unsupported`. * **SMS** threads can be started from either side. You reply inside a conversation the customer began, and — with the recipient's consent and the prerequisites in [§5](#5-start-an-outbound-sms-thread) — you can open one yourself. SMS has no typing indicator and no media in v1, and it requires the operator's completed **A2P 10DLC (TCR) registration** — the same registration their inbound number already needs. See [SMS](/channels/sms). Harness conversations require the operator's organization to be on **Starter or higher**. The handshake test works on any plan, so setup can precede the upgrade. ## 5. Start an outbound SMS thread `POST /escalations` opens a **new** SMS conversation with a customer and sends its first message. It is the outbound sibling of everything above: once the session exists, the customer's replies route to your harness through the same path as any escalation (the Flowyte AI does not answer them), so you continue on `POST /escalations/{id}/messages` exactly as you would on a thread you claimed. **This texts a real person, and an SMS cannot be unsent.** It is not a bulk or cold-outreach channel: every initiate is gated on the recipient's recorded consent, capped per workspace, restricted to US and Canada numbers, and held to the recipient's legal texting window. Read the whole section before you wire it up. ### Prerequisites All of these must hold, or the call is refused before anything is sent: | Requirement | Refusal if missing | | -------------------------------------------------------------------------------------------------------------- | ------------------------------- | | The deployment has the Agent Bridge enabled (`ESCALATION_BRIDGE_ENABLED`) | `503 bridge_disabled` | | The organization is enabled for harness conversations **and** for outbound initiation | `403 harness_outbound_disabled` | | The organization is on **Starter or higher** (PAYG fails closed) | `403 plan_required` | | The organization's A2P 10DLC (TCR) campaign is **active** | `403 sms_not_enabled` | | A connected `hermes` / `openclaw` integration behind the destination | `409 destination_unavailable` | | A destination that is `active` with `sms` in `allowedChannels` (a green handshake is the only way to `active`) | `422 destination_not_ready` | | The agent has an **SMS-active number** assigned | `422 no_sms_number` | | The key carries `escalations:initiate` | `403 insufficient_scope` | Outbound initiation is **off by default** and switched on per organization by Flowyte — ask your Flowyte contact to enable it once the rest is in place. ### The request ```jsonc theme={null} POST /escalations Authorization: Bearer flowyte_sk_… { "agentId": "agt_4f9c2b7e10a24d51", // whose SMS-active number the thread opens on "destinationId": "dest_7a2c", // the destination that will own the conversation "channel": "sms", // optional; defaults to "sms", and only "sms" is accepted "to": "+14155551234", // E.164, +1 (US/Canada) only "text": "Hi Dana — following up on the quote you asked about yesterday.", "client_message_id": "6f1c2d3e-4a5b-6789-abcd-ef0123456789" } ``` **There is no `from`.** The sending number is derived server-side from the agent's SMS-active number, and you can never choose it — an agent structurally cannot text without an assigned number, and accepting a caller-supplied `from` would route around that rule. An agent with no SMS-active number is `422 no_sms_number`. **Send the message only.** On the first text of a messaging relationship (and again after 30 days) the platform prepends the brand identity and appends the required `Msg & data rates may apply. Reply HELP for help, STOP to opt out.` block; every message is then sanitized to GSM-7 and capped at 2 segments. Do **not** write your own disclosure — you would ship it twice. ### The response ```jsonc theme={null} 201 Created { "success": true, "data": { "escalationId": "esc_9f21", "conversationId": "conv_9d2f01", "state": "active", "messageId": "emsg_4c8a", "seq": 1, "delivered": true } } ``` The session is created **already claimed by your key** and `active`, so you never race the 120-second claim SLA on a thread you started. It carries an ordinary SMS lease (**4 hours**, extended by any owner action) and auto-resolves after **24 hours** with no activity in either direction. `delivered` is always `true` on a `201` — an initiate that did not send is an error, never a `201` with `delivered: false`. `state` is `active` in the normal case; it reads `claimed` only if the first-message state transition did not land, and the thread is yours either way. **You will still receive webhooks for a thread you started** — `escalation.requested` followed by `escalation.claimed` (carrying `initiatedBy: "external_agent"`), so a connector that reconciles purely from its event stream sees the session appear and sees that it owns it, in the same ordering as every other session. The session's `reason.type` is `agent_initiated`. If your connector auto-claims on `escalation.requested`, that claim is harmless — claiming is idempotent for the key that already holds the session. ### Consent — the rule that decides most refusals The recipient's recorded consent chip must be **`conversational`** or **`express`**. * **An inbound text is consent to text back.** A customer's first inbound message stamps `conversational` automatically, so anyone who has ever texted the business can be texted back with no extra step. This is the ordinary case and needs nothing from you. * **`express`** is consent captured deliberately elsewhere — on a call, in a chat, on a form. * A number with **no consent chip, or no contact record at all**, is refused `422 consent_required`. A number that has never texted the business is exactly this case. An organization that holds consent out-of-band can be granted an audited bypass. It waives **only** the per-recipient consent check — opt-out (STOP), 10DLC registration, the number being SMS-active and quiet hours all still apply — and every use writes a row to the append-only compliance ledger. It is off by default and is not something a connector can turn on. ### Quiet hours differ from a reply A harness **reply** is exempt from the recipient's 8am–9pm window, because the customer started that conversation. A harness-**initiated** first text is business-initiated, so it is always held to the window: outside it, the call is refused `409 quiet_hours` carrying `details.nextOpen` (RFC 3339). It is a refusal, not a queue — nothing is sent, and you retry after `nextOpen`. ### Idempotency — read this before you write a retry `client_message_id` is **required**, and it is a genuine idempotency key: the claim is keyed on `(organization, client_message_id)`, is durable, and is committed **before** the text is sent. A replay therefore never produces a second text — not after the original escalation was resolved, not after a conversation-episode rollover, and not against a concurrent duplicate of itself. A completed replay returns the original `201` **byte for byte**. Reusing one id for a **different** message is `409 client_message_id_reused` — an id names one message, and replaying the first message's receipt for a second one would report a delivery that never happened. **`409 send_state_unknown` is the one refusal that does not promise the phone stayed silent.** It means the carrier request failed in a way that does not prove the message stayed home — a timeout, a transport failure, a 5xx. The message **may already have been delivered**. Do **not** blindly retry. Either re-read the thread (the response carries `details.escalationId` and `details.conversationId`), or replay the **same** `client_message_id` — a replay resolves to this same answer, or to the receipt once the state settles, and never sends a second text. Sending under a *fresh* id is what texts somebody twice. Contrast `502 upstream_error`: the carrier answered and rejected the request outright, so nothing was sent and the `client_message_id` has been released. That one **is** safe to retry once corrected. ### Limits * **200 initiates per organization per rolling 24 hours** (the deployment can configure this). Past the ceiling: `429 daily_initiate_cap`, carrying `details.nextOpen` and a `Retry-After` header. The window is rolling rather than a calendar day, so a caller cannot spend the cap twice across midnight. Refusals create nothing and an idempotent replay is answered before the cap is consulted, so neither consumes budget. * **US and Canada only.** `to` must be a `+1` NANP number; anything else is `422 unsupported_country`. * **`sms` only.** `422 channel_unsupported` for any other channel. * `text` is capped well above a real message and is refused `400` beyond it; an id longer than 200 characters is also a `400`. ### Errors | Status | Code | Meaning | | ------ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Missing or oversized `agentId`, `destinationId`, `text` or `client_message_id`; or nothing is left after GSM-7 sanitization. | | 403 | `forbidden` | No connector API key — a dashboard user session cannot initiate. | | 403 | `insufficient_scope` | The key lacks `escalations:initiate`. | | 403 | `harness_outbound_disabled` | The organization is not enabled for harness-initiated conversations. | | 403 | `plan_required` | The organization is on PAYG; Starter or higher is required. | | 403 | `sms_not_enabled` | The organization's A2P 10DLC/TCR campaign is not active. | | 409 | `conversation_busy` | A teammate's seat, or another escalation, already owns that thread — or an inbound message from that contact is being processed right now (`details.reason`). Carries `details.conversationId` and, when known, `details.escalationId`, so a harness that lost its `201` can address the thread it already owns. | | 409 | `client_message_id_reused` | That id was already used for a **different** message. Use a fresh one. | | 409 | `send_state_unknown` | The outcome is indeterminate — the message **may already have been delivered**. Carries `details.escalationId`, `details.conversationId`, `details.clientMessageId`. Do not blindly retry. | | 409 | `quiet_hours` | Outside the recipient's legal texting window. Carries `details.nextOpen` — retry after it. | | 409 | `sms_suppressed` | The recipient replied STOP. | | 409 | `destination_unavailable` | The destination's AI Harness integration is disconnected. | | 422 | `consent_required` | No recorded consent basis for that number (and no organization bypass). | | 422 | `no_sms_number` | The agent has no SMS-active number. | | 422 | `destination_not_ready` | The destination is not `active`, or does not allow the `sms` channel. | | 422 | `invalid_to` | `to` is not E.164. | | 422 | `unsupported_country` | `to` is outside the US/Canada (`+1`) allowlist. | | 422 | `channel_unsupported` | `channel` is not `sms`. | | 429 | `daily_initiate_cap` | The workspace hit its rolling-24h initiate ceiling. Carries `details.nextOpen` + `Retry-After`. | | 502 | `upstream_error` | The carrier answered and rejected the request: nothing was sent and the id is released, so a corrected retry is safe. | | 503 | `bridge_disabled` | The Agent Bridge is not enabled on this deployment. | | 503 | `sms_not_configured` | SMS sending is not wired on this deployment. | Every code above **except `send_state_unknown`** is a promise that the recipient's phone stayed silent. ### End to end `POST /escalations` with `agentId`, `destinationId`, `to`, `text` and a fresh `client_message_id`. You get `201` with `escalationId`, `conversationId`, the message's spine `seq` and `delivered: true`. The customer receives your text with the brand prefix and the opt-out disclosure appended. Store `escalationId` and the last `seq` you have seen. The session is already yours — no claim call, no SLA race. `escalation.requested` and `escalation.claimed` will also arrive on your webhook endpoint for this session; dedupe on `Flowyte-Delivery` as always. Flowyte persists the reply to the conversation spine and delivers `escalation.message.created` to your endpoint with `data.message` (`seq`, `role: "visitor"`, `text`) and `data.latestSeq`. The Flowyte AI stays silent for as long as you own the thread. Treat the webhook as the doorbell: `GET /escalations/{id}/messages?after_seq=` and process strictly increasing `seq` values. This closes any gap from a dropped or reordered delivery. `POST /escalations/{id}/messages` with a new `client_message_id` and your `text` — the ordinary reply path. It is `KindReply`, so no further disclosure block is appended and quiet hours do not hold it. Every owner action extends your 4-hour lease. `POST /escalations/{id}/resolve` when you are done, `/return` to hand the thread back to the Flowyte AI, or `/request-human` to route it to the operator's human fallback. Left alone, the thread auto-resolves after 24 idle hours. The same operation is available over the [MCP gateway](/get-started/mcp-gateway) as `initiate_escalation` (scope `escalations:initiate`), annotated destructive and open-world because it texts a real person. ## 6. The handshake test Before a destination can route real conversations, the operator runs a handshake test (`POST /escalation-destinations/{id}/test`) that proves your connector can talk back and forth: 1. Flowyte opens a **sandbox** escalation on a synthetic conversation and sends you a signed `escalation.test` webhook — a normal `escalation.requested` envelope plus `"test": true`. 2. Your connector must, within the window: **verify the signature → claim → post one message → resolve.** 3. Flowyte reports a step ledger (`{ webhook_delivered, claimed, message_posted, resolved }`) with an actionable hint on failure. Only a green run flips the destination to `active`. Treat `escalation.test` **exactly like** `escalation.requested` — same code path. Test messages reach no customer and are never billed, but your connector doesn't need to know that; just claim, reply, resolve. ## Hard rules * **Verify `Flowyte-Signature`** (constant-time; 300s replay window; accept `Flowyte-Signature-Prev` during rotation) before doing any work. * **Claim with retry on `409` / `404`;** claim is idempotent for your key. * **Idempotent `client_message_id`** on every reply; a replay returns the original receipt. * **Dedupe by `seq`; gap-sync via `GET /escalations/{id}/messages?after_seq=N`.** Webhooks are the doorbell; the REST API is the mailbox. * **Route on the `channel` field** in every notification. * Sessions are **tenant- and destination-scoped** — you only ever see your own. * **Never expose local paths, prompts, model internals, or secrets** in replies, notes, or logs. * **SMS requires completed TCR registration**; harness conversations require **Starter or higher**. * **Starting an SMS thread needs recorded consent** and a separate `escalations:initiate` scope; chat can never be started outbound. * **Never blindly retry `409 send_state_unknown`** — replay the same `client_message_id` or re-read the thread. A fresh id there is how you text somebody twice. Back to [AI Harnesses](/integrations/ai-harness) for the operator setup, or drive the same surface from an agent over the [MCP gateway](/get-started/mcp-gateway). # Build a custom integration Source: https://docs.flowyte.com/integrations/custom Call your own endpoint from a conversation with an http_webhook skill. No native connector for your system? Build a [skill](/concepts/skills) of type `http_webhook`. The agent collects inputs from the caller, then calls **your** HTTP endpoint and uses the response in the conversation. Your service is the integration — Flowyte handles the conversation and the call. ## How it works A custom skill has two halves: * **`parametersSchema`** — a JSON Schema describing what the agent collects from the caller. Use a proper object schema with a top-level `properties` map (not a flat map). Put only caller-supplied inputs here. * **`executionConfig`** — where to send them: `url` (required), plus optional `method`, `headers`, and `timeout_ms`. Operator settings live here, never in `parametersSchema`. When the agent decides the skill is relevant, it fills the parameters in natural language and POSTs them to your `url`. ## Create the skill ```bash curl theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/AGENT_ID/skills \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "name": "Check warranty status", "description": "Look up a warranty by serial number.", "skillType": "http_webhook", "parametersSchema": { "type": "object", "properties": { "serial_number": { "type": "string", "description": "Product serial number" } }, "required": ["serial_number"] }, "requiredParams": ["serial_number"], "executionConfig": { "url": "https://api.yourco.com/warranty/lookup", "method": "POST", "headers": { "X-Api-Key": "…" }, "timeout_ms": 4000 } }' ``` ```python Python theme={null} import requests requests.post( "https://builder.flowyte.com/api/v1/agents/AGENT_ID/skills", headers={"Authorization": "Bearer flowyte_sk_…"}, json={ "name": "Check warranty status", "description": "Look up a warranty by serial number.", "skillType": "http_webhook", "parametersSchema": { "type": "object", "properties": { "serial_number": {"type": "string", "description": "Product serial number"} }, "required": ["serial_number"], }, "requiredParams": ["serial_number"], "executionConfig": { "url": "https://api.yourco.com/warranty/lookup", "method": "POST", "headers": {"X-Api-Key": "…"}, "timeout_ms": 4000, }, }, ) ``` Keep your endpoint fast — return within `timeout_ms` so the agent doesn't stall the caller. Return a small JSON body the agent can read back. ## The MCP path (coming soon) A managed MCP gateway — exposing your tools to the agent over the Model Context Protocol, the same surface a customer's own LLM can drive — is on the roadmap. The `mcp` skill type is reserved for it. Until it ships, use `http_webhook` for custom actions. Reserved endpoints respond with **403** (not 404), so a 403 means a capability exists but isn't enabled for you yet. ## In the API | Action | Endpoint | Scope | | ---------------------- | ------------------------------------------------ | -------------- | | Create a custom skill | `POST /agents/{agentId}/skills` | `skills:write` | | List skills | `GET /agents/{agentId}/skills` | `skills:read` | | Update / remove | `PATCH` · `DELETE /agents/{agentId}/skills/{id}` | `skills:write` | | Browse all skill types | `GET /skill-types` | `skills:read` | New skills land on the **draft**. [Publish](/concepts/agents) so phone and chat callers can use them. # Google Calendar Source: https://docs.flowyte.com/integrations/google-calendar Let an agent check availability and book appointments on a connected calendar. Connect Google Calendar once, then provision its actions as [skills](/concepts/skills) so an agent can **check open times** and **book an appointment** mid-conversation — the agent collects the details from the caller in natural language and writes the event for you. ## Connect Google Calendar uses OAuth. Start the connect, then finish consent in a browser. `POST /integrations/google_calendar/connect` returns an `oauthUrl`. Open the `oauthUrl` while signed in to your dashboard. The browser-only callback finishes the connection — it cannot be completed with an API key. `GET /integrations` now lists `google_calendar` with `status: connected` and an `accountLabel`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/google_calendar/connect \ -H "Authorization: Bearer flowyte_sk_…" # → { "data": { "oauthUrl": "https://…" } } # open this in a browser ``` ## Provision actions as skills List the actions, then provision the ones you want onto an agent. ```bash curl theme={null} # See what's available curl https://builder.flowyte.com/api/v1/integrations/google_calendar/actions \ -H "Authorization: Bearer flowyte_sk_…" # Provision the important actions (omit "actions" to take all of them) curl -X POST \ https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/google_calendar/provision \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "actions": ["check_availability", "book_appointment"] }' ``` ```js Node theme={null} const res = await fetch( "https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/google_calendar/provision", { method: "POST", headers: { Authorization: "Bearer flowyte_sk_…", "Content-Type": "application/json", }, body: JSON.stringify({ actions: ["check_availability", "book_appointment"] }), }, ); const { data } = await res.json(); // [{ skill, created }] ``` ```python Python theme={null} import requests res = requests.post( "https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/google_calendar/provision", headers={"Authorization": "Bearer flowyte_sk_…"}, json={"actions": ["check_availability", "book_appointment"]}, ) data = res.json()["data"] # [{ "skill": …, "created": true }] ``` Each returned item carries `created: false` when the skill already existed — provisioning is an idempotent no-op, so it is safe to re-run. Actions that still need operator config land **disabled** as drafts; open them in the dashboard to finish setup. Action slugs come from `GET /integrations/{kind}/actions` — read that catalog rather than guessing names, since available actions can grow over time. ## In the API | Action | Endpoint | Scope | | ---------------- | ----------------------------------------------------------------- | -------------------- | | Connect | `POST /integrations/google_calendar/connect` | `integrations:write` | | List actions | `GET /integrations/google_calendar/actions` | `integrations:read` | | Provision skills | `POST /agents/{agentId}/integrations/google_calendar/provision` | `skills:write` | | Remove skills | `DELETE /agents/{agentId}/integrations/google_calendar/provision` | `skills:write` | | Disconnect | `DELETE /integrations/google_calendar` | `integrations:write` | Provisioning edits the agent **draft**. [Publish](/concepts/agents) before phone or chat callers can book. # Google Sheets Source: https://docs.flowyte.com/integrations/google-sheets Log every call to a spreadsheet — map captured fields to real columns, no code. Connect Google Sheets once, then provision a **log a row** action as a [skill](/concepts/skills). The agent collects fields from the caller and appends a row to the tab you choose — a lead log, an order record, a callback list. Mapping is config, not code. ## Connect Google Sheets uses OAuth. Begin the connect, then finish consent in a browser. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/google_sheets/connect \ -H "Authorization: Bearer flowyte_sk_…" # → { "data": { "oauthUrl": "https://…" } } # open this in a browser to grant access ``` The browser-only callback completes the connection; it cannot be finished with an API key. After consent, `GET /integrations` lists `google_sheets` as `connected`. ## Resolve the target sheet Before you map columns, confirm the connected account can read the spreadsheet. Pass a Sheets URL or bare id as `ref` and get back the title, its tabs, and each tab's header row — so you map captured params to the **real** column names. ```bash theme={null} curl -G https://builder.flowyte.com/api/v1/integrations/google_sheets/spreadsheet \ -H "Authorization: Bearer flowyte_sk_…" \ --data-urlencode "ref=https://docs.google.com/spreadsheets/d/SHEET_ID/edit" ``` This read-only helper uses only the granted spreadsheet scope. A `409 integration_not_connected` means Sheets isn't connected; a `404 sheet_not_accessible` means the link is wrong or the file isn't shared with the connected account. ## Provision the skill List the actions, then provision onto an agent. The column mapping is part of the skill's config — finish it in the dashboard if the action lands as a draft. ```bash curl theme={null} curl https://builder.flowyte.com/api/v1/integrations/google_sheets/actions \ -H "Authorization: Bearer flowyte_sk_…" curl -X POST \ https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/google_sheets/provision \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "actions": ["append_row"] }' ``` ```js Node theme={null} await fetch( "https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/google_sheets/provision", { method: "POST", headers: { Authorization: "Bearer flowyte_sk_…", "Content-Type": "application/json", }, body: JSON.stringify({ actions: ["append_row"] }), }, ); ``` ```python Python theme={null} import requests requests.post( "https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/google_sheets/provision", headers={"Authorization": "Bearer flowyte_sk_…"}, json={"actions": ["append_row"]}, ) ``` ## In the API | Action | Endpoint | Scope | | ---------------- | --------------------------------------------------------------- | -------------------- | | Connect | `POST /integrations/google_sheets/connect` | `integrations:write` | | Resolve a sheet | `GET /integrations/google_sheets/spreadsheet?ref=…` | `integrations:read` | | List actions | `GET /integrations/google_sheets/actions` | `integrations:read` | | Provision skills | `POST /agents/{agentId}/integrations/google_sheets/provision` | `skills:write` | | Remove skills | `DELETE /agents/{agentId}/integrations/google_sheets/provision` | `skills:write` | | Disconnect | `DELETE /integrations/google_sheets` | `integrations:write` | Provisioning edits the **draft**. [Publish](/concepts/agents) so live calls write rows. # HubSpot Source: https://docs.flowyte.com/integrations/hubspot Recognize returning callers, look up contacts, deals and tickets, and log every call to HubSpot. Connect your HubSpot CRM once, then your agent works against it live. It recognizes a returning caller by the number they're calling from and greets them by name, looks up **contacts**, **companies**, **tickets**, and **deals** mid-conversation, and opens new work — creating a contact, a support ticket, or a deal. Creates confirm with the caller first; edits to an existing record additionally require a [verified caller identity](/guides/caller-verification). ## Connect HubSpot uses OAuth. Start the connect, finish consent in a browser, and the [Connector Pack](#what-installs-the-connector-pack) skills install automatically. `POST /integrations/hubspot/connect` returns an `oauthUrl`. Open the `oauthUrl` while signed in to your dashboard and approve the HubSpot consent screen. The browser-only callback finishes the connection — it cannot be completed with an API key. `GET /integrations` now lists `hubspot` with `status: connected` and an `accountLabel`. The Connector Pack skills are compiled per-account against your connection at install. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/hubspot/connect \ -H "Authorization: Bearer flowyte_sk_…" # → { "data": { "oauthUrl": "https://…" } } # open this in a browser ``` ## What installs: the Connector Pack Connecting installs HubSpot's **Connector Pack** — a curated set of skills, each compiled per account against your connection at install time. Re-installing is idempotent: a skill that already exists is skipped and returned with `created: false`. | Skill | What it does | | ---------------- | --------------------------------------------------------------------------------------------------- | | `find_contact` | Recognize a caller by phone (find-or-nothing lookup) so the agent can greet them by name. | | `find_company` | Look up a company by name or website domain. | | `find_ticket` | Find a support ticket by subject or a keyword. | | `create_contact` | Capture a caller — **find-or-create, deduped by phone**, so a returning caller is never duplicated. | | `update_contact` | Edit a known contact by id (from a prior `find_contact`). | | `create_ticket` | Open a support request. | | `create_deal` | Open a sales opportunity. | Write skills (`create_*` / `update_*`) land **gated** — they confirm with the caller and never run in parallel. Updating an existing record additionally requires a [verified caller identity](/guides/caller-verification). ## Interaction logging Turn on **interaction logging** to write a record of every finished conversation back to the matched HubSpot contact's timeline — a per-connection setting that is **off by default** and **included free**. A voice call becomes a **Call** engagement; a chat becomes a **Note**. Each carries an AI summary and, optionally, the full transcript — scrubbed of card and ID numbers before export. Nothing is exported until you enable it, and past conversations are never back-filled. When a conversation has no CRM match, the `on_no_match` policy decides what happens: * `skip` — the interaction is skipped and counted (the default). * `create` — a contact is auto-created, then the interaction is logged to it. ```bash curl theme={null} # Read the current config + delivery counters curl https://builder.flowyte.com/api/v1/integrations/hubspot/writeback \ -H "Authorization: Bearer flowyte_sk_…" # Log a summary + transcript, auto-creating a contact on no match curl -X PATCH https://builder.flowyte.com/api/v1/integrations/hubspot/writeback \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "mode": "summary_transcript", "onNoMatch": "create" }' ``` ```js Node theme={null} const res = await fetch( "https://builder.flowyte.com/api/v1/integrations/hubspot/writeback", { method: "PATCH", headers: { Authorization: "Bearer flowyte_sk_…", "Content-Type": "application/json", }, body: JSON.stringify({ mode: "summary_transcript", onNoMatch: "create" }), }, ); const { data } = await res.json(); ``` ```python Python theme={null} import requests res = requests.patch( "https://builder.flowyte.com/api/v1/integrations/hubspot/writeback", headers={"Authorization": "Bearer flowyte_sk_…"}, json={"mode": "summary_transcript", "onNoMatch": "create"}, ) data = res.json()["data"] ``` `mode` is one of `off`, `summary`, or `summary_transcript`. `onNoMatch` (`skip` | `create`) is optional and left unchanged when omitted. ## Beyond the pack: any object or field The pack covers the common flows, but HubSpot is fully **open to author**: bind any object and field — **including custom objects** — from the account's discovered schema. Three ways in: * **"Describe it" AI authoring** — auto-map a plain-language goal to a validated binding with `POST /agents/{agentId}/integrations/hubspot/bindings/auto`. For a REST CRM like HubSpot this authors reads *and* writes (create, update, find-or-create, list-many, multi-filter, latest-sort). * **The manual builder** — pick the object and fields yourself and map a binding with `POST /agents/{agentId}/integrations/hubspot/bindings`. * **The API** — discover the schema, browse or search it, and author against the `restSearch` (read) and `restWrite` (create / update / find-or-create) grammars. Walk through the whole flow in [Map an integration's fields](/guides/map-integration-fields). ## In the API | Action | Endpoint | Scope | | ------------------------ | -------------------------------------------------------------------------------------------------------------------- | ---------------------- | | Connect (OAuth consent) | `POST /integrations/hubspot/connect` | `integrations:connect` | | Get the Connector Pack | `GET /integrations/hubspot/pack` | `integrations:read` | | Install the pack | `POST /agents/{agentId}/integrations/hubspot/pack/install` | `skills:write` | | Discover the schema | `POST /integrations/hubspot/discover` | `integrations:write` | | Browse objects | `GET /integrations/hubspot/objects` · `GET /integrations/hubspot/objects/{object}` | `integrations:read` | | Search the schema | `GET /integrations/hubspot/search` | `integrations:read` | | Author a binding | `POST /agents/{agentId}/integrations/hubspot/bindings` · `POST /agents/{agentId}/integrations/hubspot/bindings/auto` | `skills:write` | | Read interaction logging | `GET /integrations/hubspot/writeback` | `integrations:read` | | Set interaction logging | `PATCH /integrations/hubspot/writeback` | `integrations:write` | **Least privilege.** The consent grant requests the scopes for the standard objects (contacts, companies, tickets, deals). Custom-object scopes are **optional** and only requested / used when you author a binding against a custom object. Installing the pack and authoring bindings edit the agent **draft**. [Publish](/concepts/agents) so phone and chat callers get the new skills. # Integrations overview Source: https://docs.flowyte.com/integrations/overview Connect an external system once, then provision its actions as skills on any agent. An **integration** is a connection to an external system — a calendar, a spreadsheet, a store. You connect the provider once for your organization, then turn its actions into [skills](/concepts/skills) on any agent. No glue code: the action's parameters become the values the agent collects from the caller, and the platform runs the call. ## The model: catalog → connect → provision `GET /integrations/catalog` returns every connectable provider — its display name, category, a short blurb, a logo slug, how many actions it exposes, whether it ships a preset pack (`hasPack`), and whether your org has already connected it. The dashboard renders this as a card grid; you can render your own. A provider with `actionCount: 0` and `hasPack: true` is pack-based — install its [Connector Pack](#beyond-pre-built-actions-map-fields-yourself) rather than provisioning individual actions. `POST /integrations/{kind}/connect`. OAuth providers return an `oauthUrl` to open in a browser; API-key providers accept `{ "credentials": { … } }` and connect immediately. `POST /agents/{agentId}/integrations/{kind}/provision` creates one skill per action. Omit `actions` to provision every important action, or pass a list of action slugs. Provisioning edits the agent **draft**. [Publish](/concepts/agents) so phone and chat serve the new skills. ## Beyond pre-built actions: map fields yourself For any provider whose schema can be introspected, you're not limited to its pre-built actions — **discover** its full schema and **map** any field onto your agent: `POST /integrations/{kind}/discover` introspects the provider into a normalized catalog of every object, field, relationship, and operation. `GET /integrations/{kind}/schema` returns that catalog to map from. `POST /agents/{id}/integrations/{kind}/bindings` maps an operation's inputs to your agent's parameters and projects the fields you want back — compiling to a skill. Walk through it in [Map an integration's fields](/guides/map-integration-fields), or see the [Integrations concept](/concepts/integrations) for how the two models compare. Two shortcuts skip the hand-mapping: * **Auto-map from a goal** — `POST /agents/{agentId}/integrations/{kind}/bindings/auto` turns a plain-language goal into a validated binding (saved as a draft to review). * **Install a preset pack** — providers that ship a **Connector Pack** install ready-made skills in one call: `POST /agents/{agentId}/integrations/{kind}/pack/install`. ## Live vs reserved The catalog only lists providers you can connect today. Reserved providers (for example Calendly, Square, OpenTable) are coming soon and do not appear yet. | Provider | Auth | Status | | ------------------------------------------------------------- | ------------ | ---------------------- | | [Google Calendar](/integrations/google-calendar) | OAuth | Live | | [Google Sheets](/integrations/google-sheets) | OAuth | Live | | [HubSpot](/integrations/hubspot) | OAuth | Live | | [Shopify](/integrations/shopify) | API key | Live | | [SQL database — Postgres & MySQL](/integrations/sql-database) | Credentials | Live | | [Zapier](/integrations/zapier) — write to 8,000+ apps via MCP | MCP endpoint | Live | | Calendly, Square, OpenTable | — | Reserved (coming soon) | Need something not listed? Build a [custom integration](/integrations/custom) with an `http_webhook` skill that calls your own endpoint. ## In the API | Action | Endpoint | Scope | | ------------------------------ | ------------------------------------------------------------------------------------------- | -------------------- | | Browse the provider catalog | `GET /integrations/catalog` | `integrations:read` | | List connected integrations | `GET /integrations` | `integrations:read` | | Connect a provider | `POST /integrations/{kind}/connect` | `integrations:write` | | List a provider's actions | `GET /integrations/{kind}/actions` | `integrations:read` | | Provision actions as skills | `POST /agents/{agentId}/integrations/{kind}/provision` | `skills:write` | | Discover a provider's schema | `POST /integrations/{kind}/discover` | `integrations:write` | | Read the discovered schema | `GET /integrations/{kind}/schema` | `integrations:read` | | Browse objects / operations | `GET /integrations/{kind}/objects` · `GET /integrations/{kind}/operations` | `integrations:read` | | Map a binding (open model) | `POST /agents/{agentId}/integrations/{kind}/bindings` | `skills:write` | | Auto-map a binding from a goal | `POST /agents/{agentId}/integrations/{kind}/bindings/auto` | `skills:write` | | Read / install a preset pack | `GET /integrations/{kind}/pack` · `POST /agents/{agentId}/integrations/{kind}/pack/install` | `skills:write` | | Remove provisioned skills | `DELETE /agents/{agentId}/integrations/{kind}/provision` | `skills:write` | | Disconnect a provider | `DELETE /integrations/{kind}` | `integrations:write` | `GET /integrations` and the catalog return **status only** — connected tokens are stored encrypted and never echoed back. Authenticate every call with `Authorization: Bearer flowyte_sk_…` (see [Authentication](/get-started/authentication)). Never send an organization header with an API key. ```bash theme={null} curl https://builder.flowyte.com/api/v1/integrations/catalog \ -H "Authorization: Bearer flowyte_sk_…" ``` # Shopify Source: https://docs.flowyte.com/integrations/shopify Answer "where is my order?" by looking up live order status as a skill. Connect your store, then provision an **order lookup** action as a [skill](/concepts/skills). When a caller asks where their order is (WISMO), the agent collects the order number or email, looks up the live status, and reads it back — no transfer to a human required. ## Connect with an API key Shopify connects with an API-key credential, so it completes in a single call — no browser hop. Pass the credential in the connect body; it's encrypted at rest and never echoed back. ```bash curl theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/shopify/connect \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "credentials": { "shop": "your-store.myshopify.com", "access_token": "shpat_…" } }' # → { "data": { "status": "connected" } } ``` ```js Node theme={null} const res = await fetch( "https://builder.flowyte.com/api/v1/integrations/shopify/connect", { method: "POST", headers: { Authorization: "Bearer flowyte_sk_…", "Content-Type": "application/json", }, body: JSON.stringify({ credentials: { shop: "your-store.myshopify.com", access_token: "shpat_…" }, }), }, ); const { data } = await res.json(); // { status: "connected" } ``` ```python Python theme={null} import requests res = requests.post( "https://builder.flowyte.com/api/v1/integrations/shopify/connect", headers={"Authorization": "Bearer flowyte_sk_…"}, json={"credentials": {"shop": "your-store.myshopify.com", "access_token": "shpat_…"}}, ) print(res.json()["data"]["status"]) # connected ``` A store-scoped admin API token (a custom app token) is the fastest path — no OAuth app to publish. The exact credential keys are listed on the connect form in the dashboard. ## Provision the lookup skill ```bash theme={null} # Browse the actions curl https://builder.flowyte.com/api/v1/integrations/shopify/actions \ -H "Authorization: Bearer flowyte_sk_…" # Provision onto an agent (omit "actions" to take every important one) curl -X POST \ https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/shopify/provision \ -H "Authorization: Bearer flowyte_sk_…" \ -H "Content-Type: application/json" \ -d '{ "actions": ["lookup_order"] }' ``` Lookups are read-only, so the agent answers without taking any action on the store. Re-running provision is idempotent — already-created skills return `created: false`. ## In the API | Action | Endpoint | Scope | | ----------------- | --------------------------------------------------------- | -------------------- | | Connect (API key) | `POST /integrations/shopify/connect` | `integrations:write` | | List actions | `GET /integrations/shopify/actions` | `integrations:read` | | Provision skills | `POST /agents/{agentId}/integrations/shopify/provision` | `skills:write` | | Remove skills | `DELETE /agents/{agentId}/integrations/shopify/provision` | `skills:write` | | Disconnect | `DELETE /integrations/shopify` | `integrations:write` | Provisioning edits the **draft**. [Publish](/concepts/agents) so live callers get real order status. # SQL database (Postgres & MySQL) Source: https://docs.flowyte.com/integrations/sql-database Connect your own Postgres or MySQL database and let the agent read (and optionally write) live records on a call — you choose the exact columns. Connect your own **PostgreSQL** or **MySQL** database and the agent can look up and update live records mid-call — check an appointment, confirm a balance, capture a request. Unlike the curated providers, a database has no pre-built actions: you **discover** its schema and **map** the exact queries you want as [skills](/concepts/skills). Nothing runs that you didn't author and approve. Authenticate every request with a secret API key: `Authorization: Bearer flowyte_sk_…`. The `kind` is `postgres` or `mysql`. ## How it fits together Generate the SQL to create a scoped user, then run it on your database — the agent never uses your admin credentials. Validate the credentials, then connect with them. They're encrypted at rest and never echoed back. Introspect the database, then optionally block tables/columns the agent must never see. Map a read (or a scoped write) onto a skill — a frozen, parameterized statement the agent calls. ## 1. Generate a scoped database user `GET /integrations/{kind}/sql/scripts` returns copy-paste SQL to create a **least-privilege** database user: a read-only role (SELECT only) and, if you need writes, a script scoped to **only** the tables your write skills touch — never blanket write. If you don't pass a password, a strong one is generated and returned once. ```bash theme={null} curl "https://builder.flowyte.com/api/v1/integrations/postgres/sql/scripts?database=appdb&tables=appointments" \ -H "Authorization: Bearer flowyte_sk_…" # → { "data": { "readOnly": { … }, "write": { … }, "generatedPassword": "…" } } ``` Run the returned scripts on your database as an admin, then use the scoped user's credentials below. ## 2. Test the connection `POST /integrations/{kind}/sql/test` runs an ordered set of checks — reachability and TLS, authentication, reading the schema, a timed read probe, a read-only-session proof, and a live-call latency verdict — **without storing anything**. It always returns `200` with the check results and an overall `ok` flag; a host that resolves to a private, loopback, or metadata address is refused. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/postgres/sql/test \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "credentials": { "host": "db.example.com", "port": "5432", "database": "appdb", "user": "flowyte_ro", "password": "…", "sslmode": "verify-full" } }' # → { "data": { "ok": true, "checks": [ … ] } } ``` `sslmode` must be `require`, `verify-ca`, or `verify-full` — `disable` is refused at the connect boundary. Prefer `verify-full` and supply `ca_pem` when your database uses a private CA. ## 3. Connect Pass the same credentials to the generic connect endpoint. The `kind` is `postgres` or `mysql`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/postgres/connect \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "credentials": { "host": "db.example.com", "port": "5432", "database": "appdb", "user": "flowyte_ro", "password": "…", "sslmode": "verify-full" } }' # → { "data": { "status": "connected" } } ``` ## 4. Discover the schema and scope it Introspect the database into a normalized schema, then read it (or search it) to find what to map: ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/postgres/discover \ -H "Authorization: Bearer flowyte_sk_…" curl "https://builder.flowyte.com/api/v1/integrations/postgres/search?q=appointment%20status" \ -H "Authorization: Bearer flowyte_sk_…" ``` Set **data scoping** to keep sensitive tables and columns out of reach — they disappear from the schema browser and are refused at bind time. Scoping is a privacy control you set per connection and it survives re-discovery. ```bash theme={null} curl -X PATCH https://builder.flowyte.com/api/v1/integrations/postgres/scoping \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "blockedTables": ["payments"], "blockedColumns": { "customers": ["ssn", "card_last4"] } }' ``` ## 5. Bind a query as a skill Map an operation onto a skill with [a binding](/guides/map-integration-fields): a **read** filters on one indexed column; a **write** is a single-row insert or an update-by-unique-key. The binding compiles to a frozen, parameterized statement — the agent supplies the parameters and never sees raw SQL or the full schema. Writes land disabled for review, and columns you scoped out are rejected. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/postgres/bindings \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "toolName": "Look up an appointment", "operation": "appointments.read", "inputs": [ { "param": "caller_phone", "arg": "phone", "required": true } ], "projection": [ { "path": ["appointment", "starts_at"], "leaf": "appointment_time" }, { "path": ["appointment", "status"], "leaf": "appointment_status" } ], "enabled": true }' ``` Prefer describing the goal instead? `POST /agents/{id}/integrations/postgres/bindings/auto` takes a plain-language goal and the AI assistant proposes the binding for you to review. ## In the API | Action | Endpoint | Scope | | ------------------------ | --------------------------------------------------------------------- | ------------------------------------------ | | Generate setup scripts | `GET /integrations/{kind}/sql/scripts` | `integrations:read` | | Test a connection | `POST /integrations/{kind}/sql/test` | `integrations:write` | | Connect | `POST /integrations/{kind}/connect` | `integrations:write` | | Discover the schema | `POST /integrations/{kind}/discover` | `integrations:write` | | Search / read the schema | `GET /integrations/{kind}/search` · `GET /integrations/{kind}/schema` | `integrations:read` | | Get / set data scoping | `GET` · `PATCH /integrations/{kind}/scoping` | `integrations:read` · `integrations:write` | | Bind a query as a skill | `POST /agents/{agentId}/integrations/{kind}/bindings` | `skills:write` | | Disconnect | `DELETE /integrations/{kind}` | `integrations:write` | Binding edits the **draft**. [Publish](/concepts/agents) so live callers get real data. Full walkthrough: [Connect a SQL database](/guides/connect-sql-database). # Zapier Source: https://docs.flowyte.com/integrations/zapier Take live write actions across 8,000+ apps through your Zapier MCP endpoint — book, update, log, notify — and recognize callers from records you sync into Flowyte. Zapier is the **universal fallback** connector for **write actions**. When Flowyte doesn't have a native connector for an app, you connect your **Zapier MCP endpoint** and the agent can take a live action on a call — book an appointment, update a ticket, log the call, notify your team — across the 8,000+ apps Zapier supports. Each equipped tool is **frozen** into an [`mcp` skill](/concepts/skills): you choose exactly which inputs the agent fills. Unlike the curated providers, Zapier ships **no pre-built actions** — every customer's server exposes a different set, so the "pack" is the discover-and-freeze flow itself. **To recognize callers or read their data, don't read live through Zapier — sync records into Flowyte instead.** A live Zapier action takes **10–13 seconds**, too slow to block a call while the agent waits for a lookup. Instead, sync your records into Flowyte's **caller-context store** (the Zapier **Create or Update Record** action, or the [Records API](/api-reference)) and the agent reads them **locally in under 100 ms** — no Zapier call, no per-call task cost. See [Recognize callers](/guides/connect-zapier#1-recognize-callers-sync-your-customers-in). Reserve live Zapier calls for **writes**. Authenticate every request with a secret API key: `Authorization: Bearer flowyte_sk_…`. The `kind` is `zapier`. You pay Zapier's task pricing on your own plan; Flowyte adds no markup. ## How it works Paste the MCP server URL from your Zapier account — no OAuth. Flowyte runs `initialize` + `tools/list` and reports how many actions are equipped. Read the tools you've equipped — plus whether it's a `classic` or `agentic` server. Pick the specific action; Flowyte resolves it to a concrete, closed schema, pinning any dynamic parents (spreadsheet → worksheet → column) along the way. Classic servers already expose a closed schema per tool, so this step is a no-op for them. Run a write once to see what it returns, so you can pick what the agent reads back (skip it for fire-and-forget sends that don't read anything). Pin the constant arguments, expose the ones the agent fills, and allow-list any fields it reads back. ## 1. Connect Zapier connects with an API-key-style credential: paste your Zapier **MCP server endpoint URL** (the secret is embedded in the URL). It's encrypted at rest and never echoed back. The `toolCount` in the response is how many actions are equipped — the connect wizard shows it (or a designed empty state at 0). ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/integrations/zapier/connect \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "credentials": { "endpoint": "https://mcp.zapier.com/api/mcp/s/…/mcp" } }' # → { "data": { "status": "connected", "toolCount": 12 } } ``` ## 2. List the equipped tools `GET /integrations/zapier/tools` runs `tools/list` against your endpoint and returns each equipped tool's name, description, and raw JSON-Schema `inputSchema` — the discovery step for authoring a skill. The envelope also carries **`serverMode`**: ```bash theme={null} curl https://builder.flowyte.com/api/v1/integrations/zapier/tools \ -H "Authorization: Bearer flowyte_sk_…" # → { "data": { "serverMode": "classic", "tools": [ { "name": "…", "inputSchema": { … } } ] } } ``` **`serverMode: "agentic"`** means your server exposes a few *generic* executor tools (`execute_zapier_read_action`, …) instead of one tool per action — and that's fully supported. Pick the specific action you want; Flowyte calls `POST /integrations/zapier/tools/{tool}/resolve` to resolve it to a concrete, closed, pinnable schema (materializing dynamic fields once their parents are pinned), then you freeze it exactly like a classic tool. See [Connect Zapier](/guides/connect-zapier). ## 3. Probe a tool (optional) An MCP tool has **no output schema** and Zapier has no sandbox, so to learn which fields a tool returns you run it once and inspect the response. `POST /integrations/{kind}/tools/{tool}/probe` **executes the real action** with your `args` — you must set `acknowledge_execution: true` — and returns the response flattened to dotted leaf paths (with a name-based PII class per leaf) to allow-list at freeze time. Probing **runs the real action** — it may create, send, or book something. ```bash theme={null} curl -X POST "https://builder.flowyte.com/api/v1/integrations/zapier/tools/find_contact/probe" \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "acknowledge_execution": true, "args": { "email": "sam@example.com" } }' # → { "data": { "leaves": [ { "path": "contact.name", "sample": "Sam Rivera", "pii": "name" }, … ] } } ``` ## 4. Freeze a tool into a skill `POST /agents/{agentId}/integrations/zapier/mcp-skills` turns one equipped tool into a skill. Only the operator-**exposed** params become the skill's parameters; **pinned** args are locked on at runtime. Every schema-required input must be pinned or exposed; any `allowedFields` the agent reads back (dotted leaf paths from the probe) are **deny-by-default**. Because an MCP tool carries no read/write metadata the skill lands **fail-closed as a write** — it confirms with the caller and runs one at a time — which is exactly right for the write actions Zapier is for. Bulk/broadcast-shaped tools are refused. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/zapier/mcp-skills \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "toolName": "update_ticket", "name": "Update the caller'\''s ticket", "exposedParams": ["status", "note"], "pinnedArgs": { "workspace": "acme" }, "allowedFields": ["ticket.id", "ticket.status"] }' ``` ### Send a write in the background (`fireAndForget`) Some write actions are pure **sends** — log the call, create a lead, add a note, tag a contact, fire a downstream Zap — where nothing on the call reads the result back. Zapier's action executor isn't fast (an **agentic** server's `execute_zapier_write_action` commonly takes **10+ seconds**), so blocking a live turn on one wastes time the caller can feel. Add `"fireAndForget": true` to the freeze request for a write action and: 1. the agent still **confirms with the caller first** — `fireAndForget` never touches the confirm gate, it only changes what happens *after* the caller says yes; 2. on the confirmed call, Flowyte hands the **exact same pinned + validated arguments** to a durable background queue and tells the caller it's **done** immediately, instead of blocking the turn on the live Zapier call; 3. a background worker completes the real send off-turn, with automatic retries and de-duplication (a retried confirmation is never sent twice); 4. if the send ultimately fails, the caller is **not** told — they're already gone. The failure shows up as a failed step on the call's [Observe](/concepts/observe) receipt and fires an internal alert, so an operator can follow up. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/AGENT_ID/integrations/zapier/mcp-skills \ -H "Authorization: Bearer flowyte_sk_…" -H "Content-Type: application/json" \ -d '{ "toolName": "log_call_note", "name": "Log this call in the CRM", "pinnedArgs": { "workspace": "acme" }, "fireAndForget": true }' ``` `fireAndForget` fits a write that **sends and forgets** — nothing on the call reads the result. Leave it off — the default — for a write whose result the agent reads back to the caller. To *read* caller data, use the [caller-context store](/guides/connect-zapier#1-recognize-callers-sync-your-customers-in), not a live Zapier call. Don't want to hand-author? Let **Flowyte Assist** do the probe-and-freeze — just describe the action you want. ## In the API | Action | Endpoint | Scope | | -------------------------------------------------------- | ------------------------------------------------------- | -------------------- | | Connect the MCP endpoint | `POST /integrations/zapier/connect` | `integrations:write` | | List equipped tools (`serverMode`) | `GET /integrations/zapier/tools` | `integrations:read` | | Resolve an action to a pinnable schema (agentic servers) | `POST /integrations/{kind}/tools/{tool}/resolve` | `integrations:read` | | Probe a tool (executes it) | `POST /integrations/{kind}/tools/{tool}/probe` | `integrations:write` | | Freeze a tool into a skill | `POST /agents/{agentId}/integrations/zapier/mcp-skills` | `skills:write` | | Subscribe to call events | `POST /agents/{agentId}/integrations/zapier/triggers` | `webhooks:write` | | Disconnect | `DELETE /integrations/zapier` | `integrations:write` | Freezing edits the **draft**. [Publish](/concepts/agents) so live callers get the new skill. Full walkthrough: [Connect Zapier](/guides/connect-zapier). # Quickstart Source: https://docs.flowyte.com/quickstart Mint an API key and build a working agent in about five minutes. This walks you from zero to a published agent you can talk to. Every step is a single API call against the base URL `https://builder.flowyte.com/api/v1`. All requests authenticate with a secret API key: `Authorization: Bearer flowyte_sk_…`. See [Authentication](/get-started/authentication) for how to mint one. ## 1. Create an agent ```bash curl theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Support" }' ``` ```ts Node theme={null} const res = await fetch("https://builder.flowyte.com/api/v1/agents", { method: "POST", headers: { Authorization: `Bearer ${process.env.FLOWYTE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Acme Support" }), }); const { data: agent } = await res.json(); ``` ```python Python theme={null} import os, requests r = requests.post( "https://builder.flowyte.com/api/v1/agents", headers={"Authorization": f"Bearer {os.environ['FLOWYTE_API_KEY']}"}, json={"name": "Acme Support"}, ) agent = r.json()["data"] ``` The response is the `ApiResponse` envelope. Capture `data.id` — that's your `agentId`. ## 2. Give it knowledge Add a source the agent can answer from. Knowledge ingestion is **asynchronous** — poll the source until its `status` is `indexed`. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge/sources \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "url", "label": "Help center", "url": "https://acme.com/help" }' # poll until indexed curl https://builder.flowyte.com/api/v1/agents/$AGENT_ID/knowledge/sources/$SOURCE_ID \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` ## 3. Publish The tester runs your **draft**; phone and chat run the last **published** version. Publish to freeze a version your channels can serve. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publish \ -H "Authorization: Bearer $FLOWYTE_API_KEY" ``` If you connect a channel before publishing, you'll get `409 no_published_version`. Publish first. ## 4. Test it Simulate a conversation. This streams over Server-Sent Events — read it with `fetch()` streaming and stop on the `event: done` frame. ```bash theme={null} curl -N -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/simulate \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Do you offer same-day appointments?", "draftMode": false }' ``` ## 5. Go live Put the agent on an embeddable chat widget by minting a publishable key (browser-safe, origin-allowlisted), or assign it a phone number from the Numbers API. ```bash theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/publishable-keys \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Website", "allowedOrigins": ["https://acme.com"] }' ``` Drop the returned loader snippet on your site and the agent is live. Learn how agents, skills, knowledge, guardrails, and playbooks fit together. # Errors Source: https://docs.flowyte.com/resources/errors The problem+json error shape, common status codes, and named problems you'll hit. When a request fails, Flowyte returns an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) **problem+json** body with the `application/problem+json` content type. The HTTP status is the source of truth; the body adds machine-readable detail. ## The problem shape ```json theme={null} { "type": "about:blank", "title": "Validation failure", "status": 400, "detail": "name is required", "instance": "/api/v1/agents", "code": "validation_error", "errors": [ { "field": "name", "message": "is required" } ] } ``` | Field | Meaning | | ---------- | ------------------------------------------------------------------ | | `status` | HTTP status code (also on the response). | | `title` | Short, human-readable summary of the problem type. | | `detail` | Explanation specific to this occurrence. | | `code` | Stable machine string for branching (e.g. `no_published_version`). | | `errors[]` | Field-level validation errors (`field`, `message`), when relevant. | Branch on `status` and `code`, not on `detail` or `title` — those wording may change. ## Common statuses | Status | When | | ----------------------- | --------------------------------------------------------------------------------------------- | | `400 Bad Request` | Malformed or invalid request body; `errors[]` lists the offending fields. | | `401 Unauthorized` | Missing or invalid key/session. Check `Authorization: Bearer flowyte_sk_…`. | | `403 Forbidden` | Org mismatch, insufficient scope, disallowed origin — or a **reserved** endpoint (see below). | | `404 Not Found` | The resource doesn't exist **in your org's scope**. | | `409 Conflict` | Duplicate name, a publish race, or a stale `If-Match` version. | | `422 Unprocessable` | Semantically invalid — e.g. a language your plan's speech engine doesn't support. | | `429 Too Many Requests` | Rate-limited. Honor `Retry-After` and the `RateLimit-*` headers. | ## Reserved endpoints return 403 Some endpoints in the contract are **reserved** (planned ahead of release). Calling one returns **`403`, not `404`** — the route exists but isn't open to you yet. If a correctly-authenticated, correctly-scoped call returns `403`, check whether the endpoint is reserved before assuming a permissions bug. The same `403` also covers genuine scope and cross-org denials. ## Named problems A few problems carry a specific `code` worth handling: | `code` | Status | Meaning | | --------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `no_published_version` | `409` | You requested a published-mode session (e.g. a non-draft talk token) but the agent has never been published. Publish it, or use `draftMode: true`. | | `integration_unavailable` | `400` | The provider's OAuth app isn't configured for this deployment yet. | | `integration_not_connected` | `409` | A config helper needs a connected provider (e.g. resolving a Sheet before Sheets is connected). | | `sheet_not_accessible` | `404` | The spreadsheet link is wrong or hasn't been shared with the connected account. | | `file_expired` | `404` | An uploaded `fileId` was garbage-collected before it was attached. Re-upload. | | `file_too_large` | `413` | The upload exceeds the 25 MB limit. | ## Optimistic concurrency (409 on `PATCH /agents/{id}`) Agent autosave uses an `If-Match` version. If you send a stale version, the server responds `409` **with the current agent state and version** in the body. Rebase: refetch, re-apply your pending edits, and retry with the new version. For status codes per endpoint, see the [API Reference](/api-reference/introduction). # Feature matrix Source: https://docs.flowyte.com/resources/feature-matrix Every platform capability — what it does, where it lives in the dashboard, its endpoints, and the scope it needs. One row per capability. Use it to map a thing you want to do to the dashboard area that configures it, the API endpoints behind it, and the [scope](/get-started/authentication) a secret key needs to call them. All paths are relative to `https://builder.flowyte.com/api/v1`. | Capability | What it does | Dashboard | Endpoints | Scope | | ------------------------ | ------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | **Agents** | The single entity callers reach; everything else attaches to it. Create, edit (draft), publish, roll back. | Agents | `GET/POST /agents` · `PATCH /agents/{id}` · `POST /agents/{id}/publish` · `POST /agents/{id}/rollback` | `agents:read` / `agents:write` | | **Voice** | Assign a catalog voice per language and browse/search voices. | Agent → Voice | `GET /voices` · `POST /voices/search` · `PUT /agents/{id}/voice` | `agents:read` / `agents:write` | | **Knowledge** | Add sources (URL, file, text) the agent answers from; ingestion is async. | Agent → Knowledge | `GET/POST /agents/{agentId}/knowledge/sources` · `POST …/knowledge/preview` | `knowledge:read` / `knowledge:write` | | **Skills** | Tools the agent calls mid-conversation (transfer, email, lookups, geo). | Agent → Skills | `GET /skill-types` · `GET/POST /agents/{agentId}/skills` · `PATCH/DELETE …/skills/{id}` | `skills:read` / `skills:write` | | **Integrations** | Connect a provider once, then provision its actions as skills. Live: Google Calendar, Google Sheets, Shopify. | Integrations | `GET /integrations` · `GET /integrations/catalog` · `POST /integrations/{kind}/connect` · `POST /agents/{agentId}/integrations/{kind}/provision` | `integrations:read` / `integrations:write` (+ `skills:write` to provision) | | **Guardrails** | Deterministic policies and caller-identity verification. | Agent → Guardrails | `GET/PUT /agents/{agentId}/guardrails` · `GET/PUT …/caller-verification` | `guardrails:read` / `guardrails:write` | | **Playbooks** | Optional node graph for scripted, step-by-step flows. | Agent → Playbooks | `GET/POST /agents/{agentId}/playbooks` · `GET/PUT …/playbooks/{id}/graph` | `playbooks:read` / `playbooks:write` | | **Numbers** | Search, reserve, purchase, import, and assign phone numbers. | Numbers | `GET /numbers/search` · `POST /numbers/reserve` · `POST /numbers/purchase` · `POST /numbers/{id}/assign` | `numbers:read` / `numbers:write` | | **Voice channel (PSTN)** | Inbound calls reach an assigned number; mint a talk token for the in-browser tester. | Agent → Test | `POST /agents/{agentId}/talk-token` · `POST /agents/{agentId}/simulate` | `agents:write` (talk token is session-only) | | **Chat channel** | Server-side chat sessions plus an OpenAI-compatible completions endpoint. | Agent → Test / Channels | `POST /chat/sessions` · `POST /chat/sessions/{id}/messages` · `POST /chat/completions` | `chat:read` / `chat:write` | | **Widget** | Embeddable browser chat, authenticated by a publishable key. | Agent → Channels | `GET/PUT /agents/{agentId}/widget` · `POST /chat/widget/sessions` | `widgets:read` / `widgets:write` · `pubkeys:*` | | **SMS (10DLC)** | Text messaging — available after per-org brand & campaign registration. | Numbers / Channels | *registration-gated* | `sms:read` / `sms:write` | | **Observe** | Post-call analytics, conversation history, transcripts, receipts, topics, knowledge gaps. | Observe | `GET /agents/{agentId}/analytics` · `GET /conversations` · `GET /conversations/{id}/receipt` · `GET /analytics/overview` | `analytics:read` · `calls:read` | | **Billing** | Wallet, top-ups, plans, usage, and invoices via Stripe. | Billing | `GET /billing/wallet` · `POST /billing/wallet/topup` · `GET /billing/usage` · `GET /billing/invoices` | `billing:read` / `billing:write` | | **API keys** | Authenticate the API. Keys are minted and managed in the dashboard Developer page. | Developer | — | — | | **Webhooks** | Subscribe to events with signed deliveries; send test events. | Developer | `GET/POST /webhooks` · `POST /webhooks/{id}/test` · `GET /webhooks/{id}/deliveries` | `webhooks:write` | | **Audit logs** | API and key activity history. | Developer | `GET /audit-logs` | `audit:read` | **DTMF** keypad input is supported on voice calls for capturing digits (menus, account numbers). Configure it on the agent — there's no separate endpoint. Changes always edit a **draft**. Phone and chat serve the last **published** version, so publish the agent after wiring up any capability above. See [draft vs published](/get-started/draft-vs-published). For the authoritative, machine-readable list, see the [API Reference](/api-reference/introduction). # Glossary Source: https://docs.flowyte.com/resources/glossary The core Flowyte terms, defined once. The vocabulary used across these docs and the API. Each term links to where it's covered in depth. ### Agent The single user-facing entity. An agent answers calls and chats; its persona, voice, [knowledge](/concepts/knowledge), [skills](/concepts/skills), [guardrails](/concepts/guardrails), and [playbooks](/concepts/playbooks) all attach to it. You build, publish, and observe one agent at a time. See [Agents](/concepts/agents). ### Skill A tool the agent can call mid-conversation to *do* something — transfer a call, send an email, book an appointment, look up an order, check a service area. A skill has a type and a configuration. See [Skills](/concepts/skills). ### Knowledge source A URL, file, or block of text the agent answers from. Ingestion is **asynchronous**: a new source starts in a pending state and becomes answerable once its status reaches `indexed`. Poll the source until then. See [Knowledge](/concepts/knowledge). ### Guardrail A deterministic policy that constrains what the agent does — what it must not say, when to escalate, and identity checks (caller verification) that gate sensitive answers. Guardrails run as rules, not suggestions. See [Guardrails](/concepts/guardrails). ### Playbook An optional node graph for scripted, step-by-step flows (collect, branch, end) when you need the conversation to follow an exact procedure rather than free-form reasoning. See [Playbooks](/concepts/playbooks). ### Draft / published Editing an agent changes its **draft**. Phone and chat always serve the last **published** version. Publishing freezes a version of the compiled config; **rollback** repoints to an earlier one. This is why a change can work in the tester but not on a live call until you publish. See [draft vs published](/get-started/draft-vs-published). ### Scope A permission string carried by a secret API key that gates which operations it can call (e.g. `agents:write`, `knowledge:write`, `analytics:read`). Request only the scopes you need. See [Authentication](/get-started/authentication). ### Publishable key A browser-safe key (`flowyte_pk_…`) used by the embeddable chat widget. It is **agent-pinned**, **origin-allowlisted**, and limited to public chat (`chat:public`) — safe to ship in client-side code, and it can never read tenant data. ### Receipt The ordered audit trail of a single conversation — every event the agent took (answers, skill calls, transfers), in sequence. Receipts back the transcript and let you see exactly what happened on a call. Retrieved via `GET /conversations/{id}/receipt`. ### Containment An analytics measure: the share of conversations the agent fully handled **without** transferring to a human. Higher containment means more calls resolved by the agent. It appears in [Observe](/resources/feature-matrix) alongside answer and transfer rates. Looking for an error term or status code? See [Errors](/resources/errors). For a capability → endpoint → scope map, see the [Feature matrix](/resources/feature-matrix). # After-hours & overflow answering Source: https://docs.flowyte.com/solutions/after-hours Catch the calls you'd otherwise miss — route the urgent ones, capture the rest. The calls you lose aren't during business hours — they're at 9pm, on a Sunday, or all at once when every line is busy. A Flowyte **agent** picks those up so an unanswered ring never becomes a lost customer. It knows when you're open, what to do while you're closed, and which calls truly can't wait. ## The two gaps it closes When you're closed, the agent still answers — books what it can, takes a message for the rest, and tells the caller when you reopen. When your team is on other calls, it answers in parallel instead of sending callers to voicemail. No busy signal, no queue. ## It knows when you're open Each agent carries your **business hours** — a timezone, a weekly open/closed grid, and one-off holidays. The agent is live-aware of whether you're open or closed and changes its behavior accordingly, so a closed Sunday and a busy Tuesday afternoon are handled differently and correctly. ## What happens while you're closed By default the agent keeps full functionality after hours — it can still answer questions, book, and capture requests. You decide what changes: * **Take a message** — capture the caller's details and email them to your team for the morning. * **Route urgent calls** — let self-declared emergencies (no heat, a leak, an outage) go to an on-call number, even when ordinary transfers are off. * **Hand off to a person** — turn on after-hours transfer to a specific number when someone is genuinely on call. Most businesses have no one to take a 2am call, so after-hours human transfer is **off by default** — the agent helps directly and leaves a clean message instead of ringing an empty desk. Turn it on only when an on-call person is real. ## In the API Business hours and after-hours behavior live on the agent. You set them with a normal agent update, then publish. | Action | Endpoint | Scope | | ----------------------------------------- | --------------------------- | -------------- | | Set business hours & after-hours behavior | `PATCH /agents/{id}` | `agents:write` | | Add a take-a-message skill | `POST /agents/{id}/skills` | `skills:write` | | Publish the change | `POST /agents/{id}/publish` | `agents:write` | | Review what you caught | `GET /agents/{id}/calls` | `calls:read` | Editing changes the **draft**; phone and chat serve the last **published** version. Publish after you adjust hours, or the live agent keeps the old schedule. See also: [warm transfer to your team](/solutions/warm-transfer) and the [AI answering service](/solutions/answering-service). # AI answering service for HVAC & field service Source: https://docs.flowyte.com/solutions/answering-service Answer every call 24/7, recognize customers, capture new jobs into your field-service software, and hand the urgent ones to a person. Missed calls are missed jobs. For HVAC, plumbing, and trades, the phone rings during a service call, after hours, and all at once on the first cold morning of the season. A Flowyte **agent** answers every one — in a natural voice, around the clock — and works straight out of your **field-service software** instead of just taking a message. ## What it does on a call Picks up on the first ring with your greeting, in the caller's language. No hold music, no voicemail. Looks them up by phone, greets them by name, and can confirm the property on file. Collects the details and opens a request in your field-service software, tied to the customer — your office turns it into a scheduled job. Checks the customer's jobs and reads back their next scheduled visit. ## Backed by your field-service software Connect your field-service account once, then provision its actions as **skills** on the agent. Each becomes a tool the agent uses mid-call: | Skill | What the agent does | | ---------------- | ----------------------------------------------------- | | `find_client` | Recognize a returning customer and pull up their jobs | | `create_lead` | Capture a new service request in your system | | `get_job_status` | Tell a caller when their technician is coming | For a burst pipe or a no-heat call, a **transfer** skill routes straight to your on-call person. Add your hours, pricing, and FAQs as [knowledge](/concepts/knowledge) so it answers from your real policies. Want to gate by location? A free [geo](/concepts/skills) skill checks whether the caller's address is in your service area. ## Build it `POST /integrations/{kind}/connect` returns an authorization link; approve it once. Find your provider's `kind` in `GET /integrations/catalog`. `POST /agents/{id}/integrations/{kind}/provision` turns the connected account's actions into skills on the agent. Feed it your service area, pricing, and FAQs, and add a transfer for emergencies. Publish a version, test it with the simulator, then point a phone number at it. Full walkthrough: [Build an HVAC answering agent](/guides/build-hvac-agent). See also: [after-hours & overflow](/solutions/after-hours) and [warm transfer to your team](/solutions/warm-transfer). # Order status & WISMO Source: https://docs.flowyte.com/solutions/order-status Resolve "where is my order?" on voice and chat with a connected Shopify store. "Where is my order?" is the question that floods support after every shipment. A Flowyte **agent** answers it for you — on the phone, in chat, and in the website widget — by looking the order up in your **Shopify** store in real time and reading back the status. No ticket, no wait, no human required for the routine 80%. ## What the customer gets The agent looks up the actual order and tells the customer its status — not a canned "check your email." The same lookup works on a phone call, a chat session, and the embeddable widget on your site. Pair it with caller verification so it only reveals order details to the right person. A damaged or lost package can hand off to a person, with the order context already gathered. ## How it's built Connect **Shopify** once with a store credential. Tokens are stored encrypted — never echoed back. Turn the store's order-lookup action into a [skill](/concepts/skills) on your agent. The agent now collects an order number or email and fetches the status itself. Serve it on the phone, on the [chat](/concepts/agents) channel, or drop the widget on your storefront. An OpenAI-compatible completions endpoint is also available for your own app. Publish the agent so every channel serves the new capability. ## In the API | Action | Endpoint | Scope | | ------------------------------ | ------------------------------------------------- | -------------------- | | Connect the store | `POST /integrations/{kind}/connect` | `integrations:write` | | Provision lookup as a skill | `POST /agents/{id}/integrations/{kind}/provision` | `skills:write` | | Publish the agent | `POST /agents/{id}/publish` | `agents:write` | | Answer over chat (server-side) | `POST /chat/sessions/{id}/messages` | `chat:write` | | Drop-in for your own app | `POST /chat/completions` | `chat:write` | Order lookup runs against your live store, so a customer always hears the current status. Add [guardrails](/concepts/guardrails) to verify identity before disclosing order details. See also: the [AI answering service](/solutions/answering-service) and [warm transfer to your team](/solutions/warm-transfer). # Take phone orders with a delivery-area check Source: https://docs.flowyte.com/solutions/restaurant Answer menu questions, take the order, confirm it's in your delivery zone, and send it to the kitchen — by phone. A busy pizzeria can't pick up every call at the dinner rush — and every missed call is an order that went to the place down the street. A Flowyte **agent** answers, knows your menu, takes the order, and only promises delivery when the address is actually in your zone. ## What it does on a call Answers "what's on the supreme?" and "how much is a large?" from your real menu — no guessing. Before promising delivery, it checks the caller's address against your zone and offers pickup if it's out of range. Collects items, size, and any notes, reads the total back, and confirms. Posts the finished order to your kitchen display or order system the moment it's confirmed. ## How it's built Three pieces on one agent: Add your menu (a URL, a PDF, or pasted text) as [knowledge](/concepts/knowledge). The agent answers menu and price questions from it. Add a free [geo](/concepts/skills) skill with the `check_service_area` action — your shop address as the origin and your delivery radius. The agent asks for the address and confirms coverage before taking a delivery order. Add an `http_webhook` skill that posts the confirmed order to your kitchen display or POS (or an email skill if you'd rather it land in an inbox). ```bash Delivery-area skill theme={null} curl -X POST https://builder.flowyte.com/api/v1/agents/$AGENT_ID/skills \ -H "Authorization: Bearer $FLOWYTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Check delivery area", "description": "Check whether the caller's delivery address is within our delivery zone before taking a delivery order.", "skillType": "geo", "executionConfig": { "action": "check_service_area", "originAddress": "120 Main St, Boulder, CO", "radiusMiles": 5 } }' ``` Geometry is free — you're only charged when the agent has to look up a caller's address. See the full build in [Take an order with a delivery check](/guides/take-orders). # Warm transfer to your team Source: https://docs.flowyte.com/solutions/warm-transfer Let the agent handle routine calls and hand off cleanly to a person when it should. Not every call should reach a human — but some absolutely should. A Flowyte **agent** handles the routine volume (hours, pricing, booking, status) and recognizes the moment a caller needs a real person. When it does, it doesn't dump the call: it has already gathered who's calling and why, then routes them to the right number with their original caller ID intact. ## Why it's "warm" The agent does the qualifying work before the handoff, so your team doesn't start cold: The agent collects the caller's name, reason, and any details it needs, so the person who picks up isn't starting from zero. A sales question, a billing issue, and an emergency each go to a different number — based on what the caller actually said. Your team sees the customer's real number on the transferred call, not the platform's. Routine calls stay contained. The agent transfers when its rules say to — not by default. ## How routing decisions are made You set up two layers: * A **default handoff destination** — where a transfer goes when nothing more specific matches. * **Transfer-by-context rules** — a small table that maps a described situation ("caller wants to speak to billing", "caller reports an emergency") to a specific phone number. The agent matches the live conversation to a rule and transfers there. This is a **skill** the agent calls mid-conversation. Because routing is data, the same pattern scales from one front desk to thousands of locations — look up the right destination, then transfer. ## In the API | Action | Endpoint | Scope | | --------------------------------------- | --------------------------- | -------------- | | Set default destination & context rules | `PATCH /agents/{id}` | `agents:write` | | Add a transfer skill | `POST /agents/{id}/skills` | `skills:write` | | Publish | `POST /agents/{id}/publish` | `agents:write` | | See your transfer rate | `GET /agents/{id}/calls` | `calls:read` | Pair this with [guardrails](/concepts/guardrails) so the agent verifies a caller before it ever transfers sensitive requests, and with [skills](/concepts/skills) it can try first so only the calls that truly need a person reach one. See also: [after-hours & overflow answering](/solutions/after-hours) and the [AI answering service](/solutions/answering-service). # Trust & Security Source: https://docs.flowyte.com/trust/security How Flowyte handles your data, recordings, and caller PII — and how to report a vulnerability. Flowyte runs production voice and chat agents, which means we handle call audio, transcripts, and whatever a caller says out loud. This page describes how that data is protected, what your compliance obligations are, and how to reach us. It states our posture honestly — where something is in progress, we say so. ## Tenant isolation Every record belongs to one organization. Each request — whether it carries a dashboard session or a secret API key — resolves to a single tenant-scoped identity, and database row-level security enforces that boundary on every query. **A key can never read another organization's data**, even if it asks for a resource by id. ## Call recording & consent Voice calls can be recorded and transcribed so you can review them in **Observe**. Recordings are retrieved through short-lived signed URLs, not public links. You are the controller of your callers' data and are responsible for disclosing recording where the law requires it. Many jurisdictions require **two-party (all-party) consent** before a call is recorded. Configure your agent's greeting (or an opening notice) to announce recording when you operate in those regions, and confirm your obligations with counsel. You decide what the agent says, including any recording notice, and you can disable retention of recordings on request. ## Data handling & PII * **In transit:** all API traffic is served over TLS. * **At rest:** sensitive secrets — such as integration access tokens — are encrypted with authenticated encryption (AES-256-GCM) and are never returned by the API. Token DTOs surface status only. * **Minimize what you collect.** Only capture the caller fields a skill actually needs. * **Verify before disclosing.** Use a [caller-verification guardrail](/concepts/guardrails) to gate sensitive answers behind identity checks. * **Secrets show once.** API-key and webhook-signing secrets are returned a single time at creation. Store them in your secret manager. ## Data residency & retention Recordings, transcripts, and the per-call audit trail are retained so analytics and receipts work. Retention windows and regional hosting options depend on your plan — **contact us** to configure a specific residency or retention requirement, or to request deletion of stored conversation data. ## Sub-processors Flowyte relies on third-party infrastructure to deliver the service. We describe them by **category** rather than by name; the current named list is available under NDA on request. | Category | Purpose | | ------------------------------ | ------------------------------------------------------- | | Cloud hosting & infrastructure | Compute, storage, and managed databases | | Telephony carrier | PSTN connectivity, phone numbers, call routing | | Speech & language models | Real-time voice synthesis, transcription, and reasoning | | Payment processing (Stripe) | Checkout, wallet top-ups, invoices | | Email & notification delivery | Outbound notification skills | | Product analytics | Usage metrics and session replay in the dashboard | We review sub-processors before onboarding them and maintain a Data Processing Addendum (DPA), available on request. ## Compliance posture We aim to be precise here rather than aspirational: * **SOC 2:** an audit is **in progress**. We do not yet hold a completed report — contact us for current status and to be notified when it is available. * **GDPR / CCPA:** we support data-subject and deletion requests and provide a DPA on request. * **A2P 10DLC:** SMS sending requires per-organization brand and campaign registration before messages are delivered (a regulatory requirement, not a Flowyte limitation). If a compliance questionnaire or security review is part of your procurement, reach out and we'll work through it with you. ## Reporting a vulnerability If you believe you've found a security issue, email **[security@flowyte.com](mailto:security@flowyte.com)** with steps to reproduce. Please do not publicly disclose the issue until we've had a chance to investigate and respond. We welcome good-faith research and will not pursue action against researchers who act responsibly and avoid privacy violations or service disruption.