{"openapi":"3.1.0","info":{"title":"Blooio v4 API","version":"4.0.0-beta","description":"The Blooio v4 API is a multi-channel messaging API for iMessage, SMS, RCS, WhatsApp. It exposes channels, contacts and identities, chats, messages, reactions, polls, groups, webhooks, and a unified event feed.\n\n## Authentication\nAuthenticate with an API key as a bearer token: `Authorization: Bearer bl_live_...`. Create keys in the Blooio dashboard.\n\n## Conventions\n- **IDs** are prefixed and globally unique: channels `ch_`, contacts `ct_`, identities `cid_`, chats `chat_`, messages `msg_`, groups `grp_`, webhooks `wh_`, webhook deliveries `wdel_`, events `evt_`.\n- **List responses** normally return `{ \"data\": [...], \"has_more\": boolean, \"next_cursor\": string | null }` and use cursor pagination (`limit`, `cursor`). The contacts list additionally supports offset pagination for search, filtering, and sorting, returning a `pagination` object when that mode is used.\n- **Item responses** return `{ \"data\": { ... } }`. Message send endpoints return the message object at the top level (not wrapped).\n- **Errors** use `{ \"error\": { \"code\": string, \"message\": string, \"details\": object } }`. Authentication failures at the middleware boundary use a flatter shape `{ \"error\": string, \"message\": string, \"status\": number }`.","contact":{"name":"Blooio Support","email":"help@blooio.com","url":"https://docs.blooio.com"}},"servers":[{"url":"https://api.blooio.com/v4","description":"Production"}],"security":[{"BearerApiKey":[]}],"x-blooio-status":"beta","tags":[{"name":"Account","description":"Authenticated account, sender numbers, and contact cards."},{"name":"Location","description":"Find My friend locations (device passthrough)."},{"name":"Channels","description":"Sending surfaces (iMessage/Blooio, Twilio, WhatsApp, RCS) and their capabilities."},{"name":"Priorities","description":"Channel routing / number pools: ordered sets of channels walked by priority for the agnostic send."},{"name":"Contacts","description":"Contacts, their channel identities, capabilities, timeline, and tags."},{"name":"Chats","description":"Conversations and chat-level actions (typing, read, contact card, background)."},{"name":"Messages","description":"Send messages and read message history, status, events, and reactions."},{"name":"Polls","description":"Native iMessage polls (Blooio channels)."},{"name":"Groups","description":"Group chats on group-capable channels (Blooio, WhatsApp)."},{"name":"Webhooks","description":"Webhook subscriptions and delivery attempts for inbound + lifecycle events."},{"name":"Events","description":"Unified activity feed across messages, polls, groups, reactions, typing, and messaging-safety state changes."},{"name":"Phone Numbers","description":"Phone number lookup and geocoding (enterprise plans)."},{"name":"API Keys","description":"Programmatic API key management — create keys, list them, deprecate them, and assign them to channels.\n\n**Blooio Apps (OAuth) only.** Every endpoint under this tag is callable *only* with a Blooio Apps OAuth access token (`blo_at_...`) whose install was granted the `apikeys:read` / `apikeys:manage` scopes. API-key and dashboard authentication are rejected with `403 oauth_token_required`. This mirrors the API-key management that has always lived in the dashboard, exposed to installed apps under explicit user consent."},{"name":"AI","description":"Model-backed assessment of conversations. The Bloobability gauge scores, per policy, how confident it is that the policy applies to a conversation, and returns a calibrated probability alongside a frozen accept/defer decision so you can automate on it."}],"paths":{"/me":{"get":{"tags":["Account"],"summary":"Get the authenticated account","description":"Returns the authenticated organization, linked devices/numbers, usage, and API key validity.","operationId":"getMe","responses":{"200":{"description":"Account context","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/priority":{"get":{"tags":["Account"],"summary":"Get the key's default priority","description":"Returns the calling API key's default channel priority, or `{ \"data\": null }` when none is configured (in which case the agnostic send falls back to the implicit Blooio pool of the key's own numbers).","operationId":"getMyDefaultPriority","responses":{"200":{"description":"The default priority (or null)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/Priority"},{"type":"null"}]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/numbers":{"get":{"tags":["Account"],"summary":"List sender numbers","description":"Lists the phone numbers / sender addresses available to this API key.","operationId":"listNumbers","responses":{"200":{"description":"Sender numbers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/numbers/{number}/contact-card":{"get":{"tags":["Account"],"summary":"Get a number's contact card","operationId":"getContactCard","parameters":[{"$ref":"#/components/parameters/NumberParam"}],"responses":{"200":{"description":"Contact card","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Account"],"summary":"Update a number's contact card","description":"Updates the contact card (name, avatar, sharing settings) shared from this number. At least one field is required.","operationId":"updateContactCard","parameters":[{"$ref":"#/components/parameters/NumberParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"avatar":{"type":"string","description":"Public image URL or data URI."},"sharing":{"type":"object","properties":{"enabled":{"type":"boolean"},"audience":{"type":"string"},"name_format":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Updated contact card","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/me/numbers/{number}/contact-card/avatar":{"post":{"tags":["Account"],"summary":"Upload a contact card avatar","description":"Uploads an avatar image for the number's contact card. Max 10MB.","operationId":"uploadContactCardAvatar","parameters":[{"$ref":"#/components/parameters/NumberParam"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"avatar":{"type":"string","format":"binary"},"first_name":{"type":"string","description":"Optional. Sent to the device with the photo so the write is not photo-only. Recommended: without it the device keeps whatever name it currently has."},"last_name":{"type":"string","description":"Optional. See first_name."}},"required":["avatar"]}}}},"responses":{"200":{"description":"Avatar uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me/numbers/{number}/call-forwarding":{"post":{"tags":["Account"],"summary":"Request call forwarding","description":"Opens a support request to forward calls from an owned dedicated or inbound Blooio number. The destination must be a US E.164 number. Shared or foreign source numbers are rejected.","operationId":"requestCallForwarding","parameters":[{"$ref":"#/components/parameters/NumberParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"forward_to":{"type":"string","pattern":"^\\+1[2-9]\\d{9}$","description":"US E.164 number to forward calls to."}},"required":["forward_to"]}}}},"responses":{"200":{"description":"Call forwarding requested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/location/contacts":{"get":{"tags":["Location"],"summary":"List Find My contacts","description":"Lists Find My friends sharing their location with the linked Apple account.","operationId":"listLocationContacts","responses":{"200":{"description":"Find My contacts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/location/contacts/refresh":{"get":{"tags":["Location"],"summary":"Refresh Find My locations (GET)","description":"Convenience GET form of the refresh endpoint. Forces a refresh of Find My location data from the device.","operationId":"refreshLocationContactsGet","responses":{"200":{"description":"Refresh result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Location"],"summary":"Refresh Find My locations","description":"Forces a refresh of Find My location data from the device.","operationId":"refreshLocationContacts","responses":{"200":{"description":"Refresh result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/location/contacts/{handle}":{"get":{"tags":["Location"],"summary":"Get one Find My contact","operationId":"getLocationContact","parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string"},"description":"Contact handle to resolve — a phone number in E.164 format, an email address, or an Apple ID. URL-encode the value."}],"responses":{"200":{"description":"Find My contact","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/channels":{"get":{"tags":["Channels"],"summary":"List channels","description":"Lists messaging channels for the organization. Ordinary sends do not require a channel id; use this endpoint to inspect capabilities or obtain an exact channel id for advanced integrations.","operationId":"listChannels","parameters":[{"name":"type","in":"query","schema":{"$ref":"#/components/schemas/ChannelType"},"description":"Filter by channel type."},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/ChannelStatus"},"description":"Filter by channel status."},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of channels","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}},"example":{"data":[{"id":"ch_018f7b2a-1c3d-7e4f-9a2b-0c1d2e3f4a5b","type":"blooio","address":"+15551234567","status":"active","capabilities":{"protocols":["imessage","sms","rcs"],"content":["text","media"]},"created_at":1718000000000}],"has_more":false,"next_cursor":null}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/channels/blooio/available":{"get":{"tags":["Channels"],"summary":"List available Blooio numbers","description":"Browse Blooio number inventory available to purchase. `type=shared` returns available shared lines; `type=dedicated` returns unassigned dedicated inventory. When one or more `area_code` values are supplied with `type=dedicated` or `type=inbound`, the response is an area-code quote: each requested code is reported as either in stock (provisioned free) or requiring a custom order (a custom area-code fee applies and a provisioning ticket is opened at purchase). Requires the number purchase API feature to be enabled for the organization, and the `numbers:read` scope for OAuth apps.","operationId":"listAvailableBlooioNumbers","parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["shared","dedicated","inbound"],"default":"shared"},"description":"Plan family to browse."},{"name":"area_code","in":"query","schema":{"type":"string"},"description":"A desired 3-digit US area code. Repeat the parameter (or pass a comma-separated `area_codes`) to quote several. Only meaningful for `dedicated`/`inbound`."},{"name":"country","in":"query","schema":{"type":"string"},"description":"Filter inventory by ISO country code (e.g. `US`)."},{"$ref":"#/components/parameters/LimitParam"}],"responses":{"200":{"description":"Available inventory or an area-code quote","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","description":"Inventory rows (browse mode) or quote lines (when `area_code` is supplied).","items":{"oneOf":[{"type":"object","description":"Inventory row (browse mode). The full number is never returned — only a masked form.","properties":{"masked_national":{"type":["string","null"],"description":"National format with only the area code revealed, e.g. `(801) ***-****`."},"area_code":{"type":["string","null"]},"country_code":{"type":["string","null"],"description":"E.164 country calling code, e.g. `1`."},"phone_number_country":{"type":["string","null"],"description":"ISO country code, e.g. `US`."},"location":{"type":"string","description":"Geocoded `City, ST` label for NANPA numbers, when resolvable. Omitted otherwise."}}},{"type":"object","description":"Quote line (when `area_code` is supplied).","properties":{"area_code":{"type":"string"},"matched":{"type":"boolean","description":"`true` when the code is in stock (provisioned free)."},"custom_order":{"type":"boolean","description":"Present and `true` when the code needs a custom order (custom area-code fee applies)."},"auto_assigned":{"type":"boolean","description":"Present when no specific code was requested for this line and one is auto-assigned."},"zip_code":{"type":"string"}}}]}},"matched_count":{"type":"integer","description":"Quote mode only: requested area codes found in stock."},"custom_order_count":{"type":"integer","description":"Quote mode only: requested area codes needing a custom (ticketed, billed) order."},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}},"examples":{"browse":{"summary":"Browse shared inventory","value":{"data":[{"masked_national":"(801) ***-****","area_code":"801","country_code":"1","phone_number_country":"US","location":"Salt Lake City, UT"}],"has_more":false,"next_cursor":null}},"quote":{"summary":"Area-code quote","value":{"data":[{"area_code":"801","matched":true},{"area_code":"212","matched":false,"custom_order":true}],"matched_count":1,"custom_order_count":1,"has_more":false,"next_cursor":null}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/channels/blooio/purchases":{"post":{"tags":["Channels"],"summary":"Purchase Blooio numbers","description":"Purchase one or more Blooio lines. Purchases are asynchronous and billable: the call charges the organization's default payment method and returns a `purchase_id`; the lines are provisioned when Stripe confirms the invoice, at which point a `number.purchase.completed` webhook fires (or `number.purchase.failed`). A card requiring 3DS/SCA returns `status: action_required` with an `action_url`, and also emits `number.purchase.action_required`.\n\n**Idempotency is required** — send a unique `Idempotency-Key` header; replaying the same key returns the same purchase rather than charging twice.\n\nRules enforced server-side (identically to the dashboard): the organization must have a saved payment method (else `402 no_payment_method`); shared plans are limited to one line per organization (`409 shared_number_limit`); and a single order may not exceed the per-order line cap (`400 line_limit_exceeded`, default 10 — apply for an increase in the dashboard Limits tab). Requested area codes that are out of stock incur a custom area-code fee and open a provisioning ticket; in-stock codes are assigned immediately with no custom fee. Requires the number purchase API feature to be enabled, and the `numbers:manage` scope for OAuth apps.","operationId":"purchaseBlooioNumbers","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Unique key that makes the billable purchase safe to retry."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["shared_nc","shared_com","dedicated_com","dedicated_ent","inbound_basic"],"description":"Plan id to purchase (determines shared / dedicated / inbound). Use the specific id, not the tier name — e.g. `dedicated_com` or `dedicated_ent`, not `dedicated` (which returns `invalid_plan`)."},"quantity":{"type":"integer","minimum":1,"default":1,"description":"Number of new lines (dedicated/inbound). Shared is always 1."},"area_codes":{"type":"array","items":{"type":"string"},"description":"Preferred 3-digit US area codes (dedicated/inbound). Out-of-stock codes incur a custom fee and open a ticket."},"zip_codes":{"type":"array","items":{"type":"string"},"description":"Preferred ZIP codes (dedicated/inbound), used when an area code is not specified."}}},"example":{"plan":"dedicated_com","quantity":2,"area_codes":["415","628"]}}}},"responses":{"202":{"description":"Purchase accepted; provisioning proceeds asynchronously.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"purchase_id":{"type":"string"},"status":{"type":"string","enum":["provisioning","action_required"]},"action_url":{"type":["string","null"],"description":"Present when `status` is `action_required`: complete card authentication here."}}}}},"example":{"data":{"purchase_id":"idem_abc123","status":"provisioning"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Payment required or failed. `code` is `no_payment_method` (no saved card) or `payment_failed` (the card was declined).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Conflicts with a purchase rule, e.g. `shared_number_limit`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/channels/blooio/purchases/{purchaseId}":{"get":{"tags":["Channels"],"summary":"Get purchase status","description":"Poll the status of an asynchronous number purchase by its `purchase_id`. Returns `pending`/`provisioning`, `completed` (with the provisioned allocations), or `failed`. Requires the `numbers:read` scope for OAuth apps.","operationId":"getBlooioPurchase","parameters":[{"name":"purchaseId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Purchase status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"purchase_id":{"type":"string"},"status":{"type":"string"},"allocations":{"type":"array","items":{"type":"object"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/channels/{channel}":{"get":{"tags":["Channels"],"summary":"Get a channel","operationId":"getChannel","parameters":[{"$ref":"#/components/parameters/ChannelIdParam"}],"responses":{"200":{"description":"A channel","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Channel"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Channels"],"summary":"Remove (unsubscribe) a Blooio number","description":"Removes/unsubscribes an owned Blooio line, cancelling its subscription — the same flow as the dashboard unsubscribe button. Accepts either the E.164 number or the `ch_` channel id as `{channel}`. A `reasons` array (why the customer is unsubscribing) is required; the same reason handling updates the CRM, posts to Slack, and emits a `number.removed` webhook. Requires the `numbers:manage` scope for OAuth apps.","operationId":"removeBlooioNumber","parameters":[{"$ref":"#/components/parameters/ChannelIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reasons"],"properties":{"reasons":{"type":"array","items":{"type":"string"},"description":"One or more churn reason codes/labels for the removal."}}},"example":{"reasons":["too_expensive","no_longer_needed"]}}}},"responses":{"200":{"description":"The line was removed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"phone_number":{"type":"string"},"channel_id":{"type":["string","null"]},"reasons":{"type":"array","items":{"type":"string"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/channels/{channel}/capabilities":{"get":{"tags":["Channels"],"summary":"Get channel capabilities","description":"Returns supported protocols, content types, actions, and interactive features for a channel.","operationId":"getChannelCapabilities","parameters":[{"$ref":"#/components/parameters/ChannelIdParam"}],"responses":{"200":{"description":"Channel capabilities","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"channel_id":{"type":"string"},"type":{"$ref":"#/components/schemas/ChannelType"},"capabilities":{"$ref":"#/components/schemas/ChannelCapabilities"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/channels/{channel}/settings":{"get":{"tags":["Channels"],"summary":"Get channel settings","description":"Per-channel settings for channel types that support them. `settings` and `available` are per channel type; the Blooio (P2P numbers) provider exposes `auto_mark_read` and `auto_share_contact`. `available[key]` says whether THIS channel may turn `key` on (type-gated). A channel whose type has no settings yet returns `403 settings_unsupported_for_channel_type`.","operationId":"getChannelSettings","parameters":[{"$ref":"#/components/parameters/ChannelIdParam"}],"responses":{"200":{"description":"Channel settings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ChannelSettings"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Channels"],"summary":"Update channel settings","description":"Partial update of a channel's settings (send only the keys you want to change). Keys are per channel type. Blooio (P2P numbers): `auto_mark_read` is available on dedicated and inbound lines; `auto_share_contact` is dedicated-only AND the line must have a contact card (name or photo) set up. Turning a setting OFF is always allowed. Errors return a machine-readable `code`: `settings_unsupported_for_channel_type` (403, non-Blooio channel), `auto_mark_read_not_allowed` / `auto_share_contact_not_allowed` (403, line type can't enable it), `contact_card_not_set_up` (403, enable auto_share_contact with no contact card), `no_settings_provided` (422, empty update), `invalid_settings` (422, unknown key or non-boolean value). A malformed channel selector or non-object body is `400`.","operationId":"updateChannelSettings","parameters":[{"$ref":"#/components/parameters/ChannelIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelSettingsUpdate"},"examples":{"autoMarkRead":{"summary":"Blooio: enable auto mark-read","value":{"auto_mark_read":true}},"autoShareContact":{"summary":"Blooio: enable auto share contact (dedicated + contact card)","value":{"auto_share_contact":true}}}}}},"responses":{"200":{"description":"Updated channel settings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ChannelSettings"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/channels/{channel}/profile":{"put":{"tags":["Channels"],"summary":"Update channel profile","description":"Partial update of the channel profile (`display_name`, `about`, `email`, `avatar`). Stored under `metadata.profile` and returned on `GET /channels/{channel}`. Null or empty string clears a field. Unknown keys are rejected. This writes Blooio metadata only; it does not push a display name to the device.","operationId":"updateChannelProfile","parameters":[{"$ref":"#/components/parameters/ChannelIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelProfile"},"example":{"display_name":"Blooio Support","about":"We reply within minutes.","email":"support@blooio.com"}}}},"responses":{"200":{"description":"Updated channel, including `profile`","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Channel"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/channels/{channel}/messages":{"post":{"tags":["Channels"],"summary":"Send a message on a specific channel","description":"Channel-scoped equivalent of `POST /messages` with an explicit `from`: `:channel` accepts the same phone number (URL-encode `+` as `%2B`), alias, or `ch_...` value `from` accepts, and the body omits `from`. Multiple recipients on a group-capable channel (blooio/whatsapp) form a single group thread; on other channels they fan out to individual sends (max 100).","operationId":"sendMessageToChannel","parameters":[{"$ref":"#/components/parameters/ChannelIdParam"},{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendToChannelRequest"},"examples":{"text":{"summary":"Simple text","value":{"to":"+15551234567","text":"Hello from Blooio!"}}}}}},"responses":{"200":{"description":"Dry-run preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageSendResult"}}}},"201":{"description":"Message accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageSendResult"}}}},"207":{"description":"Multi-recipient fan-out with mixed results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FanOutResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/SafetyBlocked"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/ConversationLimited"},"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/priorities":{"get":{"tags":["Priorities"],"summary":"List priorities","description":"Lists channel priorities owned by the calling API key.","operationId":"listPriorities","parameters":[{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of priorities","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Priority"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Priorities"],"summary":"Create a priority","description":"Creates a channel priority. Channels sharing a `priority` (and type) form a pool; ascending priority is the send-time waterfall. The first priority created for a key becomes its default unless `is_default: false` is set.","operationId":"createPriority","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePriorityRequest"}}}},"responses":{"201":{"description":"Created priority","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Priority"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/priorities/{priorityId}":{"get":{"tags":["Priorities"],"summary":"Get a priority","operationId":"getPriority","parameters":[{"$ref":"#/components/parameters/PriorityIdParam"}],"responses":{"200":{"description":"A priority","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Priority"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Priorities"],"summary":"Update a priority","description":"Updates a priority's name, default flag, and/or channel set. When `channels` is supplied it replaces the priority's channels wholesale.","operationId":"updatePriority","parameters":[{"$ref":"#/components/parameters/PriorityIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePriorityRequest"}}}},"responses":{"200":{"description":"Updated priority","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Priority"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}},"delete":{"tags":["Priorities"],"summary":"Delete a priority","operationId":"deletePriority","parameters":[{"$ref":"#/components/parameters/PriorityIdParam"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/contacts":{"get":{"tags":["Contacts"],"summary":"List contacts","description":"Lists contacts. Cursor pagination is the default. Supplying `offset`, `q`, `tag`, `sort`, or `filter` switches to offset pagination and includes `pagination.total`.","operationId":"listContacts","parameters":[{"name":"identifier","in":"query","schema":{"type":"string"},"description":"Filter to the contact owning this phone/email."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive search across contact names, identities, and tags."},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Case-insensitive tag filter."},{"name":"sort","in":"query","schema":{"type":"string","enum":["recent","oldest","name_asc","name_desc"]}},{"name":"filter","in":"query","schema":{"type":"string","enum":["none","has_tags","no_tags","inbound_last","outbound_last"]}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0},"description":"Zero-based offset. Cannot be combined with cursor."},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of contacts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Contacts"],"summary":"Create a contact","description":"Creates a contact, optionally with one identity (phone/email on a channel type).","operationId":"createContact","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContactRequest"}}}},"responses":{"201":{"description":"Created contact","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Contact"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/contacts/{contactId}":{"get":{"tags":["Contacts"],"summary":"Get a contact","operationId":"getContact","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"responses":{"200":{"description":"A contact","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Contact"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Contacts"],"summary":"Update a contact","operationId":"updateContact","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}}}},"responses":{"200":{"description":"Updated contact","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Contact"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}},"delete":{"tags":["Contacts"],"summary":"Delete a contact","description":"Soft-deletes a contact and its identities.","operationId":"deleteContact","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/contacts/{contactId}/merge":{"post":{"tags":["Contacts"],"summary":"Merge a contact","description":"Collapses two auto-created contacts for the same person into one. The survivor is the `{contactId}` in the path; the `source_contact_id` in the body is absorbed into it. Every identity on the source is re-parented onto the survivor, tags are unioned, the survivor keeps its own name (falling back to the source's), and the source becomes a deprecated tombstone stamped with `merged_into`. Because chats and messages reference identities (not contacts), all conversations and timeline entries follow the moved identities automatically — nothing is rewritten or lost, and reads such as the survivor's timeline immediately reflect the merge. Idempotent: re-merging the same source into the same survivor returns 200; a source already merged into a different contact returns 409. Emits a `contact.merged` webhook event.","operationId":"mergeContact","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["source_contact_id"],"properties":{"source_contact_id":{"type":"string","description":"The contact to merge into the survivor, prefixed with `ct_`. Must differ from the path contact.","example":"ct_01HZ0000000000000000000000"}}}}}},"responses":{"200":{"description":"The merged survivor contact, now including the re-parented identities.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Contact"}}}}}},"400":{"description":"The source equals the survivor, or an id is malformed.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"},"code":{"type":"string"}}},"example":{"error":"ApiError","message":"Cannot merge a contact into itself","status":400,"code":"cannot_merge_self"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The source contact has already been merged into a different contact.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"},"code":{"type":"string"}}},"example":{"error":"ApiError","message":"Contact has already been merged","status":409,"code":"contact_already_merged"}}}},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/contacts/{contactId}/identities":{"get":{"tags":["Contacts"],"summary":"List contact identities","operationId":"listContactIdentities","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"responses":{"200":{"description":"Identities","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ContactIdentity"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Contacts"],"summary":"Attach an identity","description":"Attaches a phone/email identity (on a channel type) to a contact.","operationId":"attachContactIdentity","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachIdentityRequest"}}}},"responses":{"201":{"description":"Attached identity","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ContactIdentity"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/contacts/{contactId}/identities/{identityId}":{"patch":{"tags":["Contacts"],"summary":"Update an identity","description":"Renames a contact identity in place. An optional name updates the parent contact in the same request.","operationId":"updateContactIdentity","parameters":[{"$ref":"#/components/parameters/ContactIdParam"},{"name":"identityId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the contact identity, prefixed with `cid_`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["identifier"],"properties":{"identifier":{"type":"string"},"name":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"Updated identity","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ContactIdentity"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}},"delete":{"tags":["Contacts"],"summary":"Detach an identity","operationId":"detachContactIdentity","parameters":[{"$ref":"#/components/parameters/ContactIdParam"},{"name":"identityId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the contact identity (a phone/email tied to the contact), prefixed with `cid_`. Returned by the List contact identities endpoint."}],"responses":{"200":{"description":"Detached","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"detached":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/contacts/{contactId}/capabilities":{"get":{"tags":["Contacts"],"summary":"Get contact capabilities","description":"Returns per-identity reachability/capabilities (iMessage/SMS/RCS, etc.) for a contact. For iMessage (blooio) identities this is a LIVE per-number lookup performed by an active iMessage channel — the request is rejected with 409 if the API key has no active iMessage (blooio) channel provisioned.","operationId":"getContactCapabilities","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"responses":{"200":{"description":"Capabilities per identity","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"identifier":{"type":"string"},"channel_id":{"type":["string","null"]},"capabilities":{"type":"object","additionalProperties":true}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"No active iMessage (blooio) channel is provisioned for this API key, so live iMessage/SMS/RCS detection is unavailable.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"},"code":{"type":"string"}}},"example":{"error":"ApiError","message":"No active iMessage (blooio) channel is provisioned for this API key. Live per-number iMessage/SMS/RCS detection requires an active iMessage channel; provision one or contact support to enable it.","status":409,"code":"no_active_blooio_channel"}}}}}}},"/contacts/{contactId}/timeline":{"get":{"tags":["Contacts"],"summary":"Get contact timeline","description":"A single, chronological cross-channel activity feed for a contact, unifying every conversation the contact takes part in — both 1:1 chats keyed on the contact's identities and group chats where the contact is a current member — across all channel types. After a merge, the survivor's timeline includes the absorbed contact's history automatically. Each entry is a polymorphic envelope tagged by `type`; ordered newest-first by `occurred_at`. By default only `message` entries are returned; pass `include_events=true` to interleave lifecycle and chat events.","operationId":"getContactTimeline","parameters":[{"$ref":"#/components/parameters/ContactIdParam"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"},{"name":"include_events","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"When true, interleaves non-message activity (delivery/read receipts, reactions, chat/group/typing changes) from the events ledger into the feed as `message_event` / `chat_event` entries. Defaults to false (messages only)."}],"responses":{"200":{"description":"Timeline page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TimelineItem"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/contacts/{contactId}/tags":{"get":{"tags":["Contacts"],"summary":"List contact tags","operationId":"listContactTags","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"responses":{"200":{"description":"Tags","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Contacts"],"summary":"Add contact tags","operationId":"addContactTags","parameters":[{"$ref":"#/components/parameters/ContactIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}},"tag":{"type":"string"}}}}}},"responses":{"200":{"description":"Merged tags","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/contacts/{contactId}/tags/{tag}":{"delete":{"tags":["Contacts"],"summary":"Remove a contact tag","operationId":"removeContactTag","parameters":[{"$ref":"#/components/parameters/ContactIdParam"},{"name":"tag","in":"path","required":true,"schema":{"type":"string"},"description":"The tag to remove from the contact. URL-encode values that contain spaces or special characters."}],"responses":{"200":{"description":"Remaining tags","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/groups":{"get":{"tags":["Groups"],"summary":"List groups","operationId":"listGroups","parameters":[{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of groups","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Group"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Groups"],"summary":"Create a group","description":"Creates a group chat on a group-capable channel (blooio/whatsapp) with one or more members.","operationId":"createGroup","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroupRequest"}}}},"responses":{"201":{"description":"Created group","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Group"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/groups/{groupId}":{"get":{"tags":["Groups"],"summary":"Get a group","operationId":"getGroup","parameters":[{"$ref":"#/components/parameters/GroupIdParam"}],"responses":{"200":{"description":"A group","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Group"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Groups"],"summary":"Update a group","operationId":"updateGroup","parameters":[{"$ref":"#/components/parameters/GroupIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}}}},"responses":{"200":{"description":"Updated group","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Group"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}},"delete":{"tags":["Groups"],"summary":"Delete a group","operationId":"deleteGroup","parameters":[{"$ref":"#/components/parameters/GroupIdParam"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/groups/{groupId}/members":{"get":{"tags":["Groups"],"summary":"List group members","operationId":"listGroupMembers","parameters":[{"$ref":"#/components/parameters/GroupIdParam"}],"responses":{"200":{"description":"Members","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/GroupMember"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/groups/{groupId}/icon":{"post":{"tags":["Groups"],"summary":"Set group icon","description":"Uploads a group icon. Max 10MB; image/jpeg|png|gif|webp|heic|heif. Requires the group chat to be linked on a device.","operationId":"setGroupIcon","parameters":[{"$ref":"#/components/parameters/GroupIdParam"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"icon":{"type":"string","format":"binary"}},"required":["icon"]}}}},"responses":{"200":{"description":"Icon set","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"icon_url":{"type":["string","null"]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"tags":["Groups"],"summary":"Remove group icon","operationId":"removeGroupIcon","parameters":[{"$ref":"#/components/parameters/GroupIdParam"}],"responses":{"200":{"description":"Icon removed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"removed":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/chats":{"get":{"tags":["Chats"],"summary":"List chats","description":"Lists conversations, most recent first. Filter by channel type, state, channel, or contact.","operationId":"listChats","parameters":[{"name":"type","in":"query","schema":{"$ref":"#/components/schemas/ChannelType"}},{"name":"state","in":"query","schema":{"$ref":"#/components/schemas/ChatState"}},{"name":"channel_id","in":"query","schema":{"type":"string"}},{"name":"contact_id","in":"query","schema":{"type":"string"}},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of chats","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Chat"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Chats"],"summary":"Find or create a chat","description":"Finds or creates a 1:1 chat with a recipient on a channel. Returns the chat (with `created` indicating whether it was newly created).","operationId":"createChat","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateChatRequest"}}}},"responses":{"200":{"description":"Existing chat","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCreated"}}}},"201":{"description":"Created chat","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCreated"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/chats/{chatId}":{"get":{"tags":["Chats"],"summary":"Get a chat","operationId":"getChat","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"200":{"description":"A chat","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Chat"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Chats"],"summary":"Update a chat","description":"Sets chat state (`open`/`closed`) on an chat. Chats on every other channel return 422 `state_not_writable`: `closed` reflects the customer ending an conversation, and no other protocol has an equivalent.","operationId":"updateChat","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"state":{"type":"string","enum":["open","closed"],"description":"chats only. `closed` blocks further sends to the chat with 409 `chat_closed_by_user`; an chat also closes itself when the customer ends the conversation, and reopens when they message again."}}}}}},"responses":{"200":{"description":"Updated chat","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Chat"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/chats/{chatId}/typing":{"post":{"tags":["Chats"],"summary":"Start/stop typing indicator","description":"Shows or clears a business-to-customer typing indicator. Supported on Blooio channels.","operationId":"setTyping","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"state":{"type":"string","enum":["started","stopped"],"default":"started"}}}}}},"responses":{"200":{"description":"Typing state applied","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"chat_id":{"type":"string"},"state":{"type":"string"},"at":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}},"delete":{"tags":["Chats"],"summary":"Stop typing indicator","description":"Clears the business-to-customer typing indicator. Supported on Blooio channels.","operationId":"stopTyping","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"200":{"description":"Typing stopped","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"chat_id":{"type":"string"},"state":{"type":"string"},"at":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/chats/{chatId}/read":{"post":{"tags":["Chats"],"summary":"Mark chat as read","description":"Sends a read receipt. Blooio channels only.","operationId":"markChatRead","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"200":{"description":"Marked read","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"chat_id":{"type":"string"},"read":{"type":"boolean"},"at":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/chats/{chatId}/contact-card":{"post":{"tags":["Chats"],"summary":"Share contact card","description":"Shares the sender's contact card into a chat. Blooio channels only.","operationId":"shareContactCard","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"200":{"description":"Shared","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"chat_id":{"type":"string"},"shared":{"type":"boolean"},"at":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/chats/{chatId}/polls":{"post":{"tags":["Polls"],"summary":"Send a poll","description":"Sends a native iMessage poll to a chat. Requires a title and 2+ options. Blooio channels only.","operationId":"sendPoll","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendPollRequest"}}}},"responses":{"201":{"description":"Poll queued","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Poll"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/chats/{chatId}/polls/{pollId}/vote":{"post":{"tags":["Polls"],"summary":"Vote on a poll","description":"Casts or toggles a vote by `option_index` (0-based) or `option` text. Blooio channels only.","operationId":"votePoll","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"name":"pollId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the poll, prefixed with `msg_` (a poll shares the id of its underlying message)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VotePollRequest"}}}},"responses":{"200":{"description":"All votes toggled off","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PollVoteResult"}}}}}},"201":{"description":"Vote recorded","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PollVoteResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/chats/{chatId}/polls/{pollId}":{"get":{"tags":["Polls"],"summary":"Get poll results","operationId":"getPollResults","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"name":"pollId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the poll, prefixed with `msg_` (a poll shares the id of its underlying message)."}],"responses":{"200":{"description":"Poll results","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PollResults"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/chats/{chatId}/background":{"get":{"tags":["Chats"],"summary":"Get chat background","description":"Reads the chat's background image (Blooio channels). Reads live from the device.","operationId":"getChatBackground","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"200":{"description":"Background info","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"chat_id":{"type":"string"},"has_background":{"type":"boolean"},"background_id":{"type":["string","null"]},"background_version":{"type":["integer","null"]},"background_url":{"type":["string","null"]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Chats"],"summary":"Set chat background","description":"Uploads a chat background image. Max 10MB; image/jpeg|png|gif|webp|heic|heif. Blooio channels only.","operationId":"setChatBackground","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"background":{"type":"string","format":"binary"}},"required":["background"]}}}},"responses":{"200":{"description":"Background set","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"chat_id":{"type":"string"},"background_id":{"type":"string"},"background_url":{"type":["string","null"]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Chats"],"summary":"Remove chat background","operationId":"removeChatBackground","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"200":{"description":"Background removed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"chat_id":{"type":"string"},"removed":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/chats/{chatId}/messages":{"get":{"tags":["Messages"],"summary":"List chat messages","description":"Lists messages in a chat, newest first by default. Use `order=asc` for oldest first.","operationId":"listChatMessages","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of messages","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Messages"],"summary":"Send a message to a chat","description":"Sends a message into an existing chat. Both the sender channel and recipient are inferred from the chat, so `from` and `to` must not be supplied. Returns the message object at the top level (not wrapped in `data`).","operationId":"sendMessageToChat","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendToChatRequest"}}}},"responses":{"200":{"description":"Dry-run preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageSendResult"}}}},"201":{"description":"Message accepted. The message object is returned with its current `status`; a synchronously rejected send returns `status: \"failed\"` with an `error` object (still 201, since the message was created).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageSendResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/SafetyBlocked"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/ConversationLimited"},"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/messages":{"post":{"tags":["Messages"],"summary":"Send a message","description":"The primary endpoint for starting a message. The common send is entirely scalar: `{ \"from\": \"+15551230001\", \"to\": \"+15551234567\", \"text\": \"Hello\" }`.\n\n`from` is one optional string:\n\n1. **Phone number** — `\"from\": \"+15551230001\"` selects your exact numbered channel with that address.\n2. **Alias** — `\"from\": \"support\"` selects the non-numbered channel (RCS Business) with that organization-wide alias.\n3. **Channel id** — `\"from\": \"ch_...\"` is the advanced exact-channel form.\n4. **Automatic** — omit `from` to use the key's default priority / automatic routing.\n\nAdvanced routing controls (each mutually exclusive with `from`): `\"priority_id\": \"priority_...\"` routes through an explicit priority; `\"channel_type\": \"blooio\"` constrains automatic/priority routing to one type; `\"hybrid\": true` (or a representative phone number string) requests hybrid Twilio-first / Blooio-after-reply routing.\n\nPriority routing is history-first and content-aware. Poll content currently routes only through Blooio. Existing groups require an exact sender whose channel owns the group or a hybrid sender that owns that channel. Returns the resolved scalar `from` plus the chosen `channel_id`, `chat_id`, and routing explanation.","operationId":"sendMessage","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"},"examples":{"routed_text":{"summary":"Simple routed text","value":{"to":"+15551234567","text":"Hello from Blooio!"}},"exact_number":{"summary":"Send from an exact number","value":{"from":"+18582849901","to":"+15551234567","text":"Hello!"}},"alias":{"summary":"Send from an aliased sender","value":{"from":"support","to":"opaque-customer-id","text":"How can we help?"}},"media_with_caption":{"summary":"Media with a caption","value":{"from":"+18582849901","to":"+15551234567","text":"Check this out","attachments":["https://example.com/photo.jpg"]}},"blooio_type":{"summary":"Constrain routing to Blooio","value":{"channel_type":"blooio","to":"+15551234567","text":"Hello from Blooio!"}},"poll":{"summary":"Route a Blooio poll","value":{"channel_type":"blooio","to":"+15551234567","poll":{"title":"Choose one","options":["A","B"]}}},"dry_run":{"summary":"Validate/route only","value":{"to":"+15551234567","text":"Test","dry_run":true}}}}}},"responses":{"200":{"description":"Dry-run preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageSendResult"}}}},"201":{"description":"Message accepted. The message object is returned with its current `status`; a synchronously rejected send returns `status: \"failed\"` with an `error` object (still 201, since the message was created).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageSendResult"}}}},"207":{"description":"Multi-recipient fan-out with mixed results (some recipients sent, some failed). See per-recipient items in `data`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FanOutResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/SafetyBlocked"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"description":"Validation failed, or a multi-recipient fan-out in which every recipient failed (in which case the body is a `FanOutResult` rather than an error envelope).","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/FanOutResult"}]}}}},"429":{"$ref":"#/components/responses/ConversationLimited"},"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/chats/{chatId}/messages/{messageId}":{"get":{"tags":["Messages"],"summary":"Get a message","operationId":"getChatMessage","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"$ref":"#/components/parameters/MessageIdParam"}],"responses":{"200":{"description":"A message","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Message"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/chats/{chatId}/messages/{messageId}/status":{"get":{"tags":["Messages"],"summary":"Get message status","description":"Lightweight delivery status of a message.","operationId":"getMessageStatus","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"$ref":"#/components/parameters/MessageIdParam"}],"responses":{"200":{"description":"Message status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MessageStatus"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/chats/{chatId}/messages/{messageId}/events":{"get":{"tags":["Messages"],"summary":"List message events","description":"Lifecycle events for a message (sent, delivered, read, reactions, ...).","operationId":"listMessageEvents","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"$ref":"#/components/parameters/MessageIdParam"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"Message events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MessageEvent"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/chats/{chatId}/messages/{messageId}/reactions":{"post":{"tags":["Messages"],"summary":"React to a message","description":"Adds or removes a tapback reaction on any message in the chat — inbound (received) or outbound — addressed by the message `id` from the List messages endpoint. Use a `+` prefix to add and `-` to remove (e.g. `+love`, `-love`). Returns 404 if the message isn't in the chat and 409 if it hasn't been dispatched to the device yet. Blooio channels only.","operationId":"addReaction","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"$ref":"#/components/parameters/MessageIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reaction":{"type":"string","description":"e.g. '+love' to add, '-love' to remove.","examples":["+love","+like","+laugh","+emphasize","+question","+dislike"]}},"required":["reaction"]}}}},"responses":{"200":{"description":"Reaction applied","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"message_id":{"type":"string"},"reaction":{"type":"string"},"at":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhooks","operationId":"listWebhooks","parameters":[{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of webhooks","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Webhooks"],"summary":"Create a webhook","description":"Subscribes an HTTPS endpoint to events. Returns the signing secret once, on creation.","operationId":"createWebhook","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}}},"responses":{"201":{"description":"Created webhook (includes signing_secret once)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookWithSecret"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/webhooks/{webhookId}":{"get":{"tags":["Webhooks"],"summary":"Get a webhook","operationId":"getWebhook","parameters":[{"$ref":"#/components/parameters/WebhookIdParam"}],"responses":{"200":{"description":"A webhook","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Webhook"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Webhooks"],"summary":"Update a webhook","description":"Updates url, event_types, or status (`active`/`disabled`).","operationId":"updateWebhook","parameters":[{"$ref":"#/components/parameters/WebhookIdParam"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookRequest"}}}},"responses":{"200":{"description":"Updated webhook","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Webhook"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook","operationId":"deleteWebhook","parameters":[{"$ref":"#/components/parameters/WebhookIdParam"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhooks/{webhookId}/secret/rotate":{"post":{"tags":["Webhooks"],"summary":"Rotate webhook signing secret","description":"Rotates and returns a new signing secret (once).","operationId":"rotateWebhookSecret","parameters":[{"$ref":"#/components/parameters/WebhookIdParam"}],"responses":{"200":{"description":"New secret","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"signing_secret":{"type":"string"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhooks/{webhookId}/deliveries":{"get":{"tags":["Webhooks"],"summary":"List webhook deliveries","description":"Recent delivery attempts for a webhook. Also available at `/webhooks/{webhookId}/logs`.","operationId":"listWebhookDeliveries","parameters":[{"$ref":"#/components/parameters/WebhookIdParam"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of deliveries","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhooks/{webhookId}/deliveries/{deliveryId}/replay":{"post":{"tags":["Webhooks"],"summary":"Replay a webhook delivery","description":"Re-sends a past delivery. Also available at `/webhooks/{webhookId}/logs/{deliveryId}/replay`.","operationId":"replayWebhookDelivery","parameters":[{"$ref":"#/components/parameters/WebhookIdParam"},{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the webhook delivery attempt, prefixed with `wdel_`. Returned by the List webhook deliveries endpoint."}],"responses":{"201":{"description":"Replay queued","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"replay_of":{"type":"string"},"status":{"type":"string"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/events":{"get":{"tags":["Events"],"summary":"List events","description":"Unified activity feed (message lifecycle, polls, groups, reactions, typing, `safety.state_changed`, `safety.number_banned`). Filter by type, chat, or message.","operationId":"listEvents","parameters":[{"name":"type","in":"query","schema":{"type":"string"},"description":"Event type or wildcard, e.g. `message.received` or `message.*`."},{"name":"chat_id","in":"query","schema":{"type":"string"}},{"name":"message_id","in":"query","schema":{"type":"string"}},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"A page of events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/events/{eventId}":{"get":{"tags":["Events"],"summary":"Get an event","operationId":"getEvent","parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the event, prefixed with `evt_`. Returned by the List events endpoint."}],"responses":{"200":{"description":"An event","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Event"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/phone-numbers/lookup":{"get":{"tags":["Phone Numbers"],"summary":"Look up a phone number","description":"Returns parsed info / geocoding for a phone number. Requires an enterprise plan.","operationId":"lookupPhoneNumber","parameters":[{"name":"number","in":"query","required":true,"schema":{"type":"string"},"description":"Phone number in E.164."}],"responses":{"200":{"description":"Lookup result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Phone Numbers"],"summary":"Look up a phone number (POST)","description":"Same as the GET form, accepting the number in a JSON body. Returns parsed info / geocoding for a phone number. Requires an enterprise plan.","operationId":"lookupPhoneNumberPost","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","description":"Phone number in E.164."}},"required":["number"]}}}},"responses":{"200":{"description":"Lookup result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/phone-numbers/batch":{"post":{"tags":["Phone Numbers"],"summary":"Batch phone number lookup","description":"Looks up up to 100 phone numbers. Requires an enterprise plan.","operationId":"batchLookupPhoneNumbers","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"numbers":{"type":"array","items":{"type":"string"},"maxItems":100}},"required":["numbers"]}}}},"responses":{"200":{"description":"Lookup results","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/chats/{chatId}/participants":{"get":{"tags":["Chats"],"summary":"List chat participants","operationId":"listChatParticipants","x-blooio-status":"planned","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}},"post":{"tags":["Chats"],"summary":"Add a chat participant","operationId":"addChatParticipant","x-blooio-status":"planned","parameters":[{"$ref":"#/components/parameters/ChatIdParam"}],"responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/chats/{chatId}/participants/{identityId}":{"delete":{"tags":["Chats"],"summary":"Remove a chat participant","operationId":"removeChatParticipant","x-blooio-status":"planned","parameters":[{"$ref":"#/components/parameters/ChatIdParam"},{"name":"identityId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the contact identity, prefixed with `cid_`. Returned by the List contact identities endpoint."}],"responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/attachments":{"post":{"tags":["Messages"],"summary":"Register an attachment","operationId":"createAttachment","x-blooio-status":"planned","responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/attachments/{attachmentId}":{"get":{"tags":["Messages"],"summary":"Get an attachment","operationId":"getAttachment","x-blooio-status":"planned","parameters":[{"name":"attachmentId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the attachment, as referenced on a message's content."}],"responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/templates":{"get":{"tags":["Messages"],"summary":"List templates","operationId":"listTemplates","x-blooio-status":"planned","responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}},"post":{"tags":["Messages"],"summary":"Create a template","operationId":"createTemplate","x-blooio-status":"planned","responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/templates/{templateId}":{"get":{"tags":["Messages"],"summary":"Get a template","operationId":"getTemplate","x-blooio-status":"planned","parameters":[{"name":"templateId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the message template. Returned when you create a template or by the List templates endpoint."}],"responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}},"delete":{"tags":["Messages"],"summary":"Delete a template","operationId":"deleteTemplate","x-blooio-status":"planned","parameters":[{"name":"templateId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the message template. Returned when you create a template or by the List templates endpoint."}],"responses":{"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/api-keys":{"get":{"tags":["API Keys"],"summary":"List API keys","description":"Lists the installed organization's API keys with their metadata (name, type, expiry, blocked/deprecated state). The raw key values are returned so the app can reuse them.\n\n**Blooio Apps (OAuth) only.** Requires the `apikeys:read` scope. API-key and dashboard authentication are rejected with `403 oauth_token_required`.","operationId":"listApiKeys","responses":{"200":{"description":"The organization's API keys.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["API Keys"],"summary":"Create an API key","description":"Mints a new API key for the installed organization and returns it. This is the **only** way to create an API key programmatically — the raw key is returned once in this response; store it securely.\n\nA new key owns no numbers until you assign channels to it (see Assign a channel to an API key) and, by default, has no explicit expiry (`valid_until: -1`). Organizations are capped at a fixed number of active keys; exceeding it returns `409 api_key_limit_reached`.\n\n**Blooio Apps (OAuth) only.** Requires the `apikeys:manage` scope. API-key and dashboard authentication are rejected with `403 oauth_token_required`.","operationId":"createApiKey","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}}},"responses":{"201":{"description":"The created API key. The `api_key` value is shown only in this response.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKey"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api-keys/{apiKey}":{"get":{"tags":["API Keys"],"summary":"Get an API key","description":"Fetches a single API key's metadata by its value.\n\n**Blooio Apps (OAuth) only.** Requires the `apikeys:read` scope. API-key and dashboard authentication are rejected with `403 oauth_token_required`.","operationId":"getApiKey","parameters":[{"$ref":"#/components/parameters/ApiKeyParam"}],"responses":{"200":{"description":"The API key metadata.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKey"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["API Keys"],"summary":"Deprecate an API key","description":"Deprecates (revokes) an API key. The key stops authenticating immediately and its number/channel ownership is released. Deprecation is idempotent and cannot be undone via the API.\n\n**Blooio Apps (OAuth) only.** Requires the `apikeys:manage` scope. API-key and dashboard authentication are rejected with `403 oauth_token_required`.","operationId":"deprecateApiKey","parameters":[{"$ref":"#/components/parameters/ApiKeyParam"}],"responses":{"200":{"description":"The key was deprecated.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"api_key":{"type":"string"},"deprecated":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api-keys/{apiKey}/channels":{"get":{"tags":["API Keys"],"summary":"List an API key's channels","description":"Lists the numbered senders (Blooio and Twilio) the API key currently owns — the same lines the send layer will consider when a request authenticates with this key.\n\n**Blooio Apps (OAuth) only.** Requires the `apikeys:read` scope. API-key and dashboard authentication are rejected with `403 oauth_token_required`.","operationId":"listApiKeyChannels","parameters":[{"$ref":"#/components/parameters/ApiKeyParam"}],"responses":{"200":{"description":"The lines owned by this key.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"The owned line in E.164, e.g. `+15551234567`. Usable as the `{channel}` reference on the assign/unassign endpoints and as `from` when sending."},"type":{"type":"string","description":"Provider of the line (`blooio` or `twilio`)."},"banned":{"type":"boolean"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api-keys/{apiKey}/channels/{channel}":{"put":{"tags":["API Keys"],"summary":"Assign a channel to an API key","description":"Makes the API key the owner of a channel's line, so messages sent with that key can originate from it. This is the programmatic equivalent of assigning a number to a key on the dashboard **Channels** board.\n\nThe key becomes the number's sole owner: if another API key or integration owned it, that ownership is deprecated (and its hybrid mode re-evaluated). Assigning is idempotent — re-assigning a line the key already owns returns `unchanged: true`.\n\nOnly numbered, key-ownable channels can be assigned. Blooio lines are the primary case (Twilio where applicable); non-numbered channel types return `422 channel_not_assignable`. On plans without number pools, a key may own at most one line (`400`).\n\n**Blooio Apps (OAuth) only.** Requires the `apikeys:manage` scope. API-key and dashboard authentication are rejected with `403 oauth_token_required`.","operationId":"assignChannelToApiKey","parameters":[{"$ref":"#/components/parameters/ApiKeyParam"},{"$ref":"#/components/parameters/ChannelIdParam"}],"responses":{"200":{"description":"The channel is now owned by the key.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyChannelAssignment"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}},"delete":{"tags":["API Keys"],"summary":"Unassign a channel from an API key","description":"Removes the API key's ownership of a channel's line. The key can no longer send from it. Hybrid mode is re-evaluated for the key.\n\n**Blooio Apps (OAuth) only.** Requires the `apikeys:manage` scope. API-key and dashboard authentication are rejected with `403 oauth_token_required`.","operationId":"unassignChannelFromApiKey","parameters":[{"$ref":"#/components/parameters/ApiKeyParam"},{"$ref":"#/components/parameters/ChannelIdParam"}],"responses":{"200":{"description":"The channel is no longer owned by the key.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyChannelAssignment"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/ai/bloobability/assess":{"post":{"tags":["AI"],"summary":"Assess a conversation against policies","description":"Answers one question, per policy: **how confident is the model that this policy applies to this conversation?** The number you want is `policies[].calibrated_probability`.\n\n## Two passes, only the gauge is required\n- The **gauge** scores **every policy you send, independently** (a yes/no plus a decision margin read from the served logprobs), and a frozen two-tier calibration map turns that margin into `calibrated_probability`, `verdict`, `calibration_tier`, and `accepted`. This is the whole product and the common, fastest path.\n- The **matcher** is a separate, optional pass. Set `include_evidence: true` and it runs **in parallel** with the gauge and attaches `evidence_ids` (the supporting message IDs). It adds roughly no wall-clock latency but is a second full inference, so leave it off unless you need provenance.\n\nThe call is **stateless**: nothing is persisted, `reference_id` is not an idempotency key, and replaying the same request re-runs inference. Policies come back in the **same order you sent them** — `policies[i]` in the response corresponds to `policies[i]` in the request.\n\n## Limitations and correct use\n- **Domain.** Tuned for customer-support conversations in English. Other languages, other conversation genres, and non-conversational text are unmeasured.\n- **Estimating a rate.** To measure how often a policy applies across many conversations, **average `calibrated_probability` over the cohort** — do not count `yes` verdicts. The model deliberately favors precision over recall, so verdict counts undercount the true rate; the calibrated probabilities do not.\n- **Tracking a trend.** Reported movement is a conservative floor — magnitude is compressed, so the *direction* of change is more reliable than its *size*. Compare periods using the **same policy `id` and `description`** throughout; changing a definition (or refitting per period) mid-series can invert the trend.\n- **Unseen policies (`pooled_unseen`).** Probabilities are usable for your own policies, but there is no validated `accepted` decision and trend fidelity is unmeasured — keep a human in the loop.\n- **Automation vs. review.** `accepted` is intentionally strict; plan for a meaningful share of decisions (roughly a third to two fifths) to fall outside it, and route those to human review — see `evidence_ids`.\n- **Labels are model-derived.** Scores reflect a model's judgment of your policy description, not human-verified ground truth. See the [Conversation insights guide](https://docs.blooio.com/guides/conversation-insights) for the full method.","operationId":"assessBloobability","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BloobabilityAssessRequest"},"example":{"reference_id":"order-42","conversation":[{"id":"m1","role":"customer","content":"Any chance of a refund?"},{"id":"m2","role":"agent","content":"Let me check your order."}],"policies":[{"id":"refund_request","description":"The customer asks for money back."}],"include_evidence":false}}}},"responses":{"200":{"description":"Per-policy assessment, in the same order as the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BloobabilityAssessResponse"},"example":{"reference_id":"order-42","policies":[{"id":"refund_request","calibrated_probability":0.977,"verdict":"yes","calibration_tier":"roster","accepted":true}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"502":{"description":"The gauge endpoint returned a response whose yes/no logprobs could not be read, so no calibrated number can be produced. This indicates a serving misconfiguration rather than a problem with your request; retry, and contact support if it persists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","description":"Blooio API key as a bearer token, e.g. `Authorization: Bearer bl_live_...`."}},"parameters":{"LimitParam":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Maximum number of items to return per page. Must be between 1 and 200; defaults to 50. Combine with `cursor` to paginate."},"CursorParam":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque pagination cursor. Pass the `next_cursor` value from a previous response to fetch the next page; omit it on the first request."},"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional key for safe retries. Replaying the same key with an identical body returns the original result; a different body returns 409. Use a unique value (such as a UUID) per logical request."},"ChannelIdParam":{"name":"channel","in":"path","required":true,"schema":{"type":"string"},"description":"The channel reference — the same three forms `from` accepts: a phone number (URL-encode `+` as `%2B`), a channel alias, or the exact channel id (`ch_...`)."},"ContactIdParam":{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the contact, prefixed with `ct_`. Returned when you create a contact or by the List contacts endpoint."},"ChatIdParam":{"name":"chatId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the chat, prefixed with `chat_`. Returned by the List chats endpoint."},"PriorityIdParam":{"name":"priorityId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the priority, prefixed with `priority_`. Returned when you create a priority or by the List priorities endpoint."},"MessageIdParam":{"name":"messageId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the message — the `id` returned when you send a message or by the List messages endpoint. Outbound messages use a `msg_` id; inbound (received) messages use the id the List endpoint exposes for that row. Both are addressable."},"GroupIdParam":{"name":"groupId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the group, prefixed with `grp_`. Returned by the List groups endpoint."},"WebhookIdParam":{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"},"description":"Unique identifier of the webhook subscription, prefixed with `wh_`. Returned when you create a webhook or by the List webhooks endpoint."},"NumberParam":{"name":"number","in":"path","required":true,"schema":{"type":"string"},"description":"Sender phone number in E.164 format, e.g. `+15551234567`."},"ApiKeyParam":{"name":"apiKey","in":"path","required":true,"schema":{"type":"string"},"description":"The API key value (prefixed with `api_`), as returned when the key was created or by the List API keys endpoint."}},"responses":{"Unauthorized":{"description":"Authentication failed or missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"Forbidden":{"description":"Authenticated but not allowed (e.g. blocked key or plan gate).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"BadRequest":{"description":"Malformed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"Conflicting state (e.g. chat closed, idempotency reuse, channel not active).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UnprocessableEntity":{"description":"Validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotImplemented":{"description":"Endpoint is defined but not implemented yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_implemented","message":"This v4 endpoint is defined but not implemented yet.","details":{"resource":"templates.list"}}}}}},"SafetyBlocked":{"description":"Blocked by messaging safety. The error `code` identifies the gate: `safety_account_review` (the sending number is blocked pending review), `safety_reply_only` (outbound is restricted to conversations with prior inbound), `safety_new_conversations_paused` (brand-new conversations are temporarily paused; replies are unaffected), or `conversation_content_restricted` (links, media, and attachments are not allowed before the recipient's first written reply, or in a re-engagement of an inactive conversation — an emoji reaction does not unlock them). Safety actions clear automatically as the underlying pattern ages out; current per-number state is delivered via the `safety.state_changed` webhook event.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"conversation_content_restricted","message":"Links, media, and attachments are not allowed until the recipient replies. Send plain text first."}}}}},"ConversationLimited":{"description":"Conversation-state limit reached. The error `code` identifies the rule: `conversation_awaiting_reply` (up to 3 messages may be sent before the recipient responds at all), `conversation_streak_limit` (consecutive-message cap since their last response), or `conversation_inactive_paused` (no response in 14+ days and the single re-engagement message was already sent). Every response body carries `conversation_state` — one of `new`, `acknowledged` (reacted but never wrote back), `active`, `established`, `trusted`, or `inactive` — and the streak errors also carry `current_streak` and `limit`, so you can see which rung of the ladder a thread is on.\n\nThe cap rises with the thread's written history, and an **emoji reaction counts as a response**: a tapback clears the consecutive-message counter and keeps a thread from going inactive, so the limit lifts the moment the recipient reacts. A reaction is not treated as a reply, though — only writing back raises the cap and unlocks links, media, and attachments (see `conversation_content_restricted`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"conversation_awaiting_reply","message":"This conversation is awaiting a reply. Up to 3 messages may be sent before the recipient responds."}}}}}},"schemas":{"Error":{"type":"object","description":"Standard v4 error envelope returned by route handlers.","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true}},"required":["code","message"]}},"required":["error"]},"AuthError":{"type":"object","description":"Flat error shape returned by the authentication middleware (401/403).","properties":{"error":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"},"code":{"type":"string"}}},"ItemEnvelope":{"type":"object","description":"Generic single-item envelope.","properties":{"data":{"type":"object","additionalProperties":true}}},"ChannelType":{"type":"string","enum":["blooio","twilio","whatsapp","whatsapp_business","rcs_business"]},"ChannelStatus":{"type":"string","enum":["pending_verification","active","suspended","banned","deprecated"]},"ChatState":{"type":"string","enum":["open","closed","blocked","deprecated"]},"Direction":{"type":"string","enum":["inbound","outbound"]},"ChannelCapabilities":{"type":"object","description":"What a channel can do. Shape varies by channel type.","properties":{"protocols":{"type":"array","items":{"type":"string"}},"content":{"type":"array","items":{"type":"string"}},"actions":{"type":"array","items":{"type":"string"}},"interactive":{"type":"array","items":{"type":"string"}},"gates":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"Channel":{"type":"object","properties":{"id":{"type":"string","description":"Channel id (ch_...)."},"type":{"$ref":"#/components/schemas/ChannelType"},"address":{"type":["string","null"],"description":"The channel's real address: the phone number for numbered channels, or the provider address for non-numbered ones. Usable directly as `from`."},"alias":{"type":"string","description":"Movable, organization-wide name for a non-numbered channel (currently RCS Business). Usable directly as `from`. Omitted for numbered channels."},"status":{"$ref":"#/components/schemas/ChannelStatus"},"capabilities":{"$ref":"#/components/schemas/ChannelCapabilities"},"profile":{"$ref":"#/components/schemas/ChannelProfile"},"settings":{"type":"object","additionalProperties":true,"description":"Inline per-channel settings for channel types that support them (currently Blooio numbers). Each setting value plus a `can_<key>` flag for whether this channel may enable it. Omitted for channel types without settings. Full read/write via `GET`/`PATCH /channels/{channel}/settings`.","example":{"auto_mark_read":false,"auto_share_contact":false,"can_auto_mark_read":true,"can_auto_share_contact":true}},"created_at":{"type":"integer","description":"Epoch milliseconds."}}},"ChannelSettings":{"type":"object","description":"Per-channel settings envelope. `settings` and `available` shapes vary by channel type; the Blooio (P2P numbers) provider exposes `auto_mark_read` and `auto_share_contact`. `available[key]` is whether THIS channel may turn `key` on (type-gated).","properties":{"channel_id":{"type":"string","description":"Channel id (ch_...)."},"type":{"$ref":"#/components/schemas/ChannelType"},"settings":{"type":"object","additionalProperties":true,"description":"Current setting values for this channel (keys are per channel type).","example":{"auto_mark_read":false,"auto_share_contact":false}},"available":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Whether this channel may turn each setting on (type-gated).","example":{"auto_mark_read":true,"auto_share_contact":true}}}},"ChannelSettingsUpdate":{"type":"object","additionalProperties":true,"description":"Partial update of a channel's settings; keys are per channel type and validated by the channel's provider. Blooio (P2P numbers) accepts `auto_mark_read` and `auto_share_contact` and rejects any other key (422 invalid_settings).","properties":{"auto_mark_read":{"type":"boolean","description":"Blooio: mark a chat read on the device as soon as a message arrives. Dedicated + inbound lines only."},"auto_share_contact":{"type":"boolean","description":"Blooio: share the number's Name & Photo card with people who message it. Dedicated lines only, and the line must have a contact card set up."}}},"ChannelProfile":{"type":"object","additionalProperties":false,"description":"Stored channel profile. Omitted on GET when no fields are set. Null or empty string on PUT clears a field.","properties":{"display_name":{"type":["string","null"]},"about":{"type":["string","null"]},"email":{"type":["string","null"]},"avatar":{"type":["string","null"]}}},"ContactIdentity":{"type":"object","properties":{"id":{"type":"string","description":"Identity id (cid_...)."},"contact_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"identifier":{"type":"string","description":"Phone (E.164) or email."},"channel_id":{"type":["string","null"],"description":"Reserved for future use."},"created_at":{"type":"integer"}}},"Contact":{"type":"object","properties":{"id":{"type":"string","description":"Contact id (ct_...)."},"name":{"type":["string","null"]},"created_at":{"type":"integer"},"tags":{"type":"array","items":{"type":"string"}},"last_message_time":{"type":["integer","null"]},"last_direction":{"type":["string","null"],"enum":["inbound","outbound",null]},"last_text":{"type":["string","null"]},"last_status":{"type":["string","null"]},"identities":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"identifier":{"type":"string"},"channel_id":{"type":["string","null"]},"created_at":{"type":"integer"}}}}}},"TimelineItem":{"type":"object","description":"A single polymorphic entry in a contact's cross-channel timeline. The `type` discriminates the shape of `object`.","properties":{"type":{"type":"string","enum":["message","message_event","chat_event"],"description":"`message`: a message the contact sent or received. `message_event`: a lifecycle event for a message (sent/delivered/read/failed/reaction). `chat_event`: any other chat/group/typing event. Event entries only appear when `include_events=true`."},"occurred_at":{"type":"integer","description":"Epoch-millisecond timestamp the entry occurred at; the feed is ordered by this descending."},"id":{"type":"string","description":"The underlying object id — a `msg_` message id for `message` entries, an `evt_` event id otherwise. Also the pagination sort key within a given `occurred_at`."},"chat_id":{"type":["string","null"]},"channel_id":{"type":["string","null"]},"channel_type":{"type":["string","null"],"description":"The channel type this entry occurred on. Always set for `message` entries; may be null for channel-agnostic events."},"object":{"type":"object","additionalProperties":true,"description":"The full entry payload. For `message` entries: id, chat_id, channel_id, channel_type, direction, type, text, status, created_at. For event entries: id, type, chat_id, channel_id, channel_type, data, occurred_at."}}},"Chat":{"type":"object","properties":{"id":{"type":"string","description":"Chat id (chat_...)."},"channel_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"contact_id":{"type":["string","null"]},"identity_id":{"type":["string","null"]},"group_id":{"type":["string","null"]},"state":{"$ref":"#/components/schemas/ChatState"},"capabilities":{"type":"object","additionalProperties":true},"window_expires_at":{"type":["integer","null"]},"last_message_at":{"type":["integer","null"]},"created_at":{"type":"integer"}}},"ChatCreated":{"type":"object","properties":{"id":{"type":"string"},"channel_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"contact_id":{"type":["string","null"]},"identity_id":{"type":["string","null"]},"state":{"$ref":"#/components/schemas/ChatState"},"created":{"type":"boolean","description":"True if the chat was newly created."}}},"Message":{"type":"object","properties":{"id":{"type":"string","description":"Message id (msg_...)."},"chat_id":{"type":"string"},"channel_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"channel_address":{"type":["string","null"],"description":"The address of your own line on this channel (the sending/receiving number or alias, e.g. `+15559876543`). Same value regardless of direction; `sender`/`recipient` tell you which side of the conversation it is on."},"protocol":{"type":"string","enum":["pending","unknown","imessage","sms","rcs","whatsapp","whatsapp_business"],"description":"Transport state or resolved wire service; never null. `pending` = accepted and dispatched, wire service not resolved yet; `imessage` / `rcs` / `sms` = the device-confirmed transport (iMessage blue bubble, RCS, or SMS/MMS green bubble); `whatsapp` = sent over P2P WhatsApp; `whatsapp_business` = sent over the WhatsApp Business Platform (WABA); `unknown` = accepted but the transport could not be resolved before the tracking window closed (see `error`)."},"direction":{"$ref":"#/components/schemas/Direction"},"sender":{"type":["string","null"],"description":"Who the message is FROM, direction-aware. On `inbound` this is the other party (the contact who messaged you, or in a group the participant who wrote it); on `outbound` this is your own line (`channel_address`). This is the SAME field delivered on the message webhooks, so a message reads identically via REST or webhook."},"recipient":{"type":["string","null"],"description":"Who the message is TO, direction-aware. On `inbound` this is your own line (`channel_address`); on `outbound` this is the other party. The mirror of `sender`. Matches the message webhook shape."},"contact":{"type":["object","null"],"description":"The other party in the conversation (never your own line), regardless of direction — so you always know who the message is with. `null` for channel-originated group outbound messages. Matches the message webhook shape.","properties":{"identifier":{"type":"string","description":"The contact's identifier (phone number, email, or handle)."}},"additionalProperties":true},"type":{"type":"string","description":"Content type, e.g. text, media, poll."},"text":{"type":["string","null"],"description":"The plain text of the message — what the recipient actually sees. On a rich-text message the styling delimiters are NOT included here; see `formatted_text`."},"formatted_text":{"type":"string","description":"Markdown for a rich-text (bold/italic/underline/strikethrough) message. Omitted entirely when the message carries no styling, so its presence is how you detect rich text.\n\nPresent in both directions: on an outbound send made with `format: \"markdown\"`, and on an inbound iMessage whose sender styled their text — so styling a customer applied in Messages arrives here even though your integration never asked for it.\n\nAlways a normalized re-serialization of the message's actual styling rather than an echo of the source string: bold is spelled `**`, italic `*`, underline `++`, strikethrough `~~`, and any character that would otherwise read as a delimiter is backslash-escaped. Re-sending this value verbatim with `format: \"markdown\"` reproduces the same styled message. Blooio iMessage only. This is the SAME field delivered on the message webhooks, so a message reads identically via REST or webhook."},"status":{"type":"string","description":"Delivery lifecycle state: `queued` (accepted, awaiting dispatch), `sent` (handed to the channel; protocol resolves around here), `delivered` (delivery receipt received), `read` (read receipt received), `failed` (see `error`), `superseded` (row replaced by an idempotency retry or a poll-vote supersession). Inbound messages surface via the event feed / webhooks with `received`."},"reply_to_message_id":{"type":["string","null"]},"error":{"type":["object","null"],"additionalProperties":true},"attachments":{"type":"array","description":"Ordered attachments on the message. Each entry is retrievable directly: `url` (servable, decrypted URL — inbound Apple/ media is downloaded, decrypted, and re-hosted before this is populated), `media_type` (content type), `size` (bytes), and an optional `caption`. Always present (empty array when there are none). Matches the webhook `attachments` shape exactly.","items":{"type":"object","properties":{"url":{"type":["string","null"]},"media_type":{"type":["string","null"]},"size":{"type":["integer","null"]},"caption":{"type":["string","null"]}}}},"interactive":{"type":"object","description":"Structured interactive content, present on interactive messages. For an inbound reply this is the customer's actual selection (`role: reply`): `kind` (quick_reply/list_picker/time_picker/form/…), `chosen` (the selected option titles / time slots / flattened answers) and, for Form Messages, a structured `responses` array (per-page question + typed answers per §17.4) — not just a display string, and never null on an interactive reply. For an outbound send it is the prompt that was sent (`role: prompt`). The message's `text` remains a deterministic flattened convenience summary alongside this structure. This is the SAME object delivered on the `message.received` webhook, so a message reads identically via REST or webhook.","additionalProperties":true},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}}},"MessageStatus":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","description":"Delivery lifecycle state: `queued` (accepted, awaiting dispatch), `sent` (handed to the channel; protocol resolves around here), `delivered` (delivery receipt received), `read` (read receipt received), `failed` (see `error`), `superseded` (row replaced by an idempotency retry or a poll-vote supersession). Inbound messages surface via the event feed / webhooks with `received`."},"protocol":{"type":"string","enum":["pending","unknown","imessage","sms","rcs","whatsapp","whatsapp_business"],"description":"Transport state or resolved wire service; never null. `pending` = accepted and dispatched, wire service not resolved yet; `imessage` / `rcs` / `sms` = the device-confirmed transport (iMessage blue bubble, RCS, or SMS/MMS green bubble); `whatsapp` = sent over P2P WhatsApp; `whatsapp_business` = sent over the WhatsApp Business Platform (WABA); `unknown` = accepted but the transport could not be resolved before the tracking window closed (see `error`)."},"error":{"type":["object","null"],"additionalProperties":true},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}}},"MessageEvent":{"type":"object","properties":{"id":{"type":"string","description":"Event id (evt_...)."},"message_id":{"type":"string"},"kind":{"type":"string","description":"Bare lifecycle kind, e.g. sent, delivered, read, reaction."},"occurred_at":{"type":"integer"},"metadata":{"type":"object","additionalProperties":true}}},"MessageSendResult":{"type":"object","description":"Returned at the top level (not wrapped in `data`) by send endpoints.","properties":{"id":{"type":"string"},"chat_id":{"type":"string"},"channel_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"protocol":{"type":"string","enum":["pending","unknown","imessage","sms","rcs","whatsapp","whatsapp_business"],"description":"Transport state or resolved wire service; never null. `pending` = accepted and dispatched, wire service not resolved yet; `imessage` / `rcs` / `sms` = the device-confirmed transport (iMessage blue bubble, RCS, or SMS/MMS green bubble); `whatsapp` = sent over P2P WhatsApp; `whatsapp_business` = sent over the WhatsApp Business Platform (WABA); `unknown` = accepted but the transport could not be resolved before the tracking window closed (see `error`)."},"from":{"type":["string","null"],"description":"The resolved sender as a human value: the sending channel's phone number (numbered channels) or alias (non-numbered channels). This is the primary sender reference; `channel_id` remains available for exact follow-up and audit."},"direction":{"$ref":"#/components/schemas/Direction"},"type":{"type":"string"},"status":{"type":"string","description":"Initial lifecycle state for the send: normally `queued` (accepted for delivery), or `failed` if rejected before dispatch. Later transitions (`sent` → `delivered` / `read`, or `failed`) are reported via the message status endpoint and `message.*` webhooks."},"group_id":{"type":"string","description":"Existing group id for a group-targeted send."},"hybrid":{"type":"object","description":"Hybrid routing phase and optional transition placement.","additionalProperties":true},"error":{"type":["object","null"],"additionalProperties":true},"fallback":{"type":"object","description":"Present when a send could not be delivered on the requested channel and a different channel should be tried.","properties":{"recommended":{"type":"boolean","description":"True when falling back to another channel is recommended."},"reason":{"type":"string"}}},"to":{"type":"string","description":"Recipient identifier. Present only on per-recipient items inside a fan-out (FanOutResult) response."},"dry_run":{"type":"boolean"},"would_send":{"type":"boolean"},"preview":{"type":"object","additionalProperties":true},"poll":{"type":"object","properties":{"title":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}}},"routing":{"$ref":"#/components/schemas/RoutingMetadata"}}},"RoutingMetadata":{"type":"object","description":"How Blooio selected the concrete sender channel.","properties":{"mode":{"type":"string","enum":["explicit","priority","hybrid"]},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"number":{"type":"string"},"alias":{"type":"string"},"priority_id":{"type":"string"},"priority":{"type":"integer"}}},"FanOutResult":{"type":"object","description":"Returned when a multi-recipient send fans out to individual sends.","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MessageSendResult"}},"fan_out":{"type":"boolean"},"sent":{"type":"integer"},"failed":{"type":"integer"},"dry_run":{"type":"boolean"},"routing":{"$ref":"#/components/schemas/RoutingMetadata"}}},"Group":{"type":"object","properties":{"id":{"type":"string","description":"Group id (grp_...)."},"channel_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"name":{"type":["string","null"]},"icon_url":{"type":["string","null"]},"chat_id":{"type":["string","null"]},"chat_linked":{"type":"boolean","description":"Whether the group is linked to a device chat."},"created_at":{"type":"integer"},"members":{"type":"array","items":{"$ref":"#/components/schemas/GroupMember"}}}},"GroupMember":{"type":"object","properties":{"identity_id":{"type":"string"},"contact_id":{"type":["string","null"]},"role":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"identifier":{"type":"string"}}},"Webhook":{"type":"object","properties":{"id":{"type":"string","description":"Webhook id (wh_...)."},"url":{"type":"string","format":"uri"},"event_types":{"type":"array","items":{"type":"string"},"description":"The events this webhook receives. `[\"*\"]` for every webhook created since event filtering was retired; subscriptions registered before that keep their original list, which is frozen and cannot be changed."},"status":{"type":"string","enum":["active","disabled"]},"scope":{"type":"string","enum":["organization","api_key","integration"],"description":"Which lines this subscription receives events for. `organization` covers every line; the other two are limited to the lines that owner holds."},"api_key":{"type":["string","null"],"description":"The owning API key when `scope` is `api_key`, otherwise null."},"integration_id":{"type":["string","null"],"description":"The owning integration when `scope` is `integration`, otherwise null."},"channel_id":{"type":["string","null"]},"channel_type":{"type":["string","null"]},"created_at":{"type":"integer"}}},"WebhookWithSecret":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","properties":{"signing_secret":{"type":"string","description":"Returned once, on creation. Store it to verify signatures."}}}]},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","description":"Delivery id (wdel_...)."},"webhook_id":{"type":"string"},"event_id":{"type":"string"},"event_type":{"type":"string"},"status":{"type":"string"},"attempt_count":{"type":"integer"},"response_status":{"type":["integer","null"]},"last_attempt_at":{"type":["integer","null"]},"next_attempt_at":{"type":["integer","null"]},"created_at":{"type":"integer"}}},"Event":{"type":"object","properties":{"id":{"type":"string","description":"Event id (evt_...)."},"type":{"type":"string","description":"e.g. message.delivered, poll.voted."},"message_id":{"type":["string","null"]},"chat_id":{"type":["string","null"]},"channel_type":{"type":["string","null"]},"occurred_at":{"type":"integer"},"data":{"type":"object","additionalProperties":true}}},"Poll":{"type":"object","properties":{"id":{"type":"string"},"chat_id":{"type":"string"},"channel_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"type":{"type":"string","enum":["poll"]},"status":{"type":"string"},"poll":{"type":"object","properties":{"title":{"type":"string"},"options":{"type":"array","items":{"type":"string"}}}}}},"PollVoteResult":{"type":"object","properties":{"id":{"type":"string"},"poll_id":{"type":"string"},"voted_option":{"type":"string"},"toggled":{"type":"string","enum":["on","off"]},"active_votes":{"type":"array","items":{"type":"string"}},"active_vote_indices":{"type":"array","items":{"type":"integer"}}}},"PollResults":{"type":"object","properties":{"poll_id":{"type":"string"},"chat_id":{"type":"string"},"title":{"type":"string"},"options":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"votes":{"type":"integer"}}}},"total_votes":{"type":"integer"}}},"LinkPreview":{"type":"object","properties":{"imageUrl":{"type":"string","format":"uri"},"title":{"type":"string"}}},"MessageContentFields":{"type":"object","description":"Top-level message content. Provide exactly one content field (`text` and `attachments` may be combined); the content type is inferred from the field, so no separate `type` discriminator exists. Conflicting content fields are rejected with `422 invalid_content`.\n\nUnrecognised top-level request fields (typos, unsupported fields) are rejected by default with `422 unknown_fields` naming the offending field(s), so mistakes are caught immediately instead of being silently dropped. To opt out and have unknown fields logged-and-ignored instead, send the header `X-Blooio-Strict-Fields: false` (also accepts `0`, `off`, `no`, `lenient`, `warn`).","properties":{"text":{"type":"string","description":"Plain text message. May be combined with `attachments` (sent as media with a caption) — except a lone audio file, which is sent as a standalone voice memo and cannot carry a caption. Set `format: \"markdown\"` to interpret Markdown styling in this field (and in `parts[].text`)."},"format":{"type":"string","enum":["plain","markdown"],"default":"plain","description":"How to interpret `text` (and each `parts[].text`). Defaults to `plain`, which sends the string exactly as given.\n\nWith `markdown`, four constructs are parsed and delivered as real iMessage rich text — the recipient sees styled text, not delimiters:\n\n| Construct | Syntax |\n| --- | --- |\n| Bold | `**bold**` or `__bold__` |\n| Italic | `*italic*` or `_italic_` |\n| Underline | `++underline++` |\n| Strikethrough | `~~strike~~` |\n\nThey nest freely (`**bold and _italic_**`). Everything else Markdown can express — headings, lists, links, code spans, blockquotes, images — is NOT styling iMessage can carry, so it is passed through as literal characters: `[Blooio](https://blooio.com)` is delivered with its brackets and URL intact, and `# Heading` keeps its `#`. Escape a delimiter with a backslash (`\\*not italic\\*`) to send it literally.\n\nThe styling travels in the message's attributed body, so the stored `text` and the `text` returned on reads and webhooks is always the plain string the recipient sees, with the delimiters removed. The Markdown itself comes back as `formatted_text`, re-serialized into a normalized spelling rather than echoed verbatim (`__bold__` returns as `**bold**`).\n\nOnly valid on Blooio iMessage channels — `422 format_unsupported_for_channel_type` on any other channel type, since no other channel type has a rich-text equivalent and would otherwise deliver your delimiters as literal text. Rich text also requires the message to be delivered over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain text (the `text` string), because SMS cannot carry styling.\n\nRejected with `422 invalid_content` when set without `text` or `parts`, when the Markdown source exceeds 20000 characters, or when it compiles to more than 256 distinct formatting ranges."},"attachments":{"type":"array","items":{"type":"string","format":"uri"},"description":"Public https media URLs. May be combined with `text`. On Blooio iMessage, two or more image/video URLs are grouped into one photo/video carousel/collage bubble by default; set `carousel: false` to send them as separate bubbles.\n\n**Voice memos:** a single audio file (`.mp3`, `.m4a`, `.wav`, `.aac`, `.opus`, `.ogg`) is automatically sent as a voice memo (the native waveform/scrubber bubble), not a plain audio-file attachment — no extra field is needed. A voice memo is a standalone bubble, so it cannot be combined with `text` or any other attachment; do that and the send is rejected with `422 invalid_content`. To send audio with a message, send the voice memo and the text as two separate requests.","minItems":1},"rich_link":{"type":"object","description":"A tappable rich link bubble (Blooio /).","properties":{"url":{"type":"string","format":"uri"},"title":{"type":"string"}},"required":["url"]},"app_clip":{"type":"object","description":"An App Clip bubble (Blooio p2p iMessage only). Sends an App Clip launch URL that Messages renders as a native App Clip card (app icon + name + Open button); tapping it launches the instant App Clip experience without an install. Provide either a ready-made `url` (Apple's `https://appclip.apple.com/id?p=<bundleID>` launcher, or an app's own associated-domain App Clip link) or the `bundle_id` convenience field, which is expanded into the canonical Apple launch URL. Tip: App-Clip availability is region/OS gated by Apple; an unavailable clip renders a card that falls back to the App Store. To deep-link into an already-installed app instead (e.g. open a specific screen in a downloaded app), send that app's universal link via `rich_link`.","properties":{"url":{"type":"string","format":"uri","description":"The App Clip launch URL (https). Mutually exclusive with `bundle_id`; if both are given, `url` wins."},"bundle_id":{"type":"string","description":"App Clip bundle identifier (e.g. `com.example.app.Clip`), expanded to `https://appclip.apple.com/id?p=<bundle_id>`. Used when `url` is absent."},"title":{"type":"string","description":"Optional preview title override."}}},"imessage_app":{"type":"object","description":"Bring-your-own iMessage App Extension bubble (Blooio p2p iMessage only). Blooio is the transport, NOT the renderer: you point at YOUR OWN signed iMessage app extension and we assemble the MSMessage app balloon and send it. Recipients who have your app installed render its interactive overlay (its `MSMessagesAppViewController` reads `url` on tap); recipients who don't see the native template card (from `caption`/`image_url`) plus Apple's built-in App Store fallback. Identify your extension with `bundle_id` + `team_id` (used to form the `com.apple.messages.MSMessageExtensionBalloonPlugin:<team_id>:<bundle_id>` plugin id) and pass the app-state `url` your extension understands. This does NOT ship or host a renderer — the recipient must have your app for the interactive experience. (For instant, no-install experiences use `app_clip`; to deep-link into an already-installed app use `rich_link`.)","required":["bundle_id","team_id","url"],"properties":{"bundle_id":{"type":"string","description":"Your iMessage app extension's bundle identifier (e.g. `com.acme.app.MessagesExtension`)."},"team_id":{"type":"string","description":"Your Apple Developer Team ID. Combined with `bundle_id` to form the balloon plugin id."},"url":{"type":"string","description":"The app-state URL your extension reads on tap (its `MSMessage.url`). Carries the state to reconstruct on the recipient device."},"app_name":{"type":"string","description":"Display name shown for the app (the `an` field). Defaults to a generic label."},"caption":{"type":"string","description":"Template-card caption shown to everyone (including recipients without the app)."},"subcaption":{"type":"string","description":"Optional template-card subcaption."},"image_url":{"type":"string","format":"uri","description":"Optional https image used as the fallback template card thumbnail. Fetched best-effort (≤256KB); a fetch failure never fails the send."},"app_store_id":{"type":"integer","minimum":1,"description":"Optional App Store id (adam id) for your app. When the recipient doesn't have the extension installed, Messages uses it to deep-link the App Store \"get this app\" fallback to the correct listing. Omitting it leaves the send unchanged."}}},"poll":{"type":"object","description":"A native Blooio poll. Routed sends automatically consider only poll-capable channels.","properties":{"title":{"type":"string","minLength":1},"options":{"type":"array","items":{"type":"string","minLength":1},"minItems":2}},"required":["title","options"]},"interactive":{"type":"object","description":"Interactive content (WhatsApp Business / RCS). Requires `kind`.","properties":{"kind":{"type":"string"}},"required":["kind"],"additionalProperties":true},"template":{"type":"object","description":"Template content (WhatsApp Business).","properties":{"template_id":{"type":"string"}},"additionalProperties":true},"parts":{"type":"array","description":"Ordered text/media parts sent as one multipart message (Blooio only). When every part is an image/video (two or more, no text parts) Messages renders them as a photo/video carousel/collage by default; set `carousel: false` to opt out. With `format: \"markdown\"`, each part's `text` is parsed for styling independently.","items":{"type":"object","properties":{"text":{"type":"string"},"url":{"type":"string","format":"uri"}}},"minItems":1},"reply_to":{"type":"string","description":"Message id to reply to. Applies to the whole outgoing message."},"effect":{"type":"string","description":"iMessage screen/bubble effect (Blooio, text only)."},"link_preview":{"$ref":"#/components/schemas/LinkPreview"},"badge":{"type":"string","enum":["sent_with_siri","sent_with_facetime"],"description":"Optional attribution badge shown under the bubble: `sent_with_siri` renders \"Sent with Siri\" and `sent_with_facetime` renders \"Sent with FaceTime\", the way a message dictated to Siri or sent during a FaceTime call appears. Omit for no badge. Any other value is rejected with `422 invalid_content`. Applies to `text`, `attachments` (media), `parts` (multipart), `rich_link`, and `app_clip` sends on Blooio iMessage channels only; rejected with `422 invalid_content` on other content types. No effect on non-Blooio channels."},"carousel":{"type":"boolean","description":"Group image/video attachments into ONE message that Messages renders as a swipeable photo/video carousel/collage, instead of separate bubbles (Blooio p2p iMessage only). DEFAULTS ON: a Blooio send whose attachments (via `attachments` or `parts`) are two or more image/video URLs is grouped automatically — set `carousel: false` to send them as independent bubbles instead. Set `carousel: true` explicitly to also enforce the visual-only rule (a known non-visual extension like `.pdf`, a text part, or fewer than two attachments then returns `422 invalid_carousel`); left unset, a non-qualifying send simply isn't grouped. Image extensions: jpg/jpeg/png/gif/heic/heif/webp/bmp/tiff/avif; video: mov/mp4/m4v/qt/3gp/avi/mkv/webm; a URL with no extension is allowed (its real type resolves on the device). An optional `text` is carried as the carousel's caption. Only valid on Blooio iMessage channels (`422 carousel_unsupported_for_channel_type` if set on another channel type)."}}},"Recipient":{"description":"A recipient: a raw identifier string (phone/email/opaque provider id), an array of identifier strings (multiple recipients), an existing `{ group_id }` with an exact or hybrid owner sender, or (for routed sends) a `{ contact_id }`. A `contact_id` enables cross-channel-type routing via the contact's identities; a bare identifier reaches phone/email-addressable channels only.","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"contact_id":{"type":"string","description":"Contact id (ct_...) — routed sends only."}},"required":["contact_id"],"additionalProperties":false},{"type":"object","properties":{"group_id":{"type":"string","description":"Existing group id (grp_...) — exact sender or a hybrid sender that owns the group's channel."}},"required":["group_id"],"additionalProperties":false}]},"PriorityChannel":{"type":"object","description":"A channel in a priority. Channels sharing a `priority` (and type) form a pool.","properties":{"channel_id":{"type":"string","description":"Channel id (ch_...)."},"type":{"$ref":"#/components/schemas/ChannelType"},"address":{"type":["string","null"],"description":"The channel's real-world address (phone number for numbered channels)."},"alias":{"type":"string","description":"Organization-wide alias, when the channel has one (RCS Business)."},"priority":{"type":"integer","minimum":1,"description":"Tier (drag order). Lower runs first; same priority + same type = a pool."}},"required":["channel_id","priority"]},"Priority":{"type":"object","properties":{"id":{"type":"string","description":"Priority id (priority_...)."},"name":{"type":["string","null"]},"is_default":{"type":"boolean","description":"Whether this is the key's default priority (used by the agnostic send)."},"channels":{"type":"array","items":{"$ref":"#/components/schemas/PriorityChannel"}},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}}},"CreatePriorityRequest":{"type":"object","properties":{"name":{"type":"string"},"is_default":{"type":"boolean","description":"Make this the key's default priority. Defaults to true for the key's first priority."},"channels":{"type":"array","minItems":1,"description":"Channels in the priority. Each item is a channel id string or `{ channel_id, priority }`. Channels sharing a priority must be the same type.","items":{"oneOf":[{"type":"string"},{"type":"object","properties":{"channel_id":{"type":"string"},"priority":{"type":"integer","minimum":1}},"required":["channel_id"]}]}}},"required":["channels"]},"UpdatePriorityRequest":{"type":"object","description":"Any subset of fields. When `channels` is present it replaces the priority's channels wholesale.","properties":{"name":{"type":"string"},"is_default":{"type":"boolean"},"channels":{"type":"array","minItems":1,"items":{"oneOf":[{"type":"string"},{"type":"object","properties":{"channel_id":{"type":"string"},"priority":{"type":"integer","minimum":1}},"required":["channel_id"]}]}}}},"ApiKey":{"type":"object","description":"An organization API key. The `api_key` field is the secret bearer value — it is returned in full by the create and list endpoints and must be stored securely.","properties":{"api_key":{"type":"string","description":"The secret key value (prefixed `api_`). Send it as `Authorization: Bearer <api_key>` on API requests."},"name":{"type":["string","null"],"description":"Human-readable label for the key."},"type":{"type":["string","null"],"description":"Optional key type/category tag."},"created_at":{"type":["integer","null"],"description":"Creation time (epoch milliseconds)."},"valid_until":{"type":"integer","description":"Expiry time (epoch milliseconds), or `-1` when the key never expires."},"blocked":{"type":"boolean","description":"Whether the key is blocked (suspended)."},"deprecated":{"type":"boolean","description":"Whether the key has been deprecated (revoked)."}}},"CreateApiKeyRequest":{"type":"object","description":"Optional attributes for the new key. All fields are optional; an empty body mints an unnamed, non-expiring key.","properties":{"name":{"type":"string","description":"Human-readable label for the key."},"type":{"type":"string","description":"Optional key type/category tag."},"valid_until":{"type":["integer","null"],"description":"Expiry time (epoch milliseconds). Pass `null` or `-1` (or omit) for a key that never expires."}}},"ApiKeyChannelAssignment":{"type":"object","description":"Result of assigning/unassigning a channel to an API key.","properties":{"api_key":{"type":"string"},"channel_id":{"type":"string","description":"The resolved channel id (`ch_...`) the line belongs to."},"address":{"type":"string","description":"The line in E.164 that was (un)assigned."},"success":{"type":"boolean"},"reassigned":{"type":"boolean","description":"Present on assign: true when the line was taken from a previous owner."},"unchanged":{"type":"boolean","description":"Present on assign: true when the key already owned the line (no-op)."}}},"From":{"type":"string","description":"The sender as a single string: a phone number (any common format; matched to your exact numbered channel), a channel alias (organization-wide name of a non-numbered channel), or an exact channel id (ch_...). Omit it for automatic routing. If a phone number ever matches more than one active channel type, the send fails with `409 ambiguous_from` and the exact `ch_...` id must be used instead."},"SendMessageRequest":{"allOf":[{"$ref":"#/components/schemas/MessageContentFields"},{"type":"object","description":"Send to a recipient. `from` is one optional string (phone number, alias, or channel id); omit it for automatic routing. `priority_id`, `channel_type`, and `hybrid` are advanced routing controls, each mutually exclusive with an explicit `from`.","properties":{"from":{"$ref":"#/components/schemas/From"},"priority_id":{"type":"string","description":"Route through an explicit priority (priority_...). Mutually exclusive with `from`."},"channel_type":{"$ref":"#/components/schemas/ChannelType","description":"Constrain automatic/priority routing to one channel type. Mutually exclusive with `from`."},"hybrid":{"oneOf":[{"type":"boolean"},{"type":"string"}],"description":"Hybrid (Twilio-first / Blooio-after-reply) routing. `true` uses the calling key's hybrid configuration; a phone number string selects the hybrid owner of that representative number. Mutually exclusive with `from` and `priority_id`."},"to":{"$ref":"#/components/schemas/Recipient"},"dry_run":{"type":"boolean","description":"Validate and route only; do not send."}},"required":["to"]}]},"SendToChannelRequest":{"allOf":[{"$ref":"#/components/schemas/MessageContentFields"},{"type":"object","description":"Channel-scoped send. The sender comes from the path, so the body carries no `from` or routing controls.","properties":{"to":{"$ref":"#/components/schemas/Recipient"},"dry_run":{"type":"boolean","description":"Validate and route only; do not send."}},"required":["to"]}]},"SendToChatRequest":{"allOf":[{"$ref":"#/components/schemas/MessageContentFields"},{"type":"object","properties":{"dry_run":{"type":"boolean"}}}]},"CreateChatRequest":{"type":"object","properties":{"channel_id":{"type":"string"},"to":{"type":"string","description":"Recipient identifier (phone/email/opaque provider id)."}},"required":["channel_id","to"]},"CreateContactRequest":{"type":"object","properties":{"name":{"type":["string","null"]},"identifier":{"type":"string","description":"Phone (E.164) or email to attach as the first identity."},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"channel_id":{"type":"string","description":"Reserved for future use."}}},"AttachIdentityRequest":{"type":"object","properties":{"identifier":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"},"channel_id":{"type":"string","description":"Reserved for future use."}},"required":["identifier"]},"CreateGroupRequest":{"type":"object","properties":{"channel_id":{"type":"string","description":"Must be a group-capable channel (blooio/whatsapp)."},"members":{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"object","properties":{"identifier":{"type":"string"}},"required":["identifier"]}]},"minItems":1,"description":"Member phone numbers (E.164) or emails, each given as a string or an object `{ \"identifier\": \"...\" }`. Every identifier is validated; invalid members 422 `invalid_members` instead of being dropped."},"name":{"type":"string"}},"required":["channel_id","members"]},"SendPollRequest":{"type":"object","properties":{"title":{"type":"string"},"options":{"type":"array","items":{"type":"string"},"minItems":2}},"required":["title","options"]},"VotePollRequest":{"type":"object","description":"Provide option_index (0-based) or option text.","properties":{"option_index":{"type":"integer","description":"0-based option index."},"option":{"type":"string","description":"Option text."}}},"CreateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"HTTPS endpoint."},"event_types":{"type":"array","items":{"type":"string"},"deprecated":true,"description":"Retired. Every webhook receives every event; filter by `type` in your handler. Omit this field, or send `[\"*\"]`. Any narrower value is rejected with 422 `event_types_retired`. Subscriptions created before the retirement keep the list they were registered with."},"api_key":{"type":"string","description":"Scope the subscription to the lines this API key owns. Omit `api_key` and `integration_id` for an organization-wide subscription that receives events from every line. Must belong to your organization; sending both is rejected with 422 `ambiguous_scope`."},"integration_id":{"type":"string","description":"Scope the subscription to the lines this integration owns. Mutually exclusive with `api_key`."},"channel_id":{"type":"string"},"channel_type":{"$ref":"#/components/schemas/ChannelType"}},"required":["url"]},"UpdateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"event_types":{"type":"array","items":{"type":"string"},"deprecated":true,"description":"Retired. Omit this field to leave an existing subscription's frozen list untouched; sending anything narrower than `[\"*\"]` is rejected with 422 `event_types_retired`."},"status":{"type":"string","enum":["active","disabled"]}}},"BloobabilityMessage":{"type":"object","description":"One conversation message, in order. Only `id`, `role`, and `content` are forwarded to the model; any other field is dropped at the boundary so it cannot perturb the assessment.","required":["id","role","content"],"properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]*$","maxLength":128,"description":"The evidence ID the model can cite back in `evidence_ids`. Must match `^[A-Za-z0-9][A-Za-z0-9_.:-]*$` (≤128 chars) and be unique across the conversation."},"role":{"type":"string","enum":["customer","agent","system"],"description":"Who sent the message."},"content":{"type":"string","minLength":1,"description":"The message text. Must be non-empty."}}},"BloobabilityPolicy":{"type":"object","description":"A policy to assess the conversation against.","required":["id","description"],"properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]*$","maxLength":128,"description":"The policy ID. This is the **calibration key**: an `id` matching a policy the calibration map was fitted on yields the `roster` tier; a novel `id` yields `pooled_unseen`. Must match `^[A-Za-z0-9][A-Za-z0-9_.:-]*$` (≤128 chars) and be unique across the request."},"description":{"type":"string","minLength":1,"description":"What the model actually judges the conversation against. Keep it consistent with what the `id` implies — a description that means something different from a known `id` produces a \"trusted\" `roster` label on a number that is not really trustworthy."}}},"BloobabilityAssessRequest":{"type":"object","required":["conversation","policies"],"properties":{"reference_id":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]*$","maxLength":128,"description":"Optional. Your own reference, echoed back verbatim so you can reconcile a response with your records. It is **not** sent to the model and does not affect the result, and it is **not** an idempotency key. If you omit it, it is simply absent from the response — the server never mints one."},"conversation":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/BloobabilityMessage"},"description":"The messages to assess, in order. Non-empty."},"policies":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/BloobabilityPolicy"},"description":"The policies to score, each independently. Non-empty."},"include_evidence":{"type":"boolean","default":false,"description":"When `true`, the matcher runs in parallel with the gauge and each policy gains an `evidence_ids` array. Off by default."}}},"BloobabilityPolicyResult":{"type":"object","required":["id","calibrated_probability","verdict","calibration_tier"],"properties":{"id":{"type":"string","description":"The policy ID from your request."},"calibrated_probability":{"type":"number","minimum":0,"maximum":1,"description":"**The answer.** The calibrated probability (0–1) that this policy applies to the conversation."},"verdict":{"type":"string","enum":["yes","no"],"description":"The gauge's discrete call — does this policy apply? `calibrated_probability` says how confident; the accept rule is computed against this side."},"calibration_tier":{"type":"string","enum":["roster","pooled_unseen"],"description":"How trustworthy the number is. `roster`: a policy the calibration map was fitted on, with its own per-label offset — `accepted` is meaningful. `pooled_unseen`: a novel policy scored by the pooled map with no per-label offset — the probability is usable but `accepted` is omitted."},"accepted":{"type":"boolean","description":"The frozen accept/defer decision, frozen at the release's validated risk bound. `true` means the confidence on the predicted side clears the frozen per-verdict threshold. Deliberately strict (a confident `0.879` `yes` is still not accepted). **Omitted for `pooled_unseen`** — treat a missing `accepted` as \"no validated decision,\" never as `false`."},"evidence_ids":{"type":"array","items":{"type":"string"},"description":"Supporting message IDs from the **matcher** pass — present only when `include_evidence: true`. This is an independent judgment from the gauge's score, so a policy can have a high `calibrated_probability` but an empty `evidence_ids`."}}},"BloobabilityAssessResponse":{"type":"object","required":["policies"],"properties":{"reference_id":{"type":"string","description":"Verbatim echo of your `reference_id`. Absent if you did not send one."},"policies":{"type":"array","items":{"$ref":"#/components/schemas/BloobabilityPolicyResult"},"description":"One result per requested policy, in the same order as `request.policies`."}}}}},"x-blooio-webhook-event-notes":{"message.reaction":"A tapback (classic reaction like `love`/`like` or an emoji) was added to or removed from a message. Payload carries `reaction`, `action` (`add`/`remove`), the reacted `message_id`, `original_text`, the `sender`, and — crucially for multi-image sends — `part_index`: WHICH part of a multipart/carousel message was reacted to (0-based, counting the message's parts). The enriched payload also includes the message's full ordered `attachments` array and, when the tapback landed on an image, `reacted_attachment`: the exact `{ index, url, media_type, size, caption }` that was reacted to (null when the reaction targeted a text/caption part or a non-multipart message). So a reaction on the 4th photo of a carousel reports `part_index: 3` and a `reacted_attachment` pointing at that specific image — not just \"loved the message\".","message.pending":"An outbound message row was accepted but no device has taken it yet. Emitted only by the v1/v2 and GHL send paths, which persist the row before dispatching it; a native `/v4` send inserts straight as `queued` and so never reports a pending state. `message.queued` is the first lifecycle event common to every send path.","safety.state_changed":"Fires when a sending number's warm-up tier or safety action changes. Payload carries `tier`, `previous_tier`, `action`, `previous_action` (one of `none`, `queue`, `slow`, `pause_new`, `reply_only`, `review`), and `reasons`.","safety.number_banned":"Fires when Apple bans a dedicated or shared sending number. Payload carries `channel_id`, `phone_number`, `allocation_type` (`dedicated` or `shared`), and `banned_at`.","number.purchase.completed":"A number purchase finished provisioning. Because purchases are asynchronous (the charge triggers a Stripe invoice whose `invoice.paid` provisions the lines), this is how you learn the order succeeded. A purchase may provision several lines, so the payload reports all of them: `purchase_id`, `status` (`completed`), `lines` (one entry per provisioned line, each `{ phone_number, channel_id }`), and the flat `phone_numbers` (E.164) and `channel_ids` (`ch_` ids) arrays covering every provisioned line. When lines are owned by different API keys, every owning key's subscription receives the event.","number.purchase.action_required":"The saved card needs additional authentication (3DS/SCA) before the purchase can complete. Payload carries `purchase_id`, `status` (`action_required`), and `action_url` — open it to finish authentication. Once completed, provisioning proceeds and a `number.purchase.completed` event follows.","number.purchase.failed":"A number purchase failed terminally; no lines were provisioned. Payload carries `purchase_id`, `status` (`failed`), and `reason`.","number.removed":"A line was removed/unsubscribed (via `DELETE /channels/{channel}` or the dashboard). Payload carries `phone_number`, `channel_id`, `binding_id`, and `reasons` (the churn reason labels)."},"x-blooio-webhook-event-types":["message.received","message.queued","message.pending","message.sent","message.delivered","message.failed","message.read","message.reaction","poll.created","poll.received","poll.voted","group.name_changed","group.icon_changed","contact.shared","contact.merged","typing.started","typing.stopped","safety.state_changed","safety.number_banned","chat.closed","identity.verified","number.purchase.completed","number.purchase.action_required","number.purchase.failed","number.removed","handoff.created","handoff.customer_reply","handoff.acknowledged","handoff.resolved"],"x-blooio-endpoints":["DELETE /v4/api-keys/:apiKey","DELETE /v4/api-keys/:apiKey/channels/:channel","DELETE /v4/channels/:channel","DELETE /v4/chats/:chatId/background","DELETE /v4/chats/:chatId/participants/:identityId","DELETE /v4/chats/:chatId/typing","DELETE /v4/contacts/:contactId","DELETE /v4/contacts/:contactId/identities/:identityId","DELETE /v4/contacts/:contactId/tags/:tag","DELETE /v4/groups/:groupId","DELETE /v4/groups/:groupId/icon","DELETE /v4/priorities/:priorityId","DELETE /v4/templates/:templateId","DELETE /v4/webhooks/:webhookId","GET /v4/api-keys","GET /v4/api-keys/:apiKey","GET /v4/api-keys/:apiKey/channels","GET /v4/attachments/:attachmentId","GET /v4/channels","GET /v4/channels/:channel","GET /v4/channels/:channel/capabilities","GET /v4/channels/:channel/settings","GET /v4/channels/blooio/available","GET /v4/channels/blooio/purchases/:purchaseId","GET /v4/chats","GET /v4/chats/:chatId","GET /v4/chats/:chatId/background","GET /v4/chats/:chatId/messages","GET /v4/chats/:chatId/messages/:messageId","GET /v4/chats/:chatId/messages/:messageId/events","GET /v4/chats/:chatId/messages/:messageId/status","GET /v4/chats/:chatId/participants","GET /v4/chats/:chatId/polls/:pollId","GET /v4/contacts","GET /v4/contacts/:contactId","GET /v4/contacts/:contactId/capabilities","GET /v4/contacts/:contactId/identities","GET /v4/contacts/:contactId/profile","GET /v4/contacts/:contactId/tags","GET /v4/contacts/:contactId/timeline","GET /v4/events","GET /v4/events/:eventId","GET /v4/groups","GET /v4/groups/:groupId","GET /v4/groups/:groupId/members","GET /v4/location/contacts","GET /v4/location/contacts/:handle","GET /v4/location/contacts/refresh","GET /v4/me","GET /v4/me/numbers","GET /v4/me/numbers/:number/contact-card","GET /v4/me/priority","GET /v4/phone-numbers/lookup","GET /v4/priorities","GET /v4/priorities/:priorityId","GET /v4/templates","GET /v4/templates/:templateId","GET /v4/webhooks","GET /v4/webhooks/:webhookId","GET /v4/webhooks/:webhookId/deliveries","GET /v4/webhooks/:webhookId/logs","PATCH /v4/channels/:channel/settings","PATCH /v4/chats/:chatId","PATCH /v4/contacts/:contactId","PATCH /v4/contacts/:contactId/identities/:identityId","PATCH /v4/groups/:groupId","PATCH /v4/priorities/:priorityId","PATCH /v4/webhooks/:webhookId","POST /v4/ai/bloobability/assess","POST /v4/api-keys","POST /v4/attachments","POST /v4/channels/:channel/messages","POST /v4/channels/blooio/purchases","POST /v4/chats","POST /v4/chats/:chatId/contact-card","POST /v4/chats/:chatId/messages","POST /v4/chats/:chatId/messages/:messageId/reactions","POST /v4/chats/:chatId/participants","POST /v4/chats/:chatId/polls","POST /v4/chats/:chatId/polls/:pollId/vote","POST /v4/chats/:chatId/read","POST /v4/chats/:chatId/typing","POST /v4/contacts","POST /v4/contacts/:contactId/identities","POST /v4/contacts/:contactId/merge","POST /v4/contacts/:contactId/tags","POST /v4/groups","POST /v4/groups/:groupId/icon","POST /v4/location/contacts/refresh","POST /v4/me/numbers/:number/call-forwarding","POST /v4/me/numbers/:number/contact-card/avatar","POST /v4/messages","POST /v4/phone-numbers/batch","POST /v4/phone-numbers/lookup","POST /v4/priorities","POST /v4/templates","POST /v4/webhooks","POST /v4/webhooks/:webhookId/deliveries/:deliveryId/replay","POST /v4/webhooks/:webhookId/logs/:deliveryId/replay","POST /v4/webhooks/:webhookId/secret/rotate","PUT /v4/api-keys/:apiKey/channels/:channel","PUT /v4/channels/:channel/profile","PUT /v4/chats/:chatId/background","PUT /v4/me/numbers/:number/contact-card"]}