# BloomText API documentation > Full text of every page at https://www.bloomtext.com/developers/api/. Base URL https://api.bloomtext.com/v1. OpenAPI spec: https://www.bloomtext.com/developers/api/openapi/bloomtext-api.yaml. # BloomText API > Read and send messages in BloomText conversations from your own software, AI agents, and scripts. Source: https://www.bloomtext.com/developers/api/ The BloomText API lets your software take part in BloomText conversations. Read messages as they arrive, reply in threads, react, manage who's in a conversation, check broadcast delivery, and export conversation history, all over a REST API with JSON bodies. Your integration acts as an **app user**: a named, non-human member of your organization. Your team sees it in conversations like any other participant, and it can only reach the conversations it has been added to. - [Quickstart](https://www.bloomtext.com/developers/api/quickstart/) - [API reference](https://www.bloomtext.com/developers/api/reference/) - [Webhooks](https://www.bloomtext.com/developers/api/webhooks/) - [Build an AI agent](https://www.bloomtext.com/developers/api/ai-agents/) ## What you can build - **Reminders and notifications:** post appointment reminders or intake nudges into a patient's conversation from your EMR or scheduling system. See [Send appointment reminders](https://www.bloomtext.com/developers/api/guides/appointment-reminders/). - **Two-way workflows:** turn patient replies into actions in your own system, like confirming an appointment. See [Handle patient replies](https://www.bloomtext.com/developers/api/guides/handle-replies/). - **AI assistants:** let an agent answer routine questions and hand anything clinical to your staff. See [Build an AI agent](https://www.bloomtext.com/developers/api/ai-agents/). - **Records and compliance:** export a conversation's history on a schedule and file it with the patient's chart. See [Export conversation history](https://www.bloomtext.com/developers/api/guides/export-history/). ## How it works ```mermaid sequenceDiagram participant App as Your app participant API as BloomText API participant Staff as Your staff App->>API: POST /conversations/{id}/messages API-->>Staff: Message appears in the conversation Staff->>API: Staff or patient replies API-->>App: conversation.message.created webhook App->>API: GET /conversations/{id}/messages ``` 1. An organization admin creates an app user for your integration and issues it an [API key](https://www.bloomtext.com/developers/api/authentication/). 2. Admins add the app user to the conversations it should work in. 3. Your app reads and writes those conversations with the API, and listens for changes with [webhooks](https://www.bloomtext.com/developers/api/webhooks/). ## Access API keys are issued to organizations with a signed BAA. [Request API access](https://calendly.com/tyler-bloom/bloomtext-homepage-demo-request?utm_campaign=api-access) and we'll set up your app user and first key. > **Warning:** Don't include patient information when you request access. The form isn't a secure channel. --- # Quickstart > Make your first BloomText API request and send a message in five minutes. Source: https://www.bloomtext.com/developers/api/quickstart/ This guide takes you from an API key to a message in a real BloomText conversation. You'll list the conversations your app user can see, then post into one. ### Get an API key API keys are issued to organizations with a signed BAA. [Request API access](https://calendly.com/tyler-bloom/bloomtext-homepage-demo-request?utm_campaign=api-access) if you don't have one yet. Once your organization is enabled, an admin creates an app user for your integration and gives you its key. Store the key in an environment variable. Never commit it or ship it to a browser. ```bash filename="Terminal" export BLOOMTEXT_API_KEY="bt_live_4f7c2a9e1b..." ``` ### Add your app user to a conversation In BloomText, open the conversation you want to test with and add your app user as a participant, like you'd add a colleague. The API only returns conversations your app user belongs to. > **Note:** Use an internal staff conversation for testing, not one with a patient. ### List your conversations ```bash filename="Request" curl https://api.bloomtext.com/v1/conversations \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="list-conversations.mjs" const response = await fetch('https://api.bloomtext.com/v1/conversations', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}` }, }) const { data } = await response.json() console.log(data) ``` ```python filename="list_conversations.py" import os import requests response = requests.get( "https://api.bloomtext.com/v1/conversations", headers={"Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}"}, ) response.raise_for_status() print(response.json()["data"]) ``` ```go filename="main.go" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.bloomtext.com/v1/conversations", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```ruby filename="list_conversations.rb" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/conversations") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body)["data"] ``` The response lists every conversation your app user is in. Copy the `id` of your test conversation. ```json filename="Response · 200 OK" { "data": [ { "id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "type": "group", "created_at": "2026-09-14T16:02:11Z" } ], "pagination": { "next_cursor": null, "has_more": false } } ``` ### Send a message Post a message into that conversation. The `Idempotency-Key` header makes the request safe to retry. See [Idempotency](https://www.bloomtext.com/developers/api/idempotency/). ```bash filename="Request" curl -X POST https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"body": "Hello from the BloomText API 👋"}' ``` ```js filename="send-message.mjs" const conversationId = 'e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0' const response = await fetch(`https://api.bloomtext.com/v1/conversations/${conversationId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({ body: 'Hello from the BloomText API 👋' }), }) console.log(await response.json()) ``` ```python filename="send_message.py" import os import uuid import requests conversation_id = "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0" response = requests.post( f"https://api.bloomtext.com/v1/conversations/{conversation_id}/messages", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={"body": "Hello from the BloomText API 👋"}, ) response.raise_for_status() print(response.json()) ``` ```go filename="main.go" package main import ( "bytes" "fmt" "io" "net/http" "os" "github.com/google/uuid" ) func main() { conversationID := "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0" url := "https://api.bloomtext.com/v1/conversations/" + conversationID + "/messages" body := bytes.NewBufferString(`{"body":"Hello from the BloomText API 👋"}`) req, _ := http.NewRequest("POST", url, body) req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) req.Header.Set("Idempotency-Key", uuid.NewString()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="send_message.rb" require "net/http" require "json" require "securerandom" conversation_id = "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0" uri = URI("https://api.bloomtext.com/v1/conversations/#{conversation_id}/messages") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" request["Idempotency-Key"] = SecureRandom.uuid request["Content-Type"] = "application/json" request.body = { body: "Hello from the BloomText API 👋" }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body) ``` You get back the new [Message object](https://www.bloomtext.com/developers/api/reference/message-object/), and the message shows up in BloomText under your app user's name. ```json filename="Response · 201 Created" { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "created_at": "2026-09-21T15:04:05Z", "type": "text", "body": "Hello from the BloomText API 👋", "file": null, "reply_to_message_id": null } ``` ## Next steps - [Listen with webhooks](https://www.bloomtext.com/developers/api/webhooks/) - [Understand scopes](https://www.bloomtext.com/developers/api/authentication/) - [Browse the API](https://www.bloomtext.com/developers/api/reference/) --- # Core concepts > App users, conversations, and the other building blocks of the BloomText API. Source: https://www.bloomtext.com/developers/api/concepts/ A few ideas explain almost everything about how the API behaves. Read this page once and the rest of the docs will make sense. ## Organizations Everything in BloomText belongs to an organization: a practice, clinic, agency, or health system. An API key is issued to one organization and can never see data from another. [Retrieve the organization](https://www.bloomtext.com/developers/api/reference/get-organization/) to confirm which one a key belongs to. ## App users Every API key belongs to an **app user**, a named member of your organization that represents your software, like "Intake Bot" or "Scheduling Sync". - It shows up as the sender of every message it writes, so staff always know what came from an integration. - It can't sign in to BloomText or administer the organization. - It sees only the conversations it has been added to. > **Note:** Create one app user per integration. If you run a reminder service and an AI assistant, give each its own app user and key so you can scope, audit, and revoke them separately. ## Conversations and membership A [conversation](https://www.bloomtext.com/developers/api/reference/conversation-object/) is a thread between people in your organization and, often, a patient or family member. Conversations are `direct`, `group`, or `broadcast`. **Membership is the access boundary.** Your app user can read and write a conversation only while it's a [participant](https://www.bloomtext.com/developers/api/reference/participant-object/). Scopes decide *what* a key can do; membership decides *where*. ```mermaid flowchart LR Key[API key] -->|belongs to| AppUser[App user] AppUser -->|participant in| C1[Conversation A] AppUser -->|participant in| C2[Conversation B] AppUser -.->|not a participant| C3[Conversation C] style C3 stroke-dasharray: 4 4 ``` ## Messages, replies, and reactions - A [message](https://www.bloomtext.com/developers/api/reference/message-object/) carries either text (`body`) or a file (`file`). - A reply is a message with `reply_to_message_id` set. [List replies](https://www.bloomtext.com/developers/api/reference/list-replies/) returns a message's thread. - [Reactions](https://www.bloomtext.com/developers/api/reference/reaction-object/) are emoji on a message. They have no ID of their own; each is identified by the message, the user, and the emoji. ## Broadcasts A [broadcast](https://www.bloomtext.com/developers/api/reference/broadcast-object/) is a one-to-many campaign your staff send from BloomText. The API lets you read broadcasts and their delivery status. Reading a broadcast never grants access to the conversations it was sent to. ## Exports An [export](https://www.bloomtext.com/developers/api/reference/export-object/) packages a conversation's history into a file. Exports run asynchronously: start one, then poll it or listen for the `export.ready` [webhook](https://www.bloomtext.com/developers/api/webhooks/) to get a short-lived download URL. ## IDs, timestamps, and requests | Convention | Detail | | --- | --- | | Base URL | `https://api.bloomtext.com/v1` | | IDs | Every object has a stable UUID `id`. | | Timestamps | RFC 3339 strings in UTC, like `2026-09-21T12:00:00Z`. | | Bodies | JSON in and out. Send `Content-Type: application/json`. | | Lists | Cursor-paginated. See [Pagination](https://www.bloomtext.com/developers/api/pagination/). | | Writes | Every `POST` takes an `Idempotency-Key`. See [Idempotency](https://www.bloomtext.com/developers/api/idempotency/). | | Errors | RFC 9457 problem details. See [Errors](https://www.bloomtext.com/developers/api/errors/). | --- # Authentication > Authenticate BloomText API requests with organization-scoped API keys, scopes, and revocation. Source: https://www.bloomtext.com/developers/api/authentication/ Every request is authenticated with an API key sent as a Bearer token: ```http filename="Header" Authorization: Bearer bt_live_4f7c2a9e1b... ``` Requests without a valid key return `401 Unauthorized`. All requests must use HTTPS. ## Getting a key API keys are issued to organizations with a signed BAA. [Request API access](https://calendly.com/tyler-bloom/bloomtext-homepage-demo-request?utm_campaign=api-access) to get started. ### Your organization is enabled After the BAA is signed, we turn on API access for your organization. ### An admin creates an app user The app user represents your integration in conversations. Give it a clear name, like "Intake Bot". ### The admin issues a key with scopes Pick only the [scopes](#scopes) your integration needs. The key is shown once, so store it in your secrets manager straight away. > **Warning:** Keep keys server-side. Never put a key in a browser, a mobile app, a public repository, or an AI prompt that leaves your infrastructure. ## Scopes Each key carries the scopes an admin grants it. A request outside the key's scopes returns `403` with `code: insufficient_scope`. Every endpoint in the [API reference](https://www.bloomtext.com/developers/api/reference/) lists the scope it needs. | Scope | Allows | Endpoints | | --- | --- | --- | | `organization:read` | Read the organization | [Retrieve the organization](https://www.bloomtext.com/developers/api/reference/get-organization/) | | `users:read` | List and read members | [List users](https://www.bloomtext.com/developers/api/reference/list-users/), [Retrieve a user](https://www.bloomtext.com/developers/api/reference/get-user/) | | `conversations:read` | List and read conversations | [List](https://www.bloomtext.com/developers/api/reference/list-conversations/), [Retrieve](https://www.bloomtext.com/developers/api/reference/get-conversation/) | | `messages:read` | Read messages and replies | [List messages](https://www.bloomtext.com/developers/api/reference/list-messages/), [List replies](https://www.bloomtext.com/developers/api/reference/list-replies/) | | `messages:write` | Send messages and replies | [Send a message](https://www.bloomtext.com/developers/api/reference/create-message/) | | `reactions:read` | Read reactions | [List reactions](https://www.bloomtext.com/developers/api/reference/list-reactions/) | | `reactions:write` | Add and remove reactions | [Add](https://www.bloomtext.com/developers/api/reference/create-reaction/), [Remove](https://www.bloomtext.com/developers/api/reference/delete-reaction/) | | `participants:read` | List participants | [List participants](https://www.bloomtext.com/developers/api/reference/list-participants/) | | `participants:write` | Add and remove participants | [Add](https://www.bloomtext.com/developers/api/reference/add-participant/), [Remove](https://www.bloomtext.com/developers/api/reference/remove-participant/) | | `broadcasts:read` | Read broadcasts | [List](https://www.bloomtext.com/developers/api/reference/list-broadcasts/), [Retrieve](https://www.bloomtext.com/developers/api/reference/get-broadcast/), [Messages](https://www.bloomtext.com/developers/api/reference/list-broadcast-messages/) | | `exports:read` | Check and download exports | [Retrieve an export](https://www.bloomtext.com/developers/api/reference/get-chat-export/) | | `exports:write` | Start exports | [Export a conversation](https://www.bloomtext.com/developers/api/reference/create-chat-export/) | ### Common scope sets Posts reminders and nothing else. ```text conversations:read messages:write ``` Reads incoming messages, replies, and reacts to acknowledge. ```text conversations:read messages:read messages:write reactions:write ``` Exports conversation history on a schedule. ```text conversations:read exports:write exports:read ``` ## Keys for AI agents An AI agent acts on its own judgment, so give it less access than a script you wrote line by line. ### Give the agent its own app user Never share a key between an agent and another integration. A dedicated app user, named so staff recognize it (like "Scheduling Assistant"), lets you audit and revoke the agent on its own. ### Start read-only Begin with `conversations:read` and `messages:read`. Have the agent post drafts to a staff group for review, and add `messages:write` for patient conversations only once you trust its output. ### Limit where it can go Add the app user only to the conversations the agent needs. Membership is the boundary: an agent can't read or post anywhere it hasn't been added. ### Keep the key out of the model Store the key in your agent's secret store or environment, like `BLOOMTEXT_API_KEY`. Never put it in a prompt, a tool description, or chat history. > **Warning:** The model provider behind your agent sees whatever the agent reads. If the agent can read patient conversations, the provider needs a BAA with your organization. See [Build an AI agent](https://www.bloomtext.com/developers/api/ai-agents/) and the [MCP server](https://www.bloomtext.com/developers/api/mcp/) for the full setup. ## Conversation access Scopes decide *what* a key can do. [Conversation membership](https://www.bloomtext.com/developers/api/concepts/#conversations-and-membership) decides *where*. A key can read or write a conversation only while its app user is a participant, and a request for any other conversation returns `403` with `code: conversation_membership_required`. Adding a participant with `participants:write` is limited to existing organization members. It never widens what your app user can see. ## Rotating and revoking keys - **Rotate** by issuing a second key for the same app user, deploying it, then revoking the old one. Both work during the overlap. - **Revoke** from BloomText at any time. Revocation takes effect on the next request. - **Remove** the app user from a conversation to cut off just that conversation. ## Verify your key A quick way to check a key works and see which organization it belongs to: ```bash curl https://api.bloomtext.com/v1/organization \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js const response = await fetch('https://api.bloomtext.com/v1/organization', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}` }, }) console.log(response.status, await response.json()) ``` ```python import os import requests response = requests.get( "https://api.bloomtext.com/v1/organization", headers={"Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}"}, ) print(response.status_code, response.json()) ``` ```go req, _ := http.NewRequest("GET", "https://api.bloomtext.com/v1/organization", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) ``` ```ruby uri = URI("https://api.bloomtext.com/v1/organization") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } ``` ```json filename="Response · 200 OK" { "id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "name": "Riverside Family Clinic" } ``` --- # Webhooks > Receive signed BloomText webhook events when messages, reactions, participants, or exports change. Source: https://www.bloomtext.com/developers/api/webhooks/ Webhooks push events to your HTTPS endpoint as they happen, so you don't have to poll. Use them to react to new patient messages, keep your records in sync, or trigger an AI agent. ```mermaid sequenceDiagram participant BT as BloomText participant You as Your endpoint BT->>You: POST event + X-BloomText-Signature You->>You: Verify signature, deduplicate by id You-->>BT: 200 OK (within 5 seconds) You->>You: Process the event asynchronously ``` ## Events | Event | Sent when | | --- | --- | | [`conversation.message.created`](https://www.bloomtext.com/developers/api/reference/webhook-events/#conversationmessagecreated) | A message is sent in a conversation your app user is in. | | [`message.reaction.created`](https://www.bloomtext.com/developers/api/reference/webhook-events/#messagereactioncreated) | A reaction is added to a message. | | [`message.reaction.deleted`](https://www.bloomtext.com/developers/api/reference/webhook-events/#messagereactiondeleted) | A reaction is removed. | | [`conversation.participant.added`](https://www.bloomtext.com/developers/api/reference/webhook-events/#conversationparticipantadded) | Someone joins a conversation. | | [`conversation.participant.removed`](https://www.bloomtext.com/developers/api/reference/webhook-events/#conversationparticipantremoved) | Someone leaves or is removed. | | [`export.ready`](https://www.bloomtext.com/developers/api/reference/webhook-events/#exportready) | An export has finished. | You only receive events for conversations your app user participates in. See the [webhook events reference](https://www.bloomtext.com/developers/api/reference/webhook-events/) for every payload. ## Set up an endpoint ### Build a receiver Expose an HTTPS `POST` route, like `https://example.com/webhooks/bloomtext`. It must read the **raw request body** before any JSON parsing, because the signature covers the exact bytes. ### Register the URL An organization admin adds the URL to your app user in BloomText and picks the events to send. BloomText shows the endpoint's **signing secret** once; store it as `BLOOMTEXT_WEBHOOK_SECRET`. ### Verify every delivery Reject anything that fails [signature verification](#verify-signatures) before you trust it. ### Respond fast, process later Return a `2xx` within 5 seconds, then do the real work in a background job. ## The event envelope Every delivery has the same shape. `payload` varies by event type and carries IDs, not message content. ```json filename="conversation.message.created" { "id": "2d8f1b0b-7b27-4b3b-bf2c-4d05d89d2f4d", "type": "conversation.message.created", "occurred_at": "2026-09-21T12:00:00Z", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "delivery_attempt": 1, "payload": { "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "message_id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55" } } ``` > **Note:** Payloads deliberately leave out message text and patient details. Fetch the message with [List messages](https://www.bloomtext.com/developers/api/reference/list-messages/) when you need its content, so your key's scopes and membership still apply. ## Verify signatures Every delivery includes a signature header: ```http filename="Header" X-BloomText-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` To verify it: 1. Split the header into `t` (Unix seconds) and `v1` (hex digest). 2. Reject the request if `t` is more than 300 seconds from now. This blocks replayed deliveries. 3. Compute HMAC-SHA256 of `{t}.{raw_request_body}` with your signing secret. 4. Compare your digest to `v1` in constant time. Only then parse the JSON. ```js filename="server.mjs" import crypto from 'node:crypto' import express from 'express' const app = express() // express.raw keeps the exact bytes BloomText signed. app.post('/webhooks/bloomtext', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('X-BloomText-Signature') if (!verify(req.body, header, process.env.BLOOMTEXT_WEBHOOK_SECRET)) { return res.sendStatus(400) } const event = JSON.parse(req.body) // Deduplicate by event.id, store the event, then process it in the background. res.sendStatus(200) }) function verify(rawBody, header, secret) { if (!header) return false const parts = Object.fromEntries(header.split(',').map((part) => part.split('='))) if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.`).update(rawBody).digest('hex') return expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)) } app.listen(3000) ``` ```python filename="app.py" import hashlib import hmac import os import time from flask import Flask, abort, request app = Flask(__name__) SECRET = os.environ["BLOOMTEXT_WEBHOOK_SECRET"].encode() def verify(raw_body: bytes, header: str | None) -> bool: if not header: return False parts = dict(part.split("=", 1) for part in header.split(",")) timestamp, signature = parts.get("t"), parts.get("v1") if not timestamp or not signature or abs(time.time() - int(timestamp)) > 300: return False expected = hmac.new(SECRET, f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) @app.post("/webhooks/bloomtext") def bloomtext_webhook(): if not verify(request.get_data(), request.headers.get("X-BloomText-Signature")): abort(400) event = request.get_json() # Deduplicate by event["id"], store the event, then process it in the background. return "", 200 ``` ```go filename="main.go" package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "os" "strconv" "strings" "time" ) func verify(body []byte, header, secret string) bool { var timestamp, signature string for _, part := range strings.Split(header, ",") { key, value, _ := strings.Cut(part, "=") switch key { case "t": timestamp = value case "v1": signature = value } } ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || signature == "" { return false } if age := time.Now().Unix() - ts; age > 300 || age < -300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) } func webhook(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil || !verify(body, r.Header.Get("X-BloomText-Signature"), os.Getenv("BLOOMTEXT_WEBHOOK_SECRET")) { http.Error(w, "invalid signature", http.StatusBadRequest) return } var event map[string]any _ = json.Unmarshal(body, &event) // Deduplicate by event["id"], store the event, then process it in the background. w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhooks/bloomtext", webhook) http.ListenAndServe(":3000", nil) } ``` ```ruby filename="app.rb" require "sinatra" require "openssl" require "json" def verify(raw_body, header, secret) return false unless header parts = header.split(",").to_h { |part| part.split("=", 2) } timestamp, signature = parts["t"], parts["v1"] return false unless timestamp && signature && (Time.now.to_i - timestamp.to_i).abs <= 300 expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}") Rack::Utils.secure_compare(expected, signature) end post "/webhooks/bloomtext" do raw_body = request.body.read halt 400 unless verify(raw_body, request.env["HTTP_X_BLOOMTEXT_SIGNATURE"], ENV.fetch("BLOOMTEXT_WEBHOOK_SECRET")) event = JSON.parse(raw_body) # Deduplicate by event["id"], store the event, then process it in the background. status 200 end ``` > **Warning:** Most frameworks parse JSON before your handler runs, and re-serialized JSON won't match the signature. Make sure you verify the raw bytes, as each example above does. ## Responses and retries | You return | BloomText does | | --- | --- | | Any `2xx` within 5 seconds | Marks the delivery done. | | `4xx` | Treats the delivery as permanently rejected and stops. | | `5xx`, a timeout, or no response | Retries after 1 minute, 5 minutes, 30 minutes, 2 hours, and 24 hours, then stops. | `delivery_attempt` tells you which attempt you're receiving. Retries reuse the same event `id`. ## Best practices - **Deduplicate by `id`.** The same event can arrive more than once. Record processed IDs and skip repeats. - **Don't depend on order.** Events can arrive out of order. Use `occurred_at` and fetch current state from the API when order matters. - **Acknowledge first.** Store the event and return `200`, then process it in a queue. Slow handlers cause timeouts and retries. - **Rotate secrets safely.** When you rotate a signing secret, deliveries are signed with both the old and new secrets for 24 hours. Accept either during the overlap. ## Test locally Tunnel a local port with a tool like `ngrok` or `cloudflared`, register the tunnel URL on a test app user, and send a message in a test conversation. You'll receive a real `conversation.message.created` event. ```bash filename="Terminal" cloudflared tunnel --url http://localhost:3000 ``` --- # Build an AI agent > Build an AI agent that reads and replies in BloomText conversations, with safe scopes and human handoff. Source: https://www.bloomtext.com/developers/api/ai-agents/ An AI agent can take routine work off your staff: answering scheduling questions, confirming appointments, and routing everything else to a person. In BloomText an agent is just an [app user](https://www.bloomtext.com/developers/api/concepts/#app-users), so it follows the same rules as any integration. - **Visible:** every message it sends shows the app user as the sender. - **Scoped:** it reads and writes only conversations it's been added to, with only the scopes its key has. - **Revocable:** an admin can revoke its key or remove it from a conversation at any time. ## Two ways to connect - [MCP server](https://www.bloomtext.com/developers/api/mcp/) - [REST API + webhooks](#build-the-loop) ## Build the loop ```mermaid flowchart LR W[conversation.message.created] --> F[Fetch recent messages] F --> D{Can the agent answer?} D -->|Yes| R[Reply in the thread] D -->|No| H[Post to a staff group] R --> L[Log the decision] H --> L ``` ### Create a dedicated app user Name it so staff recognize it, like "Scheduling Assistant". Grant only what the loop needs: ```text conversations:read messages:read messages:write reactions:write ``` ### Receive new messages Subscribe to [`conversation.message.created`](https://www.bloomtext.com/developers/api/reference/webhook-events/#conversationmessagecreated) and [verify each signature](https://www.bloomtext.com/developers/api/webhooks/#verify-signatures). Ignore events where `sender_id` is your own app user, or the agent will answer itself. ### Load context Fetch the last few messages with [List messages](https://www.bloomtext.com/developers/api/reference/list-messages/) so the model sees the conversation, not a single line. ### Decide and reply Call your model with the context and a narrow instruction set. Reply in the thread with [Send a message](https://www.bloomtext.com/developers/api/reference/create-message/), setting `reply_to_message_id`. ### Hand off to people Anything outside the agent's job goes to staff: post in an internal group and stay quiet in the patient thread. ```js filename="agent.mjs" import Anthropic from '@anthropic-ai/sdk' const anthropic = new Anthropic() const API = 'https://api.bloomtext.com/v1' const headers = { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}` } const APP_USER_ID = process.env.BLOOMTEXT_APP_USER_ID const STAFF_GROUP_ID = process.env.BLOOMTEXT_STAFF_GROUP_ID // Call this from your verified webhook handler. export async function handleMessageCreated(event) { const { conversation_id, message_id, sender_id } = event.payload if (sender_id === APP_USER_ID) return // don't answer ourselves const history = await fetch(`${API}/conversations/${conversation_id}/messages?page[limit]=20`, { headers }) .then((r) => r.json()) const reply = await anthropic.messages.create({ model: 'claude-sonnet-5', max_tokens: 300, system: 'You are the scheduling assistant for a medical practice. Answer only questions about appointment times, ' + 'directions, and forms. For anything clinical or anything you are unsure about, respond with exactly HANDOFF.', messages: [{ role: 'user', content: history.data.map((m) => `${m.sender_id}: ${m.body ?? '[file]'}`).join('\n') }], }) const text = reply.content[0].text.trim() const post = (conversationId, body) => fetch(`${API}/conversations/${conversationId}/messages`, { method: 'POST', headers: { ...headers, 'Idempotency-Key': `${message_id}:${conversationId}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (text === 'HANDOFF') { await post(STAFF_GROUP_ID, { body: `Needs a staff reply: conversation ${conversation_id}` }) } else { await post(conversation_id, { body: text, reply_to_message_id: message_id }) } } ``` ```python filename="agent.py" import os import anthropic import requests client = anthropic.Anthropic() API = "https://api.bloomtext.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}"} APP_USER_ID = os.environ["BLOOMTEXT_APP_USER_ID"] STAFF_GROUP_ID = os.environ["BLOOMTEXT_STAFF_GROUP_ID"] SYSTEM = ( "You are the scheduling assistant for a medical practice. Answer only questions about appointment times, " "directions, and forms. For anything clinical or anything you are unsure about, respond with exactly HANDOFF." ) def post(conversation_id: str, body: dict, key: str) -> None: requests.post( f"{API}/conversations/{conversation_id}/messages", headers={**HEADERS, "Idempotency-Key": key}, json=body, ).raise_for_status() def handle_message_created(event: dict) -> None: """Call this from your verified webhook handler.""" payload = event["payload"] if payload["sender_id"] == APP_USER_ID: return # don't answer ourselves history = requests.get( f"{API}/conversations/{payload['conversation_id']}/messages", headers=HEADERS, params={"page[limit]": 20}, ).json()["data"] reply = client.messages.create( model="claude-sonnet-5", max_tokens=300, system=SYSTEM, messages=[{"role": "user", "content": "\n".join(f"{m['sender_id']}: {m['body'] or '[file]'}" for m in history)}], ) text = reply.content[0].text.strip() key = f"{payload['message_id']}:reply" if text == "HANDOFF": post(STAFF_GROUP_ID, {"body": f"Needs a staff reply: conversation {payload['conversation_id']}"}, key) else: post(payload["conversation_id"], {"body": text, "reply_to_message_id": payload["message_id"]}, key) ``` > **Note:** The idempotency key is derived from the incoming message ID, so a retried webhook can never make the agent reply twice. See [Idempotency](https://www.bloomtext.com/developers/api/idempotency/). ## Guardrails that matter - **Keep a narrow job.** A clear list of what the agent may answer beats a long list of what it may not. - **Make handoff the default.** When the model is unsure, it should route to staff, not guess. - **Never let it act clinically.** No diagnoses, dosing, or triage advice in automated replies. - **Use a model provider with a BAA.** The agent reads PHI, so its provider needs a BAA with your organization. - **Log every decision** with the incoming message ID, the reply, and whether it handed off. - **Start read-only.** Run the agent in "suggest" mode, posting drafts to a staff group, before you let it reply to patients. --- # Send appointment reminders > Send next-day appointment reminders into BloomText conversations from your EMR or scheduling system. Source: https://www.bloomtext.com/developers/api/guides/appointment-reminders/ This guide builds a nightly job that reads tomorrow's appointments from your scheduling system and posts a reminder into each patient's BloomText conversation. Replies land in the same thread your front desk already works from. **You'll need:** an app user with `conversations:read` and `messages:write`, and a way to read appointments from your EMR or scheduler. ### Map patients to conversations The BloomText API doesn't look up patients by name or phone. Your app keeps a mapping from your patient ID to the BloomText conversation ID, created when the patient's conversation is set up. ```sql filename="schema.sql" create table bloomtext_conversations ( patient_id text primary key, -- your EMR's patient ID conversation_id uuid not null -- BloomText conversation ID ); ``` ### Build the reminder text Keep reminders short and free of clinical detail. Times, locations, and a reply instruction are enough. ```text filename="Template" Reminder: you have an appointment tomorrow at {time} with {clinic}. Reply C to confirm or R to reschedule. ``` ### Send one message per appointment Derive the idempotency key from the appointment ID and date, so rerunning the job never double-sends. ```js filename="send-reminders.mjs" const API = 'https://api.bloomtext.com/v1' export async function sendReminders(appointments, lookupConversation) { for (const appointment of appointments) { const conversationId = await lookupConversation(appointment.patientId) if (!conversationId) continue // no BloomText conversation for this patient yet const response = await fetch(`${API}/conversations/${conversationId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, 'Idempotency-Key': `reminder:${appointment.id}:${appointment.date}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ body: `Reminder: you have an appointment tomorrow at ${appointment.time}. Reply C to confirm or R to reschedule.`, }), }) if (response.status === 429) { await new Promise((r) => setTimeout(r, Number(response.headers.get('Retry-After')) * 1000)) continue // pick it up on the next run; the idempotency key keeps it safe } const message = await response.json() console.log(appointment.id, message.id) await new Promise((r) => setTimeout(r, 1100)) // stay under 60 requests per minute } } ``` ```python filename="send_reminders.py" import os import time import requests API = "https://api.bloomtext.com/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['BLOOMTEXT_API_KEY']}" def send_reminders(appointments, lookup_conversation): for appointment in appointments: conversation_id = lookup_conversation(appointment["patient_id"]) if not conversation_id: continue # no BloomText conversation for this patient yet response = session.post( f"{API}/conversations/{conversation_id}/messages", headers={"Idempotency-Key": f"reminder:{appointment['id']}:{appointment['date']}"}, json={ "body": f"Reminder: you have an appointment tomorrow at {appointment['time']}. " "Reply C to confirm or R to reschedule." }, ) if response.status_code == 429: time.sleep(int(response.headers["Retry-After"])) continue # pick it up on the next run; the idempotency key keeps it safe response.raise_for_status() print(appointment["id"], response.json()["id"]) time.sleep(1.1) # stay under 60 requests per minute ``` ### Schedule it Run the job once a day, in the evening, from cron or your job scheduler. ```bash filename="crontab" 0 18 * * * node /srv/reminders/send-reminders.mjs >> /var/log/reminders.log 2>&1 ``` ## Next Turn "C" replies into confirmations in your EMR with [Handle patient replies](https://www.bloomtext.com/developers/api/guides/handle-replies/). --- # Handle patient replies > Use BloomText webhooks to turn patient replies into actions, like confirming an appointment in your EMR. Source: https://www.bloomtext.com/developers/api/guides/handle-replies/ When a patient answers a reminder with "C", you can confirm the appointment in your EMR automatically and let staff see it was handled. This guide wires a [webhook](https://www.bloomtext.com/developers/api/webhooks/) to that action. **You'll need:** an app user with `messages:read` and `reactions:write`, a webhook endpoint subscribed to `conversation.message.created`, and the reminder job from [Send appointment reminders](https://www.bloomtext.com/developers/api/guides/appointment-reminders/). ### Receive and verify the event Start from the [verified receiver](https://www.bloomtext.com/developers/api/webhooks/#verify-signatures). Acknowledge fast and queue the event. ### Fetch the message The event carries IDs only, so read the latest messages to get the text: ```bash filename="Request" curl "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages?page[limit]=5" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ### Match the reply and act ```js filename="handle-reply.mjs" const API = 'https://api.bloomtext.com/v1' const headers = { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}` } export async function handleReply(event, { findPendingReminder, confirmInEmr, alreadyProcessed }) { if (await alreadyProcessed(event.id)) return // webhooks can repeat const { conversation_id, message_id, sender_id } = event.payload if (sender_id === process.env.BLOOMTEXT_APP_USER_ID) return const { data } = await fetch(`${API}/conversations/${conversation_id}/messages?page[limit]=5`, { headers }) .then((r) => r.json()) const message = data.find((m) => m.id === message_id) const answer = message?.body?.trim().toUpperCase() const reminder = await findPendingReminder(conversation_id) if (!reminder || answer !== 'C') return // anything else is for staff await confirmInEmr(reminder.appointmentId) await fetch(`${API}/messages/${message_id}/reactions`, { method: 'POST', headers: { ...headers, 'Idempotency-Key': `confirm:${message_id}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ emoji: '👍' }), }) } ``` ```python filename="handle_reply.py" import os import requests API = "https://api.bloomtext.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}"} def handle_reply(event, find_pending_reminder, confirm_in_emr, already_processed): if already_processed(event["id"]): return # webhooks can repeat payload = event["payload"] if payload["sender_id"] == os.environ["BLOOMTEXT_APP_USER_ID"]: return messages = requests.get( f"{API}/conversations/{payload['conversation_id']}/messages", headers=HEADERS, params={"page[limit]": 5}, ).json()["data"] message = next((m for m in messages if m["id"] == payload["message_id"]), None) answer = (message or {}).get("body", "").strip().upper() reminder = find_pending_reminder(payload["conversation_id"]) if not reminder or answer != "C": return # anything else is for staff confirm_in_emr(reminder["appointment_id"]) requests.post( f"{API}/messages/{payload['message_id']}/reactions", headers={**HEADERS, "Idempotency-Key": f"confirm:{payload['message_id']}"}, json={"emoji": "👍"}, ).raise_for_status() ``` ### Let staff see what happened The 👍 reaction shows staff the reply was handled automatically. Anything you don't recognize stays unreacted, so it stands out for a person to answer. > **Note:** Keep the matching strict. "C" confirms; "can I come at 4 instead?" doesn't. When in doubt, leave it for staff. --- # Export conversation history > Export a BloomText conversation's messages on a schedule and file them with the patient's record. Source: https://www.bloomtext.com/developers/api/guides/export-history/ Exports package a conversation's messages into a downloadable file. Use them to archive texting history with the patient's chart or to meet a records request. **You'll need:** an app user with `exports:write` and `exports:read` that participates in the conversations you export. ### Start the export ```bash filename="Request" curl -X POST https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/exports \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \ -H "Idempotency-Key: export:e5c3b7b8:2026-09" \ -H "Content-Type: application/json" \ -d '{"projection": "messages", "from": "2026-09-01T00:00:00Z", "to": "2026-09-30T23:59:59Z"}' ``` The response is `202 Accepted` with an [Export object](https://www.bloomtext.com/developers/api/reference/export-object/) in `queued` status. ### Wait for it to finish Either listen for the [`export.ready`](https://www.bloomtext.com/developers/api/reference/webhook-events/#exportready) webhook, or poll [Retrieve an export](https://www.bloomtext.com/developers/api/reference/get-chat-export/) every 15 seconds until `status` is `ready`. ```js filename="wait-for-export.mjs" async function waitForExport(exportId) { for (;;) { const exp = await fetch(`https://api.bloomtext.com/v1/exports/${exportId}`, { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}` }, }).then((r) => r.json()) if (exp.status === 'ready') return exp.download_url if (exp.status === 'failed') throw new Error(`Export ${exportId} failed`) await new Promise((r) => setTimeout(r, 15_000)) } } ``` ```python filename="wait_for_export.py" import os import time import requests def wait_for_export(export_id: str) -> str: while True: exp = requests.get( f"https://api.bloomtext.com/v1/exports/{export_id}", headers={"Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}"}, ).json() if exp["status"] == "ready": return exp["download_url"] if exp["status"] == "failed": raise RuntimeError(f"Export {export_id} failed") time.sleep(15) ``` ### Download and file it The `download_url` is short-lived. Download it right away and upload the file to your EMR's document store with the patient's record. > **Warning:** Exports contain PHI. Store downloaded files in the same protected systems as the rest of your patient records, and delete temporary copies. --- # MCP server > Connect Claude, ChatGPT, Cursor, VS Code, Codex, or any MCP client to BloomText with the BloomText MCP server. Source: https://www.bloomtext.com/developers/api/mcp/ The BloomText MCP server lets any [Model Context Protocol](https://modelcontextprotocol.io) client read and send BloomText messages. It's a remote server, so there's nothing to install: point your client at the URL and authenticate with an API key. The agent gets exactly the scopes and conversations that key has. ```text filename="Server URL" https://mcp.bloomtext.com/mcp ``` ## Connect your client Replace `YOUR_BLOOMTEXT_API_KEY` with a key from an app user created for the agent. Don't have one? [Request API access](https://calendly.com/tyler-bloom/bloomtext-homepage-demo-request?utm_campaign=api-access). Run this once in your terminal: ```bash filename="Terminal" claude mcp add --transport http bloomtext https://mcp.bloomtext.com/mcp \ --header "Authorization: Bearer YOUR_BLOOMTEXT_API_KEY" ``` Check it's connected with `claude mcp list`, or type `/mcp` inside Claude Code. In Claude on the web or desktop: 1. Open **Settings → Connectors** and choose **Add custom connector**. 2. Name it `BloomText` and paste `https://mcp.bloomtext.com/mcp` as the URL. 3. Under advanced settings, add the header `Authorization: Bearer YOUR_BLOOMTEXT_API_KEY`. 4. Enable the connector in a chat from the tools menu. > **Warning:** Claude.ai connectors run on Anthropic's infrastructure. Confirm your organization has a BAA with Anthropic before connecting a key that can read patient conversations. In ChatGPT with developer mode enabled for your workspace: 1. Open **Settings → Connectors → Create**. 2. Name it `BloomText`, set the MCP server URL to `https://mcp.bloomtext.com/mcp`, and choose header authentication. 3. Paste `Bearer YOUR_BLOOMTEXT_API_KEY` as the `Authorization` value. 4. Turn the connector on in a conversation. > **Warning:** Confirm your OpenAI workspace is covered by a BAA before connecting a key that can read patient conversations. Use the **Add to Cursor** button above, or add this to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project): ```json filename=".cursor/mcp.json" { "mcpServers": { "bloomtext": { "url": "https://mcp.bloomtext.com/mcp", "headers": { "Authorization": "Bearer YOUR_BLOOMTEXT_API_KEY" } } } } ``` Use the **Add to VS Code** button above, or add this to `.vscode/mcp.json`. VS Code prompts for the key the first time and stores it securely: ```json filename=".vscode/mcp.json" { "inputs": [ { "type": "promptString", "id": "bloomtext-key", "description": "BloomText API key", "password": true } ], "servers": { "bloomtext": { "type": "http", "url": "https://mcp.bloomtext.com/mcp", "headers": { "Authorization": "Bearer ${input:bloomtext-key}" } } } } ``` Add the server to `~/.codex/config.toml`, reading the key from your environment: ```toml filename="~/.codex/config.toml" [mcp_servers.bloomtext] url = "https://mcp.bloomtext.com/mcp" bearer_token_env_var = "BLOOMTEXT_API_KEY" ``` Then export `BLOOMTEXT_API_KEY` in the shell you start Codex from. Any client that supports remote MCP servers over Streamable HTTP works. Use this config shape: ```json filename="mcp.json" { "mcpServers": { "bloomtext": { "type": "http", "url": "https://mcp.bloomtext.com/mcp", "headers": { "Authorization": "Bearer YOUR_BLOOMTEXT_API_KEY" } } } } ``` For clients that only speak stdio, bridge with `mcp-remote`: ```bash filename="Terminal" npx mcp-remote https://mcp.bloomtext.com/mcp --header "Authorization: Bearer ${BLOOMTEXT_API_KEY}" ``` ## Let your agent set it up Paste this into your coding agent and it will wire up the server for you: **Set up the BloomText MCP server:** ```text Add the BloomText MCP server to this project. It's a remote Streamable HTTP server at https://mcp.bloomtext.com/mcp that authenticates with the header 'Authorization: Bearer '. Read the key from the BLOOMTEXT_API_KEY environment variable and never hard-code it. Setup docs: https://www.bloomtext.com/developers/api/mcp.md ``` ## Tools Each tool maps to one API endpoint and requires the same [scope](https://www.bloomtext.com/developers/api/authentication/#scopes). The agent only sees tools its key's scopes allow. | Tool | Does | Scope | | --- | --- | --- | | `get_organization` | Returns the organization the key belongs to. | `organization:read` | | `list_users` | Lists organization members. | `users:read` | | `list_conversations` | Lists conversations the app user is in. | `conversations:read` | | `get_conversation` | Returns one conversation. | `conversations:read` | | `list_messages` | Reads messages in a conversation, newest page first. | `messages:read` | | `list_replies` | Reads a message's thread. | `messages:read` | | `send_message` | Sends a message or threaded reply. | `messages:write` | | `add_reaction` | Reacts to a message. | `reactions:write` | | `remove_reaction` | Removes the app user's reaction. | `reactions:write` | | `list_participants` | Lists who is in a conversation. | `participants:read` | | `list_broadcasts` | Lists broadcasts and their status. | `broadcasts:read` | | `create_export` | Starts a conversation export. | `exports:write` | | `get_export` | Checks an export and returns its download URL. | `exports:read` | `send_message` and `create_export` generate idempotency keys for you, so an agent retrying a tool call never sends twice. ## Use cases Each prompt works as-is once the server is connected. Swap in your own conversation names.
Morning catch-up Summarize overnight messages so the front desk starts the day knowing what needs a reply. **Morning catch-up:** ```text Read the messages sent since 6pm yesterday in every conversation you can see. Group them into: needs a staff reply, already handled, and FYI. Keep patient details out of the summary; use conversation names only. ```
Schedule change notice Post a change to a staff group and acknowledge the request that prompted it. **Schedule change:** ```text Reply in the Scheduling group thread: 'Dr. Lee's 2pm is moved to 3pm today.' Then react 👍 to the original request so staff know it's handled. ```
Handoff triage Find patient messages that need a person and route them to the right staff group. **Handoff triage:** ```text Find messages from the last 2 hours that ask a clinical question or mention a problem. For each, post 'Needs a staff reply' with the conversation name in the Front Desk group. Don't reply to the patient yourself. ```
Unanswered messages Catch conversations where the last word is still the patient's. **Unanswered messages:** ```text List conversations where the most recent message is from someone outside our staff and is older than 1 hour. Show the conversation name and how long it's been waiting. ```
Records export Package a conversation's history for the chart or a records request. **Records export:** ```text Export last month's messages from the Intake conversation and give me the download link when it's ready. ```
Care team check Make sure the right people are in a conversation. **Care team check:** ```text List the participants in the Johnson family care team conversation and tell me if anyone from the Nursing group is missing. ```
## Agent skill The [BloomText API skill](https://www.bloomtext.com/developers/api/skills/bloomtext-api/SKILL.md) teaches an agent the API's rules: authentication, idempotency, pagination, error codes, and what never to send. Agents that support [Agent Skills](https://agentskills.io) load it automatically when a task involves BloomText. ```bash filename="Terminal" mkdir -p .claude/skills/bloomtext-api curl -o .claude/skills/bloomtext-api/SKILL.md https://www.bloomtext.com/developers/api/skills/bloomtext-api/SKILL.md ``` ```bash filename="Terminal" mkdir -p ~/.codex/skills/bloomtext-api curl -o ~/.codex/skills/bloomtext-api/SKILL.md https://www.bloomtext.com/developers/api/skills/bloomtext-api/SKILL.md ``` Download [SKILL.md](https://www.bloomtext.com/developers/api/skills/bloomtext-api/SKILL.md) into your agent's skills folder, or paste it into its system instructions. ## Safety > **Warning:** An agent with `messages:write` can post as your app user in every conversation it's in. Start with read-only scopes, add the app user only to the conversations the agent needs, and review what it sends before widening access. - Messages the agent sends appear under its app user's name, so staff always know what came from it. - An organization admin can revoke the key or remove the app user at any time. - Keep patient data inside tools covered by a BAA. See [Security and BAA](https://www.bloomtext.com/developers/api/security-and-baa/). For building your own agent loop on the REST API instead, see [Build an AI agent](https://www.bloomtext.com/developers/api/ai-agents/). --- # CLI > Use the bloomtext command-line tool to read and send BloomText messages from scripts, cron jobs, and AI coding agents. Source: https://www.bloomtext.com/developers/api/cli/ The `bloomtext` command-line tool wraps the API for scripts, cron jobs, quick checks from a terminal, and AI coding agents. It covers every endpoint in the [API reference](https://www.bloomtext.com/developers/api/reference/), handles pagination and idempotency for you, and prints JSON on request. ## Setup Installation instructions come with your API key. [Request API access](https://calendly.com/tyler-bloom/bloomtext-homepage-demo-request?utm_campaign=api-access) if you don't have one yet. The CLI reads your key from the environment: ```bash filename="Terminal" export BLOOMTEXT_API_KEY="bt_live_4f7c2a9e1b..." bloomtext whoami ``` ```text filename="Output" Riverside Family Clinic (6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c) App user: Intake Bot Scopes: conversations:read messages:read messages:write ``` ## Commands ```bash bloomtext conversations list bloomtext conversations get e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0 ``` ```bash # Latest 20 messages bloomtext messages list --conversation e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0 --limit 20 # Send a message bloomtext messages send \ --conversation e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0 \ --body "Your appointment is confirmed for Tuesday at 10:00." # Reply in a thread bloomtext messages send --conversation e5c3b7b8-... --reply-to 4cbfdb50-... --body "Done ✅" # Read a thread bloomtext messages replies 4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1 ``` ```bash bloomtext reactions add 4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1 --emoji 👍 bloomtext reactions remove 4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1 --emoji 👍 ``` ```bash bloomtext participants list --conversation e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0 bloomtext participants add --conversation e5c3b7b8-... --user 8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55 bloomtext participants remove --conversation e5c3b7b8-... --user 8b9c2d2f-... ``` ```bash # Start an export and wait for the download URL bloomtext exports create --conversation e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0 \ --projection messages --from 2026-09-01 --to 2026-09-30 --wait ``` Run `bloomtext --help` for every flag. ## Global flags | Flag | Does | | --- | --- | | `--json` | Print raw API JSON instead of tables. Stable, and safe to parse. | | `--all` | Follow `next_cursor` and return every page. | | `--limit ` | Page size, up to 100. | | `--idempotency-key ` | Use your own key instead of a generated one. | | `--quiet` | Print only IDs, one per line. | ## Exit codes | Code | Meaning | | --- | --- | | `0` | Success. | | `1` | The API returned an error. The [problem details](https://www.bloomtext.com/developers/api/errors/) are printed to stderr. | | `2` | Invalid flags or arguments. | | `3` | Authentication failed (`401`) or a scope is missing (`403`). | | `4` | Rate limited after automatic retries. | The CLI retries `429` and `503` responses with backoff, reusing the same idempotency key, before giving up. ## Scripting Combine `--json` with `jq`: ```bash filename="Terminal" # IDs of every conversation the app user is in bloomtext conversations list --all --json | jq -r '.data[].id' # Send one message per line of a file (conversation_idtext) while IFS=$'\t' read -r conversation text; do bloomtext messages send --conversation "$conversation" --body "$text" --quiet done < reminders.tsv ``` ## Use with AI coding agents Agents like Claude Code, Codex, and Cursor work well with the CLI because every command has a `--json` mode, clear exit codes, and `--help` text written for machines as much as people. Add this to your repository's `AGENTS.md` or `CLAUDE.md` so the agent knows how to use it: ```md filename="AGENTS.md" ## BloomText - Use the `bloomtext` CLI to read and send BloomText messages. The key is in `BLOOMTEXT_API_KEY`; never print or commit it. - Always pass `--json` and parse the output. Check the exit code: 0 ok, 1 API error (details on stderr), 3 auth or scope problem. - Only post to conversations the task names. Never send patient information to any tool outside BloomText. - Docs for agents: https://www.bloomtext.com/developers/api/llms.txt ``` For agents that support skills, install the [BloomText API skill](https://www.bloomtext.com/developers/api/mcp/#agent-skill) as well. It covers the API's rules in more depth. Or hand an assistant the whole reference in one go: **Learn the BloomText API:** ```text Read https://www.bloomtext.com/developers/api/llms-full.txt and help me use the bloomtext CLI and the BloomText API. ``` For agents that call tools directly rather than a shell, the [MCP server](https://www.bloomtext.com/developers/api/mcp/) is usually a better fit. --- # Docs for AI agents > Machine-readable BloomText API docs for LLMs and coding agents, including llms.txt, per-page Markdown, and the OpenAPI spec. Source: https://www.bloomtext.com/developers/api/llms/ These docs are built to be read by agents as easily as by people. Point your coding agent or LLM at any of these. | Resource | URL | Use it for | | --- | --- | --- | | `llms.txt` | [`/developers/api/llms.txt`](https://www.bloomtext.com/developers/api/llms.txt) | An index of every page with a one-line summary. | | `llms-full.txt` | [`/developers/api/llms-full.txt`](https://www.bloomtext.com/developers/api/llms-full.txt) | Every page's full text in one file, ready to drop into context. | | Page Markdown | Add `.md` to any page URL, like [`/developers/api/quickstart.md`](https://www.bloomtext.com/developers/api/quickstart.md) | One page as clean Markdown. | | OpenAPI 3.1 | [`/developers/api/openapi/bloomtext-api.yaml`](https://www.bloomtext.com/developers/api/openapi/bloomtext-api.yaml) | Generating clients, tools, and typed SDKs. | | Agent skill | [`/developers/api/skills/bloomtext-api/SKILL.md`](https://www.bloomtext.com/developers/api/skills/bloomtext-api/SKILL.md) | Teaching Claude Code, Codex, and other agents the API's rules. See [Agent skill](https://www.bloomtext.com/developers/api/mcp/#agent-skill). | | MCP server | `https://mcp.bloomtext.com/mcp` | Letting an agent act in BloomText. See [MCP server](https://www.bloomtext.com/developers/api/mcp/). | ## Copy any page Every page has a **Copy page** menu at the top. It copies the page as Markdown or opens it in ChatGPT or Claude so you can ask questions about it. ## Give your agent the whole API **Build a BloomText integration:** ```text Use the BloomText API docs at https://www.bloomtext.com/developers/api/llms-full.txt and the OpenAPI spec at https://www.bloomtext.com/developers/api/openapi/bloomtext-api.yaml. Authenticate with the BLOOMTEXT_API_KEY environment variable as a Bearer token, and send an Idempotency-Key on every POST. Then help me build an integration. ``` --- # Pagination > Page through BloomText API list endpoints with opaque cursors, page[limit], and page[after]. Source: https://www.bloomtext.com/developers/api/pagination/ Every list endpoint returns results a page at a time, in a stable order, with an opaque cursor for the next page. ```json filename="Response" { "data": [ { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "...": "..." } ], "pagination": { "next_cursor": "eyJpZCI6IjRjYmZkYjUwIn0", "has_more": true } } ``` ## Parameters - `page[limit]` (integer): How many records to return. Defaults to `50`, maximum `100`. - `page[after]` (string): The `next_cursor` from the previous page. Omit it for the first page. ## Response fields - `data` (array, required): The records on this page, in stable order. - `pagination.next_cursor` (string or null, required): Pass as `page[after]` to get the next page. `null` on the last page. - `pagination.has_more` (boolean, required): Whether another page exists. ## Ordering Results are ordered by creation time, oldest first, with the ID as a tiebreaker. The order never changes between requests, so retrying a page is always safe and never skips or repeats records. ## Fetch every page ```bash filename="Request" curl "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages?page[limit]=100&page[after]=eyJpZCI6IjRjYmZkYjUwIn0" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="paginate.mjs" async function* listAll(path) { let cursor = null do { const url = new URL(`https://api.bloomtext.com/v1${path}`) url.searchParams.set('page[limit]', '100') if (cursor) url.searchParams.set('page[after]', cursor) const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}` }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const { data, pagination } = await response.json() yield* data cursor = pagination.has_more ? pagination.next_cursor : null } while (cursor) } for await (const message of listAll('/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages')) { console.log(message.id) } ``` ```python filename="paginate.py" import os import requests session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['BLOOMTEXT_API_KEY']}" def list_all(path): params = {"page[limit]": 100} while True: response = session.get(f"https://api.bloomtext.com/v1{path}", params=params) response.raise_for_status() page = response.json() yield from page["data"] if not page["pagination"]["has_more"]: return params["page[after]"] = page["pagination"]["next_cursor"] for message in list_all("/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages"): print(message["id"]) ``` ```go filename="paginate.go" type page struct { Data []json.RawMessage `json:"data"` Pagination struct { NextCursor *string `json:"next_cursor"` HasMore bool `json:"has_more"` } `json:"pagination"` } func listAll(path string) ([]json.RawMessage, error) { var all []json.RawMessage params := url.Values{"page[limit]": {"100"}} for { req, _ := http.NewRequest("GET", "https://api.bloomtext.com/v1"+path+"?"+params.Encode(), nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { return nil, err } var p page err = json.NewDecoder(res.Body).Decode(&p) res.Body.Close() if err != nil { return nil, err } all = append(all, p.Data...) if !p.Pagination.HasMore { return all, nil } params.Set("page[after]", *p.Pagination.NextCursor) } } ``` ```ruby filename="paginate.rb" require "net/http" require "json" def list_all(path) Enumerator.new do |yielder| params = { "page[limit]" => 100 } loop do uri = URI("https://api.bloomtext.com/v1#{path}") uri.query = URI.encode_www_form(params) request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } page = JSON.parse(response.body) page["data"].each { |record| yielder << record } break unless page["pagination"]["has_more"] params["page[after]"] = page["pagination"]["next_cursor"] end end end list_all("/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages").each { |message| puts message["id"] } ``` ## Tips - **Treat cursors as opaque.** Don't parse, build, or store them long-term. A cursor is valid for 24 hours. - **Use the biggest page you need.** `page[limit]=100` means fewer requests against your [rate limit](https://www.bloomtext.com/developers/api/rate-limits/). - **Sync incrementally.** To pick up new messages, keep the last `next_cursor` you saw and resume from it, or use [webhooks](https://www.bloomtext.com/developers/api/webhooks/) instead of polling. --- # Idempotency > Safely retry BloomText API writes with the Idempotency-Key header. Source: https://www.bloomtext.com/developers/api/idempotency/ Networks fail. A request can time out after BloomText has already sent your message. Idempotency keys let you retry any write without sending it twice. Every `POST` request requires an `Idempotency-Key` header. Generate a new UUID for each logical write, and reuse the same key when you retry that write. ```http filename="Header" Idempotency-Key: 9f1c2b3a-7d4e-4f5a-8b6c-1d2e3f4a5b6c ``` ## How it works | You send | BloomText returns | | --- | --- | | A new key | Processes the request normally and remembers the result for 24 hours. | | The same key, method, path, and body within 24 hours | The original response, without running the write again. | | The same key with a different method, path, or body | `409 Conflict` with `code: idempotency_key_reused`. | | The same key while the first request is still running | `409 Conflict` with `code: idempotency_request_in_progress`. Retry shortly. | `DELETE` requests are identified by their path, so they don't take a key. Deleting something twice returns `404` the second time, which you can treat as success. ## Retry safely ```js filename="send-with-retry.mjs" async function sendMessage(conversationId, body) { const idempotencyKey = crypto.randomUUID() // one key per logical send for (let attempt = 1; attempt <= 4; attempt++) { try { const response = await fetch(`https://api.bloomtext.com/v1/conversations/${conversationId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json', }, body: JSON.stringify({ body }), }) if (response.status < 500 && response.status !== 429) return await response.json() } catch { // Network error: safe to retry with the same key. } await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 250)) } throw new Error('Message not sent after 4 attempts') } ``` ```python filename="send_with_retry.py" import os import time import uuid import requests def send_message(conversation_id: str, body: str) -> dict: idempotency_key = str(uuid.uuid4()) # one key per logical send for attempt in range(1, 5): try: response = requests.post( f"https://api.bloomtext.com/v1/conversations/{conversation_id}/messages", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", "Idempotency-Key": idempotency_key, }, json={"body": body}, timeout=10, ) if response.status_code < 500 and response.status_code != 429: return response.json() except requests.RequestException: pass # Network error: safe to retry with the same key. time.sleep(2**attempt * 0.25) raise RuntimeError("Message not sent after 4 attempts") ``` ```go filename="retry.go" func sendMessage(conversationID, text string) (*http.Response, error) { idempotencyKey := uuid.NewString() // one key per logical send payload, _ := json.Marshal(map[string]string{"body": text}) url := "https://api.bloomtext.com/v1/conversations/" + conversationID + "/messages" for attempt := 1; attempt <= 4; attempt++ { req, _ := http.NewRequest("POST", url, bytes.NewReader(payload)) req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) req.Header.Set("Idempotency-Key", idempotencyKey) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err == nil && res.StatusCode < 500 && res.StatusCode != 429 { return res, nil } if err == nil { res.Body.Close() } time.Sleep(time.Duration(1< BloomText API error responses, HTTP status codes, and machine-readable error codes. Source: https://www.bloomtext.com/developers/api/errors/ BloomText uses standard HTTP status codes and returns [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details with `Content-Type: application/problem+json`. Branch on the `code` field, not the human-readable text. ```json filename="Response · 403 Forbidden" { "type": "https://www.bloomtext.com/developers/api/errors/#conversation_membership_required", "title": "Forbidden", "status": 403, "code": "conversation_membership_required", "detail": "The app user is not a participant in this conversation.", "request_id": "0b6c2d1e-5f4a-4c3b-9a8d-7e6f5a4b3c2d", "field_errors": [] } ``` ## The problem object - `type` (URL, required): Link to the documentation for this error. - `title` (string, required): Short, human-readable summary of the status. - `status` (integer, required): The HTTP status code, repeated for convenience. - `code` (string, required): Machine-readable error code. See [Error codes](#error-codes). - `detail` (string): Explanation of this specific occurrence. - `request_id` (UUID, required): Unique ID for the request. Include it when you contact support. - `field_errors` (array of FieldError, required): Per-field problems for `422` responses. Empty otherwise. Child attributes: - `path` (string, required): JSON pointer to the field, like `/body`, or the query parameter or header name. - `code` (string, required): Machine-readable validation code, like `too_long` or `required`. - `message` (string, required): Human-readable explanation. ## HTTP status codes | Status | Meaning | Retry? | | --- | --- | --- | | `200`, `201`, `202`, `204` | Success. | No | | `400` | The request is malformed. | No, fix the request. | | `401` | The API key is missing, revoked, or invalid. | No, fix the key. | | `403` | The key lacks the scope, or the app user isn't a participant. | No | | `404` | The resource doesn't exist or isn't visible to this key. | No | | `409` | An idempotency or state conflict. | Sometimes. See the code. | | `422` | A field failed validation. | No, fix the fields. | | `429` | Rate limited. | Yes, after `Retry-After` seconds. | | `500`, `503` | A temporary problem on our side. | Yes, with backoff and the same `Idempotency-Key`. | ## Error codes | Code | Status | What to do | | --- | --- | --- | | `invalid_request` | 400 | Check the JSON syntax and query string. | | `unauthorized` | 401 | Check the `Authorization` header and that the key hasn't been revoked. | | `insufficient_scope` | 403 | Ask an admin to add the scope listed on the endpoint's reference page. | | `conversation_membership_required` | 403 | Add your app user to the conversation. | | `not_found` | 404 | Check the ID. Resources outside your app user's conversations also return this. | | `idempotency_key_reused` | 409 | Use a new `Idempotency-Key` for a different request. | | `idempotency_request_in_progress` | 409 | The original request is still running. Retry in a moment. | | `participant_not_in_organization` | 422 | Only members of your organization can be added to a conversation. | | `validation_failed` | 422 | Read `field_errors` for each field to fix. | | `rate_limited` | 429 | Wait `Retry-After` seconds, then retry. | | `internal_error` | 500 | Retry with backoff. Contact support with the `request_id` if it persists. | | `service_unavailable` | 503 | Retry with backoff. | ## Handle errors ```js const response = await fetch(url, options) if (!response.ok) { const problem = await response.json() switch (problem.code) { case 'conversation_membership_required': // Ask an admin to add the app user to this conversation. break case 'rate_limited': await new Promise((r) => setTimeout(r, Number(response.headers.get('Retry-After')) * 1000)) break default: throw new Error(`${problem.status} ${problem.code}: ${problem.detail} (request ${problem.request_id})`) } } ``` ```python response = session.post(url, json=payload, headers=headers) if not response.ok: problem = response.json() if problem["code"] == "conversation_membership_required": ... # Ask an admin to add the app user to this conversation. elif problem["code"] == "rate_limited": time.sleep(int(response.headers["Retry-After"])) else: raise RuntimeError(f"{problem['status']} {problem['code']}: {problem.get('detail')} (request {problem['request_id']})") ``` ```go type Problem struct { Status int `json:"status"` Code string `json:"code"` Detail string `json:"detail"` RequestID string `json:"request_id"` } if res.StatusCode >= 400 { var p Problem json.NewDecoder(res.Body).Decode(&p) switch p.Code { case "rate_limited": wait, _ := strconv.Atoi(res.Header.Get("Retry-After")) time.Sleep(time.Duration(wait) * time.Second) default: return fmt.Errorf("%d %s: %s (request %s)", p.Status, p.Code, p.Detail, p.RequestID) } } ``` ```ruby unless response.is_a?(Net::HTTPSuccess) problem = JSON.parse(response.body) case problem["code"] when "rate_limited" sleep response["Retry-After"].to_i else raise "#{problem["status"]} #{problem["code"]}: #{problem["detail"]} (request #{problem["request_id"]})" end end ``` > **Note:** Every response, success or error, includes an `X-Request-Id` header with the same value as `request_id`. Log it with your own request logs. --- # Rate limits > BloomText API rate limits, rate limit headers, and how to stay under them. Source: https://www.bloomtext.com/developers/api/rate-limits/ Each organization can make **60 requests per minute**, with bursts of up to **10 requests per second**. Limits are shared by every key in the organization. ## Rate limit headers Every response tells you where you stand: | Header | Meaning | | --- | --- | | `RateLimit-Limit` | Requests allowed per minute. | | `RateLimit-Remaining` | Requests left in the current window. | | `RateLimit-Reset` | Seconds until the window resets. | | `Retry-After` | On `429` responses only: whole seconds to wait before retrying. | ```http filename="Response · 429 Too Many Requests" HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 12 RateLimit-Limit: 60 RateLimit-Remaining: 0 RateLimit-Reset: 12 ``` ## Staying under the limit - **Use webhooks instead of polling.** A single [webhook](https://www.bloomtext.com/developers/api/webhooks/) replaces dozens of list requests. If you must poll, wait at least 15 seconds between polls of the same conversation. - **Use conditional requests.** Send `If-None-Match` with the last `ETag` you received. Unchanged resources return `304 Not Modified`. - **Page with `page[limit]=100`.** Fewer, larger pages. See [Pagination](https://www.bloomtext.com/developers/api/pagination/). - **Back off exponentially** after a `429` or `503`, starting from `Retry-After`. - **Spread batch jobs out.** Sending 500 reminders at once takes about 9 minutes at 60 per minute. Queue them and send at a steady rate. > **Note:** Need a higher limit for a large organization? Mention it when you [request API access](https://calendly.com/tyler-bloom/bloomtext-homepage-demo-request?utm_campaign=api-access). --- # Security and BAA > How the BloomText API protects patient data, and the BAA required for API access. Source: https://www.bloomtext.com/developers/api/security-and-baa/ BloomText is HIPAA-compliant, and API access requires a signed Business Associate Agreement. [Request API access](https://calendly.com/tyler-bloom/bloomtext-homepage-demo-request?utm_campaign=api-access) and we'll send the BAA with your onboarding. ## How the API protects data | Protection | What it means | | --- | --- | | **Least privilege** | Keys carry only the scopes an admin grants, and app users see only the conversations they're in. | | **Minimal responses** | Users and organizations return only IDs, names, and roles. Passwords, private contact details, and internal fields are never returned. | | **Signed webhooks** | Every delivery is signed with HMAC-SHA256 and protected against replay. Payloads carry IDs, not message text. | | **Short-lived downloads** | Export URLs expire quickly and work only for the organization that created the export. | | **Attributed writes** | Every write is recorded against a named app user with a stable ID, so staff always see what came from an integration. | | **Instant revocation** | Admins can revoke a key or remove an app user from a conversation at any time. | All traffic uses TLS 1.2 or later. Plain HTTP requests are refused. ## Your responsibilities Messages sent through the API are protected health information when they're about patients. Your integration becomes part of your organization's HIPAA program: - **Store API data like any other PHI.** Encrypt it at rest, restrict who can read it, and log access. - **Keep keys in a secrets manager.** Rotate them when people leave, and never paste them into chat tools or AI prompts. - **Send the minimum.** Appointment times and "your forms are ready" are fine. Keep diagnoses and clinical detail out of automated messages unless your compliance team approves. - **Mind Part 2 data.** If you work with substance use disorder records under 42 CFR Part 2, have your compliance lead approve message templates. - **Check your AI vendors.** If an AI agent reads BloomText data, the model provider needs a BAA with your organization too. > **Warning:** Don't include PHI in support requests or the access request form. Share a `request_id` instead. --- # Changelog > Dated changes to the BloomText API, MCP server, CLI, and these docs, including new endpoints, fields, and behavior changes. Source: https://www.bloomtext.com/developers/api/changelog/ Changes to the BloomText API, MCP server, and CLI. The API is versioned in the URL (`/v1`). Additive changes, like new endpoints, fields, or event types, ship within `v1`, so build clients that ignore fields they don't recognize. Breaking changes would ship as a new version, announced here first. ## September 22, 2026 ### BloomText API v1 The first public version of the BloomText API. - **REST API** at `https://api.bloomtext.com/v1`, with organization-scoped [API keys](https://www.bloomtext.com/developers/api/authentication/) and [scopes](https://www.bloomtext.com/developers/api/authentication/#scopes). - **Resources:** [organization](https://www.bloomtext.com/developers/api/reference/organization-object/), [users](https://www.bloomtext.com/developers/api/reference/user-object/), [conversations](https://www.bloomtext.com/developers/api/reference/conversation-object/), [messages and replies](https://www.bloomtext.com/developers/api/reference/message-object/), [reactions](https://www.bloomtext.com/developers/api/reference/reaction-object/), [participants](https://www.bloomtext.com/developers/api/reference/participant-object/), [broadcasts](https://www.bloomtext.com/developers/api/reference/broadcast-object/), and [exports](https://www.bloomtext.com/developers/api/reference/export-object/). - **[Webhooks](https://www.bloomtext.com/developers/api/webhooks/)** for messages, reactions, participants, and exports, signed with HMAC-SHA256. - **[Idempotency keys](https://www.bloomtext.com/developers/api/idempotency/)** on every write, [cursor pagination](https://www.bloomtext.com/developers/api/pagination/), and [problem details errors](https://www.bloomtext.com/developers/api/errors/). - **[MCP server](https://www.bloomtext.com/developers/api/mcp/)** at `https://mcp.bloomtext.com/mcp`, the [`bloomtext` CLI](https://www.bloomtext.com/developers/api/cli/), and an [agent skill](https://www.bloomtext.com/developers/api/mcp/#agent-skill). - **[OpenAPI 3.1 spec](https://www.bloomtext.com/developers/api/openapi/bloomtext-api.yaml)** and [docs for AI agents](https://www.bloomtext.com/developers/api/llms/). --- # API reference > Every BloomText API endpoint, object, and webhook event, with parameters, example requests, and responses. Source: https://www.bloomtext.com/developers/api/reference/ The BloomText API is a REST API with JSON request and response bodies, standard HTTP status codes, and bearer-token authentication. Everything on these pages is generated from the [OpenAPI 3.1 spec](https://www.bloomtext.com/developers/api/openapi/bloomtext-api.yaml), which you can also feed to any client generator. ```text filename="Base URL" https://api.bloomtext.com/v1 ``` > **Note:** New to the API? Start with the [Quickstart](https://www.bloomtext.com/developers/api/quickstart/), then read [Authentication](https://www.bloomtext.com/developers/api/authentication/) for keys and scopes. ## Resources - [Organization](https://www.bloomtext.com/developers/api/reference/organization-object/) - [Users](https://www.bloomtext.com/developers/api/reference/user-object/) - [Conversations](https://www.bloomtext.com/developers/api/reference/conversation-object/) - [Messages](https://www.bloomtext.com/developers/api/reference/message-object/) - [Reactions](https://www.bloomtext.com/developers/api/reference/reaction-object/) - [Participants](https://www.bloomtext.com/developers/api/reference/participant-object/) - [Broadcasts](https://www.bloomtext.com/developers/api/reference/broadcast-object/) - [Exports](https://www.bloomtext.com/developers/api/reference/export-object/) - [Webhook events](https://www.bloomtext.com/developers/api/reference/webhook-events/) ## Conventions - **Authentication:** send `Authorization: Bearer bt_live_…` on every request. See [Authentication](https://www.bloomtext.com/developers/api/authentication/). - **IDs:** every object has a stable UUID `id`. - **Timestamps:** RFC 3339 strings in UTC, like `2026-09-21T12:00:00Z`. - **Lists:** cursor-paginated with `page[limit]` and `page[after]`. See [Pagination](https://www.bloomtext.com/developers/api/pagination/). - **Writes:** every `POST` takes an `Idempotency-Key`. See [Idempotency](https://www.bloomtext.com/developers/api/idempotency/). - **Errors:** RFC 9457 problem details. See [Errors](https://www.bloomtext.com/developers/api/errors/). ## All endpoints ### Organization | Method | Path | Endpoint | | --- | --- | --- | | GET | `/organization` | [Retrieve the organization](https://www.bloomtext.com/developers/api/reference/get-organization/) | ### Users | Method | Path | Endpoint | | --- | --- | --- | | GET | `/users` | [List users](https://www.bloomtext.com/developers/api/reference/list-users/) | | GET | `/users/{userId}` | [Retrieve a user](https://www.bloomtext.com/developers/api/reference/get-user/) | ### Conversations | Method | Path | Endpoint | | --- | --- | --- | | GET | `/conversations` | [List conversations](https://www.bloomtext.com/developers/api/reference/list-conversations/) | | GET | `/conversations/{conversationId}` | [Retrieve a conversation](https://www.bloomtext.com/developers/api/reference/get-conversation/) | ### Messages | Method | Path | Endpoint | | --- | --- | --- | | GET | `/conversations/{conversationId}/messages` | [List messages](https://www.bloomtext.com/developers/api/reference/list-messages/) | | POST | `/conversations/{conversationId}/messages` | [Send a message](https://www.bloomtext.com/developers/api/reference/create-message/) | | GET | `/messages/{messageId}/replies` | [List replies to a message](https://www.bloomtext.com/developers/api/reference/list-replies/) | ### Reactions | Method | Path | Endpoint | | --- | --- | --- | | GET | `/messages/{messageId}/reactions` | [List reactions](https://www.bloomtext.com/developers/api/reference/list-reactions/) | | POST | `/messages/{messageId}/reactions` | [Add a reaction](https://www.bloomtext.com/developers/api/reference/create-reaction/) | | DELETE | `/messages/{messageId}/reactions` | [Remove a reaction](https://www.bloomtext.com/developers/api/reference/delete-reaction/) | ### Participants | Method | Path | Endpoint | | --- | --- | --- | | GET | `/conversations/{conversationId}/participants` | [List conversation participants](https://www.bloomtext.com/developers/api/reference/list-participants/) | | POST | `/conversations/{conversationId}/participants` | [Add a participant](https://www.bloomtext.com/developers/api/reference/add-participant/) | | DELETE | `/conversations/{conversationId}/participants/{userId}` | [Remove a participant](https://www.bloomtext.com/developers/api/reference/remove-participant/) | ### Broadcasts | Method | Path | Endpoint | | --- | --- | --- | | GET | `/broadcasts` | [List broadcasts](https://www.bloomtext.com/developers/api/reference/list-broadcasts/) | | GET | `/broadcasts/{broadcastId}` | [Retrieve a broadcast](https://www.bloomtext.com/developers/api/reference/get-broadcast/) | | GET | `/broadcasts/{broadcastId}/messages` | [List broadcast messages](https://www.bloomtext.com/developers/api/reference/list-broadcast-messages/) | ### Exports | Method | Path | Endpoint | | --- | --- | --- | | POST | `/conversations/{conversationId}/exports` | [Export a conversation](https://www.bloomtext.com/developers/api/reference/create-chat-export/) | | GET | `/exports/{exportId}` | [Retrieve an export](https://www.bloomtext.com/developers/api/reference/get-chat-export/) | --- # The Organization object > Attributes of a BloomText Organization and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/organization-object/ The organization your API key belongs to. ### Attributes - `id` (UUID, required): Stable organization UUID. - `name` (string, required): Organization display name. ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | GET | `/organization` | [Retrieve the organization](https://www.bloomtext.com/developers/api/reference/get-organization/) | ```json filename="The Organization object" { "id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "name": "Riverside Family Clinic" } ``` --- # Retrieve the organization > Returns the ID and name of the organization that owns the API key. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/get-organization/ `GET /organization` Returns the ID and name of the organization that owns the API key. **Scope** `organization:read` ### Returns Returns `200 OK` with the [Organization object](https://www.bloomtext.com/developers/api/reference/organization-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/organization" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/organization', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/organization", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/organization", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/organization") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "name": "Riverside Family Clinic" } ``` --- # The User object > Attributes of a BloomText User and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/user-object/ Members of your organization. ### Attributes - `id` (UUID, required): Unique ID of the user. - `name` (string, required): Display name. - `role` (string, required): The user's role in the organization. ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | GET | `/users` | [List users](https://www.bloomtext.com/developers/api/reference/list-users/) | | GET | `/users/{userId}` | [Retrieve a user](https://www.bloomtext.com/developers/api/reference/get-user/) | ```json filename="The User object" { "id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "name": "Jordan Lee", "role": "member" } ``` --- # List users > Lists the members of your organization. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/list-users/ `GET /users` Lists the members of your organization. **Scope** `users:read` ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [User object](https://www.bloomtext.com/developers/api/reference/user-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/users" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/users', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/users", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/users", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/users") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "name": "Jordan Lee", "role": "member" } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # Retrieve a user > Returns a single member of your organization. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/get-user/ `GET /users/{userId}` Returns a single member of your organization. **Scope** `users:read` ### Path parameters - `userId` (UUID, required): ID of the user. ### Returns Returns `200 OK` with the [User object](https://www.bloomtext.com/developers/api/reference/user-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/users/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/users/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/users/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/users/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/users/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "name": "Jordan Lee", "role": "member" } ``` --- # The Conversation object > Attributes of a BloomText Conversation and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/conversation-object/ Conversations your app user participates in. ### Attributes - `id` (UUID, required): Unique ID of the conversation. - `organization_id` (UUID, required): Organization the conversation belongs to. - `type` (direct, group, broadcast, required): Direct message, group, or broadcast conversation. - `created_at` (timestamp, required): When the conversation was created. ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | GET | `/conversations` | [List conversations](https://www.bloomtext.com/developers/api/reference/list-conversations/) | | GET | `/conversations/{conversationId}` | [Retrieve a conversation](https://www.bloomtext.com/developers/api/reference/get-conversation/) | ```json filename="The Conversation object" { "id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "type": "direct", "created_at": "2026-09-21T15:04:05Z" } ``` --- # List conversations > Lists the conversations your app user is a participant in. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/list-conversations/ `GET /conversations` Lists the conversations your app user is a participant in. **Scope** `conversations:read` ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [Conversation object](https://www.bloomtext.com/developers/api/reference/conversation-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/conversations" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/conversations", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/conversations", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/conversations") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "type": "direct", "created_at": "2026-09-21T15:04:05Z" } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # Retrieve a conversation > Returns a conversation your app user participates in. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/get-conversation/ `GET /conversations/{conversationId}` Returns a conversation your app user participates in. **Scope** `conversations:read` ### Path parameters - `conversationId` (UUID, required): ID of the conversation. ### Returns Returns `200 OK` with the [Conversation object](https://www.bloomtext.com/developers/api/reference/conversation-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 403 | `conversation_membership_required` | Your app user is not a participant in this conversation. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "type": "direct", "created_at": "2026-09-21T15:04:05Z" } ``` --- # The Message object > Attributes of a BloomText Message and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/message-object/ Messages and threaded replies. ### Attributes - `id` (UUID, required): Unique ID of the message. - `conversation_id` (UUID, required): Conversation the message was sent in. - `sender_id` (UUID, required): User or app user who sent the message. - `created_at` (timestamp, required): When the message was sent. - `type` (text, file, required): Whether the message carries text or a file. - `body` (string or null): Message text. Null for file messages. - `file` (FileReference or null): File metadata. Null for text messages. Child attributes: - `id` (UUID, required): Unique ID of the file. - `name` (string, required): Original file name. - `mime` (string or null): MIME type, such as `application/pdf`. - `size_bytes` (integer or null): File size in bytes. - `reply_to_message_id` (UUID or null): Message this is a reply to. Null for top-level messages. ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | GET | `/conversations/{conversationId}/messages` | [List messages](https://www.bloomtext.com/developers/api/reference/list-messages/) | | POST | `/conversations/{conversationId}/messages` | [Send a message](https://www.bloomtext.com/developers/api/reference/create-message/) | | GET | `/messages/{messageId}/replies` | [List replies to a message](https://www.bloomtext.com/developers/api/reference/list-replies/) | ```json filename="The Message object" { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "created_at": "2026-09-21T15:04:05Z", "type": "text", "body": "Your appointment is confirmed for Tuesday at 10:00.", "file": null, "reply_to_message_id": null } ``` --- # List messages > Lists messages in a conversation, oldest first. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/list-messages/ `GET /conversations/{conversationId}/messages` Lists messages in a conversation, oldest first. **Scope** `messages:read` ### Path parameters - `conversationId` (UUID, required): ID of the conversation. ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [Message object](https://www.bloomtext.com/developers/api/reference/message-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 403 | `conversation_membership_required` | Your app user is not a participant in this conversation. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "created_at": "2026-09-21T15:04:05Z", "type": "text", "body": "Your appointment is confirmed for Tuesday at 10:00.", "file": null, "reply_to_message_id": null } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # Send a message > Sends a text or file message as your app user. Set reply_to_message_id to reply in a thread. Source: https://www.bloomtext.com/developers/api/reference/create-message/ `POST /conversations/{conversationId}/messages` Sends a text or file message as your app user. Set `reply_to_message_id` to reply in a thread. **Scope** `messages:write` · **Idempotent** with `Idempotency-Key` ### Path parameters - `conversationId` (UUID, required): ID of the conversation. ### Headers - `Idempotency-Key` (UUID, required): A UUID you generate for each write. Retrying with the same key returns the original result for 24 hours. Reusing a key with a different method, path, or body returns 409. ### Body parameters Provide exactly one of body or file_id. - `body` (string): Message text, up to 10,000 characters. - `file_id` (UUID): ID of a file already uploaded to BloomText. Mutually exclusive with body. - `reply_to_message_id` (UUID or null): Set to reply in a thread. ### Returns Returns `201 Created` with the [Message object](https://www.bloomtext.com/developers/api/reference/message-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 403 | `conversation_membership_required` | Your app user is not a participant in this conversation. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 409 | `idempotency_key_reused` | The Idempotency-Key was already used with a different request. | | 422 | `validation_failed` | A field failed validation. See `field_errors`. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl -X POST "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"body":"Your appointment is confirmed for Tuesday at 10:00."}' ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({"body":"Your appointment is confirmed for Tuesday at 10:00."}), }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import uuid import requests response = requests.post( "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={"body":"Your appointment is confirmed for Tuesday at 10:00."}, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "bytes" "fmt" "io" "net/http" "os" "github.com/google/uuid" ) func main() { body := bytes.NewBufferString(`{"body":"Your appointment is confirmed for Tuesday at 10:00."}`) req, err := http.NewRequest("POST", "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages", body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) req.Header.Set("Idempotency-Key", uuid.NewString()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" require "securerandom" uri = URI("https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" request["Idempotency-Key"] = SecureRandom.uuid request["Content-Type"] = "application/json" request.body = { body: "Your appointment is confirmed for Tuesday at 10:00." }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 201 Created" { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "created_at": "2026-09-21T15:04:05Z", "type": "text", "body": "Your appointment is confirmed for Tuesday at 10:00.", "file": null, "reply_to_message_id": null } ``` --- # List replies to a message > Lists the replies to a message, oldest first. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/list-replies/ `GET /messages/{messageId}/replies` Lists the replies to a message, oldest first. **Scope** `messages:read` ### Path parameters - `messageId` (UUID, required): ID of the message. ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [Message object](https://www.bloomtext.com/developers/api/reference/message-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/replies" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/replies', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/replies", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/replies", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/replies") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "created_at": "2026-09-21T15:04:05Z", "type": "text", "body": "Your appointment is confirmed for Tuesday at 10:00.", "file": null, "reply_to_message_id": null } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # The Reaction object > Attributes of a BloomText Reaction and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/reaction-object/ Emoji reactions on messages. ### Attributes - `message_id` (UUID, required): Message the reaction is on. - `user_id` (UUID, required): User who reacted. - `emoji` (string, required): The emoji. - `created_at` (timestamp, required): When the reaction was added. ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | GET | `/messages/{messageId}/reactions` | [List reactions](https://www.bloomtext.com/developers/api/reference/list-reactions/) | | POST | `/messages/{messageId}/reactions` | [Add a reaction](https://www.bloomtext.com/developers/api/reference/create-reaction/) | | DELETE | `/messages/{messageId}/reactions` | [Remove a reaction](https://www.bloomtext.com/developers/api/reference/delete-reaction/) | ```json filename="The Reaction object" { "message_id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "emoji": "👍", "created_at": "2026-09-21T15:04:05Z" } ``` --- # List reactions > Lists the emoji reactions on a message. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/list-reactions/ `GET /messages/{messageId}/reactions` Lists the emoji reactions on a message. **Scope** `reactions:read` ### Path parameters - `messageId` (UUID, required): ID of the message. ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [Reaction object](https://www.bloomtext.com/developers/api/reference/reaction-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "message_id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "emoji": "👍", "created_at": "2026-09-21T15:04:05Z" } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # Add a reaction > Adds an emoji reaction to a message as your app user. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/create-reaction/ `POST /messages/{messageId}/reactions` Adds an emoji reaction to a message as your app user. **Scope** `reactions:write` · **Idempotent** with `Idempotency-Key` ### Path parameters - `messageId` (UUID, required): ID of the message. ### Headers - `Idempotency-Key` (UUID, required): A UUID you generate for each write. Retrying with the same key returns the original result for 24 hours. Reusing a key with a different method, path, or body returns 409. ### Body parameters - `emoji` (string, required): A single emoji, such as 👍. ### Returns Returns `201 Created` with the [Reaction object](https://www.bloomtext.com/developers/api/reference/reaction-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 409 | `idempotency_key_reused` | The Idempotency-Key was already used with a different request. | | 422 | `validation_failed` | A field failed validation. See `field_errors`. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl -X POST "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"emoji":"👍"}' ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({"emoji":"👍"}), }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import uuid import requests response = requests.post( "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={"emoji":"👍"}, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "bytes" "fmt" "io" "net/http" "os" "github.com/google/uuid" ) func main() { body := bytes.NewBufferString(`{"emoji":"👍"}`) req, err := http.NewRequest("POST", "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions", body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) req.Header.Set("Idempotency-Key", uuid.NewString()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" require "securerandom" uri = URI("https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" request["Idempotency-Key"] = SecureRandom.uuid request["Content-Type"] = "application/json" request.body = { emoji: "👍" }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 201 Created" { "message_id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "emoji": "👍", "created_at": "2026-09-21T15:04:05Z" } ``` --- # Remove a reaction > Removes your app user's reaction from a message. Reactions have no ID of their own, so pass the exact emoji you added. Source: https://www.bloomtext.com/developers/api/reference/delete-reaction/ `DELETE /messages/{messageId}/reactions` Removes your app user's reaction from a message. Reactions have no ID of their own, so pass the exact emoji you added. **Scope** `reactions:write` ### Path parameters - `messageId` (UUID, required): ID of the message. ### Query parameters - `emoji` (string, required): The exact emoji your app user reacted with. ### Returns Returns `204 No Content` with an empty body. ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl -X DELETE "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions?emoji=%F0%9F%91%8D" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions?emoji=%F0%9F%91%8D', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) ``` ```python filename="Request" import os import requests response = requests.delete( "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions?emoji=%F0%9F%91%8D", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("DELETE", "https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions?emoji=%F0%9F%91%8D", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/messages/4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1/reactions?emoji=%F0%9F%91%8D") request = Net::HTTP::Delete.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts response.code ``` ```text filename="Response" HTTP/1.1 204 No Content ``` --- # The Participant object > Attributes of a BloomText Participant and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/participant-object/ Who is in a conversation. ### Attributes - `user_id` (UUID, required): The participating user. - `conversation_id` (UUID, required): The conversation. - `joined_at` (timestamp, required): When the user joined the conversation. ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | GET | `/conversations/{conversationId}/participants` | [List conversation participants](https://www.bloomtext.com/developers/api/reference/list-participants/) | | POST | `/conversations/{conversationId}/participants` | [Add a participant](https://www.bloomtext.com/developers/api/reference/add-participant/) | | DELETE | `/conversations/{conversationId}/participants/{userId}` | [Remove a participant](https://www.bloomtext.com/developers/api/reference/remove-participant/) | ```json filename="The Participant object" { "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "joined_at": "2026-09-21T15:04:05Z" } ``` --- # List conversation participants > Lists the participants in a conversation. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/list-participants/ `GET /conversations/{conversationId}/participants` Lists the participants in a conversation. **Scope** `participants:read` ### Path parameters - `conversationId` (UUID, required): ID of the conversation. ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [Participant object](https://www.bloomtext.com/developers/api/reference/participant-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 403 | `conversation_membership_required` | Your app user is not a participant in this conversation. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "joined_at": "2026-09-21T15:04:05Z" } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # Add a participant > Adds a member of your organization to a conversation. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/add-participant/ `POST /conversations/{conversationId}/participants` Adds a member of your organization to a conversation. **Scope** `participants:write` · **Idempotent** with `Idempotency-Key` ### Path parameters - `conversationId` (UUID, required): ID of the conversation. ### Headers - `Idempotency-Key` (UUID, required): A UUID you generate for each write. Retrying with the same key returns the original result for 24 hours. Reusing a key with a different method, path, or body returns 409. ### Body parameters - `user_id` (UUID, required): Organization member to add. ### Returns Returns `201 Created` with the [Participant object](https://www.bloomtext.com/developers/api/reference/participant-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 403 | `conversation_membership_required` | Your app user is not a participant in this conversation. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 409 | `idempotency_key_reused` | The Idempotency-Key was already used with a different request. | | 422 | `validation_failed` | A field failed validation. See `field_errors`. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl -X POST "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"user_id":"8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55"}' ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({"user_id":"8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55"}), }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import uuid import requests response = requests.post( "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={"user_id":"8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55"}, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "bytes" "fmt" "io" "net/http" "os" "github.com/google/uuid" ) func main() { body := bytes.NewBufferString(`{"user_id":"8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55"}`) req, err := http.NewRequest("POST", "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants", body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) req.Header.Set("Idempotency-Key", uuid.NewString()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" require "securerandom" uri = URI("https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" request["Idempotency-Key"] = SecureRandom.uuid request["Content-Type"] = "application/json" request.body = { user_id: "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55" }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 201 Created" { "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "joined_at": "2026-09-21T15:04:05Z" } ``` --- # Remove a participant > Removes a participant from a conversation. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/remove-participant/ `DELETE /conversations/{conversationId}/participants/{userId}` Removes a participant from a conversation. **Scope** `participants:write` ### Path parameters - `conversationId` (UUID, required): ID of the conversation. - `userId` (UUID, required): ID of the user. ### Returns Returns `204 No Content` with an empty body. ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 403 | `conversation_membership_required` | Your app user is not a participant in this conversation. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl -X DELETE "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) ``` ```python filename="Request" import os import requests response = requests.delete( "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("DELETE", "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/participants/8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55") request = Net::HTTP::Delete.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end puts response.code ``` ```text filename="Response" HTTP/1.1 204 No Content ``` --- # The Broadcast object > Attributes of a BloomText Broadcast and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/broadcast-object/ Broadcast campaigns and their delivery status. ### Attributes - `id` (UUID, required): Unique ID of the broadcast. - `organization_id` (UUID, required): Organization that sent the broadcast. - `status` (draft, queued, sent, partial, failed, required): Delivery status. `partial` means some recipients failed. - `created_at` (timestamp, required): When the broadcast was created. ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | GET | `/broadcasts` | [List broadcasts](https://www.bloomtext.com/developers/api/reference/list-broadcasts/) | | GET | `/broadcasts/{broadcastId}` | [Retrieve a broadcast](https://www.bloomtext.com/developers/api/reference/get-broadcast/) | | GET | `/broadcasts/{broadcastId}/messages` | [List broadcast messages](https://www.bloomtext.com/developers/api/reference/list-broadcast-messages/) | ```json filename="The Broadcast object" { "id": "1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "status": "sent", "created_at": "2026-09-21T15:04:05Z" } ``` --- # List broadcasts > Lists your organization's broadcasts. Reading broadcasts does not give access to the conversations they were sent to. Source: https://www.bloomtext.com/developers/api/reference/list-broadcasts/ `GET /broadcasts` Lists your organization's broadcasts. Reading broadcasts does not give access to the conversations they were sent to. **Scope** `broadcasts:read` ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [Broadcast object](https://www.bloomtext.com/developers/api/reference/broadcast-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/broadcasts" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/broadcasts', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/broadcasts", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/broadcasts", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/broadcasts") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "id": "1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "status": "sent", "created_at": "2026-09-21T15:04:05Z" } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # Retrieve a broadcast > Returns a broadcast and its delivery status. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/get-broadcast/ `GET /broadcasts/{broadcastId}` Returns a broadcast and its delivery status. **Scope** `broadcasts:read` ### Path parameters - `broadcastId` (UUID, required): ID of the broadcast. ### Returns Returns `200 OK` with the [Broadcast object](https://www.bloomtext.com/developers/api/reference/broadcast-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "id": "1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "status": "sent", "created_at": "2026-09-21T15:04:05Z" } ``` --- # List broadcast messages > Lists the messages sent by a broadcast. Parameters, responses, and code examples in cURL, JavaScript, Python, Go, and Ruby. Source: https://www.bloomtext.com/developers/api/reference/list-broadcast-messages/ `GET /broadcasts/{broadcastId}/messages` Lists the messages sent by a broadcast. **Scope** `broadcasts:read` ### Path parameters - `broadcastId` (UUID, required): ID of the broadcast. ### Query parameters - `page[limit]` (integer): Number of records to return. Defaults to 50, maximum 100. - `page[after]` (string): Cursor from the previous page's `pagination.next_cursor`. ### Returns Returns `200 OK` with a `data` array of [Message object](https://www.bloomtext.com/developers/api/reference/message-object/)s and a `pagination` object. See [pagination](https://www.bloomtext.com/developers/api/pagination/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c/messages" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c/messages', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c/messages", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c/messages", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/broadcasts/1f6a9c3e-2d4b-4e8a-9b7c-5d3e2f1a0b9c/messages") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "data": [ { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "created_at": "2026-09-21T15:04:05Z", "type": "text", "body": "Your appointment is confirmed for Tuesday at 10:00.", "file": null, "reply_to_message_id": null } ], "pagination": { "next_cursor": null, "has_more": false } } ``` --- # The Export object > Attributes of a BloomText Export and the endpoints that return it. Source: https://www.bloomtext.com/developers/api/reference/export-object/ Asynchronous conversation exports. ### Attributes - `id` (UUID, required): Unique ID of the export. - `status` (queued, running, ready, failed, required): Progress of the export. - `created_at` (timestamp, required): When the export was requested. - `download_url` (URL or null): Short-lived download URL ### Endpoints | Method | Path | Endpoint | | --- | --- | --- | | POST | `/conversations/{conversationId}/exports` | [Export a conversation](https://www.bloomtext.com/developers/api/reference/create-chat-export/) | | GET | `/exports/{exportId}` | [Retrieve an export](https://www.bloomtext.com/developers/api/reference/get-chat-export/) | ```json filename="The Export object" { "id": "7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e", "status": "ready", "created_at": "2026-09-21T15:04:05Z", "download_url": "https://files.bloomtext.com/exports/7a2e4c6b.zip?expires=1790003600&signature=…" } ``` --- # Export a conversation > Starts an asynchronous export of one conversation. Poll the export, or listen for the export.ready webhook, to get the download URL. Source: https://www.bloomtext.com/developers/api/reference/create-chat-export/ `POST /conversations/{conversationId}/exports` Starts an asynchronous export of one conversation. Poll the export, or listen for the `export.ready` webhook, to get the download URL. **Scope** `exports:write` · **Idempotent** with `Idempotency-Key` ### Path parameters - `conversationId` (UUID, required): ID of the conversation. ### Headers - `Idempotency-Key` (UUID, required): A UUID you generate for each write. Retrying with the same key returns the original result for 24 hours. Reusing a key with a different method, path, or body returns 409. ### Body parameters Limits an export to a date range and projection. - `projection` (messages, messages_and_participants, required): What to export: messages only, or messages plus the participant list. - `from` (timestamp): Earliest message to include. Defaults to the start of the conversation. - `to` (timestamp): Latest message to include. Defaults to now. - `include_files` (boolean): Include file metadata in the export. ### Returns Returns `202 Accepted` with the [Export object](https://www.bloomtext.com/developers/api/reference/export-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 403 | `conversation_membership_required` | Your app user is not a participant in this conversation. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 409 | `idempotency_key_reused` | The Idempotency-Key was already used with a different request. | | 422 | `validation_failed` | A field failed validation. See `field_errors`. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl -X POST "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/exports" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"projection":"messages","from":"2026-09-01T00:00:00Z","to":"2026-09-30T23:59:59Z"}' ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/exports', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({"projection":"messages","from":"2026-09-01T00:00:00Z","to":"2026-09-30T23:59:59Z"}), }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import uuid import requests response = requests.post( "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/exports", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={"projection":"messages","from":"2026-09-01T00:00:00Z","to":"2026-09-30T23:59:59Z"}, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "bytes" "fmt" "io" "net/http" "os" "github.com/google/uuid" ) func main() { body := bytes.NewBufferString(`{"projection":"messages","from":"2026-09-01T00:00:00Z","to":"2026-09-30T23:59:59Z"}`) req, err := http.NewRequest("POST", "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/exports", body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) req.Header.Set("Idempotency-Key", uuid.NewString()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" require "securerandom" uri = URI("https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/exports") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" request["Idempotency-Key"] = SecureRandom.uuid request["Content-Type"] = "application/json" request.body = { projection: "messages", from: "2026-09-01T00:00:00Z", to: "2026-09-30T23:59:59Z" }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 202 Accepted" { "id": "7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e", "status": "ready", "created_at": "2026-09-21T15:04:05Z", "download_url": "https://files.bloomtext.com/exports/7a2e4c6b.zip?expires=1790003600&signature=…" } ``` --- # Retrieve an export > Returns the status of an export and, once it is ready, a short-lived download URL. Source: https://www.bloomtext.com/developers/api/reference/get-chat-export/ `GET /exports/{exportId}` Returns the status of an export and, once it is ready, a short-lived download URL. **Scope** `exports:read` ### Path parameters - `exportId` (UUID, required): ID of the export. ### Returns Returns `200 OK` with the [Export object](https://www.bloomtext.com/developers/api/reference/export-object/). ### Errors Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint: | Status | Code | Meaning | | --- | --- | --- | | 400 | `invalid_request` | The request is malformed, such as invalid JSON. | | 401 | `unauthorized` | The API key is missing, revoked, or invalid. | | 403 | `insufficient_scope` | The key lacks the required scope. | | 404 | `not_found` | The resource doesn’t exist or isn’t visible to this key. | | 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. | ```bash filename="Request" curl "https://api.bloomtext.com/v1/exports/7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e" \ -H "Authorization: Bearer $BLOOMTEXT_API_KEY" ``` ```js filename="Request" const response = await fetch('https://api.bloomtext.com/v1/exports/7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e', { headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`, }, }) if (!response.ok) throw new Error(`BloomText API error: ${response.status}`) const data = await response.json() ``` ```python filename="Request" import os import requests response = requests.get( "https://api.bloomtext.com/v1/exports/7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e", headers={ "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}", }, ) response.raise_for_status() data = response.json() ``` ```go filename="Request" package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest("GET", "https://api.bloomtext.com/v1/exports/7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(out)) } ``` ```ruby filename="Request" require "net/http" require "json" uri = URI("https://api.bloomtext.com/v1/exports/7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end data = JSON.parse(response.body) ``` ```json filename="Response · 200 OK" { "id": "7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e", "status": "ready", "created_at": "2026-09-21T15:04:05Z", "download_url": "https://files.bloomtext.com/exports/7a2e4c6b.zip?expires=1790003600&signature=…" } ``` --- # Webhook events > Every webhook event type the BloomText API sends, with its signed envelope, payload fields, and an example. Source: https://www.bloomtext.com/developers/api/reference/webhook-events/ Every delivery shares one envelope. The `payload` changes with the event `type` and carries IDs, not message content, so fetch the resource with the API when you need more. See [Webhooks](https://www.bloomtext.com/developers/api/webhooks/) to set up an endpoint and verify signatures. ### The event envelope - `id` (UUID, required): Stable event UUID used for consumer deduplication. - `type` (conversation.message.created, message.reaction.created, message.reaction.deleted, conversation.participant.added, conversation.participant.removed, export.ready, required): - `occurred_at` (timestamp, required): - `organization_id` (UUID, required): - `delivery_attempt` (integer, required): - `payload` (object, required): Event-specific fields. See Webhooks for each event's payload. ## `conversation.message.created` A message was sent in a conversation your app user participates in, including messages your app user sent. ### Payload - `conversation_id` (UUID, required): - `message_id` (UUID, required): - `sender_id` (UUID, required): ```json filename="conversation.message.created" { "id": "2d8f1b0b-7b27-4b3b-bf2c-4d05d89d2f4d", "type": "conversation.message.created", "occurred_at": "2026-09-21T12:00:00Z", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "delivery_attempt": 1, "payload": { "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "message_id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "sender_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55" } } ``` ## `message.reaction.created` Someone added an emoji reaction to a message in one of your conversations. ### Payload - `conversation_id` (UUID, required): - `message_id` (UUID, required): - `user_id` (UUID, required): - `emoji` (string, required): ```json filename="message.reaction.created" { "id": "2d8f1b0b-7b27-4b3b-bf2c-4d05d89d2f4d", "type": "message.reaction.created", "occurred_at": "2026-09-21T12:00:00Z", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "delivery_attempt": 1, "payload": { "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "message_id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "emoji": "👍" } } ``` ## `message.reaction.deleted` Someone removed their reaction from a message. ### Payload - `conversation_id` (UUID, required): - `message_id` (UUID, required): - `user_id` (UUID, required): - `emoji` (string, required): ```json filename="message.reaction.deleted" { "id": "2d8f1b0b-7b27-4b3b-bf2c-4d05d89d2f4d", "type": "message.reaction.deleted", "occurred_at": "2026-09-21T12:00:00Z", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "delivery_attempt": 1, "payload": { "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "message_id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55", "emoji": "👍" } } ``` ## `conversation.participant.added` A user joined one of your conversations. ### Payload - `conversation_id` (UUID, required): - `user_id` (UUID, required): ```json filename="conversation.participant.added" { "id": "2d8f1b0b-7b27-4b3b-bf2c-4d05d89d2f4d", "type": "conversation.participant.added", "occurred_at": "2026-09-21T12:00:00Z", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "delivery_attempt": 1, "payload": { "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55" } } ``` ## `conversation.participant.removed` A user left or was removed from one of your conversations. If it was your app user, you receive no further events for that conversation. ### Payload - `conversation_id` (UUID, required): - `user_id` (UUID, required): ```json filename="conversation.participant.removed" { "id": "2d8f1b0b-7b27-4b3b-bf2c-4d05d89d2f4d", "type": "conversation.participant.removed", "occurred_at": "2026-09-21T12:00:00Z", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "delivery_attempt": 1, "payload": { "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "user_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55" } } ``` ## `export.ready` An export you started has finished. Fetch the export to get its short-lived `download_url`. ### Payload - `export_id` (UUID, required): - `conversation_id` (UUID, required): - `status` (string, required): ```json filename="export.ready" { "id": "2d8f1b0b-7b27-4b3b-bf2c-4d05d89d2f4d", "type": "export.ready", "occurred_at": "2026-09-21T12:00:00Z", "organization_id": "6db1e3f5-9b7f-4f2b-8be1-0f1e1d7d7d8c", "delivery_attempt": 1, "payload": { "export_id": "7a2e4c6b-9d1f-4b3a-8e5c-2f0d1b3a5c7e", "conversation_id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "status": "ready" } } ```