REST API
Programmatic access to your Octocom data and configuration over plain HTTP — list and read conversations, manage workflows, articles and bot rules, search your product catalogue, record purchases, and more. This page explains what the API covers and where to find the full reference.
Octocom exposes a REST API for reading your customer support data and managing your bot's configuration programmatically. It's the right choice when you want to pull data into your own systems, run scripts and scheduled jobs, or integrate Octocom from a backend service — anywhere a stable HTTP contract is easier to work with than MCP.
Looking for the full, interactive reference? Every endpoint — with parameters, request/response schemas, and a "try it" console — lives at api-docs.octocom.ai. This page is the orientation; that's the spec.
Base URL
All endpoints are served from production under the /rest/v1 prefix:
https://api.octocom.ai/rest/v1The machine-readable OpenAPI 3 specification is available at https://api.octocom.ai/openapi.json — point your own codegen, Postman, or client library at it.
Browse the API through Copilot or MCP
Agents can inspect the deployed public API directly through three read-only
documentation tools. In Copilot, discover the rest_api_documentation capability
group:
list_rest_api_groups()returns every group, its description and operation count.list_rest_api_operations({ group: "Conversations" })returns endpoint methods, template paths and short summaries. FollownextOffsetuntil it isnull. Carry the first page'srevisioninto subsequent requests to detect changes during pagination; restart at offset 0 if the specification changes.get_rest_api_operation({ method: "POST", path: "/rest/v1/conversations/outbound-email" })returns the full operation contract, including optional parameters, defaults, schemas, examples, errors and authentication guidance.
The catalog is fetched from the public OpenAPI specification on each call. An
operation lookup includes inherited path parameters, servers and declared
security, and expands local schema references. Recursive references retain their
definitions in referenceDefinitions; any external references are listed in
unresolvedReferences. Large Copilot results may be saved to a file: read the
saved result before implementing the request.
These tools read documentation only. They do not execute API operations, provide credentials or change the caller's permissions. Check the full contract when using a Python helper: a helper may expose fewer options than its REST endpoint.
Authentication
Every request (except the public Browser Sessions endpoint and temporary recording downloads) authenticates with an API key sent in the X-API-Key header:
X-API-Key: your_api_key_hereCreate and manage named keys from Settings → API keys in your Octocom dashboard. Each key is scoped to your entire organization and has the same API access. Use one key per integration so you can rename, regenerate, or delete it independently. The existing API key is retained as Default; its value and authentication header do not change.
New keys are shown only once when created or regenerated. Copy them to a secure secret store before closing the dialog. Existing legacy keys remain viewable for compatibility. Regenerating or deleting a key immediately invalidates its previous value; other keys keep working. Already authenticated requests may finish.
Managing keys requires the existing API key permission. Rate limits remain shared across the organization, regardless of how many keys it has. Octocom automations use a separate internal credential, so deleting customer keys does not interrupt Python actions or background execution.
A first request
List conversations (oldest first by default):
curl https://api.octocom.ai/rest/v1/conversations \
-H "X-API-Key: $OCTOCOM_API_KEY"List endpoints are paginated and filterable. For example, fetch the second page of currently closed conversations that were created in a given window:
curl -G https://api.octocom.ai/rest/v1/conversations \
-H "X-API-Key: $OCTOCOM_API_KEY" \
--data-urlencode "status=closed" \
--data-urlencode "startDate=2026-01-01T00:00:00Z" \
--data-urlencode "endDate=2026-01-31T23:59:59Z" \
--data-urlencode "page=2"Conversations can also be filtered by handedOff state and by a metadataKey / metadataValue pair (e.g. to find the conversation tied to a specific Zendesk ticket). See the reference for the full set of parameters on each endpoint.
To find whether a customer has contacted support, filter directly by their
email address. Add businessSlug when the same organization contains multiple
businesses, and use imported=all if migrated help-desk history should count:
curl -G https://api.octocom.ai/rest/v1/conversations \
-H "X-API-Key: $OCTOCOM_API_KEY" \
--data-urlencode "[email protected]" \
--data-urlencode "businessSlug=your-business" \
--data-urlencode "imported=all"Conversation list items include the associated customer, derived status,
closedAt, and rating. GET /rest/v1/conversations/{id} exposes the same
rating as { "score": 5, "comment": "..." }, or null when the customer has
not submitted one.
Conversation recordings
Use GET /rest/v1/conversations/{id}/recording with your X-API-Key to retrieve
recording availability for a conversation UUID:
{
"data": {
"conversationId": "22222222-2222-4222-8222-222222222222",
"status": "ready",
"downloadUrl": "https://api.octocom.ai/rest/v1/recording-downloads?token=TEMPORARY_TOKEN",
"expiresAt": "2026-10-16T12:15:00.000Z"
}
}Status is processing, ready, or unavailable. The URL and expiry are null
unless ready. Keep the conversation ID and request a fresh URL when needed;
links expire after 15 minutes. Anyone holding a link can download that
conversation's recording without another API key, so keep links private.
The download returns a WAV file combining saved call recordings in call order,
including post-transfer segments. It rechecks access and recording availability.
Missing recordings remain processing during an active call and for up to ten
minutes after completion; afterwards available segments are served, or the
status becomes unavailable if none exist. Scrubbed recordings are excluded.
Readiness reflects saved recording references; provider retrieval errors return
502 on download. Downloads allow 30 requests/minute per organization, up to
64 MiB of source audio, with a 30-second total fetch deadline. An expired or
invalid link returns 404; request a new one through the authenticated endpoint.
Conversation listing and bulk details
GET /rest/v1/conversations accepts limit=1..200 (default 50), page
(default 1), sortBy=createdAt|updatedAt (default createdAt), and
sortOrder=asc|desc (default asc). UUID breaks timestamp ties consistently.
startDate / endDate are inclusive creation-time bounds.
updatedSince / updatedBefore are inclusive change-tracking-time bounds.
Both list items and details include updatedAt; a conversation without a stored
update timestamp uses its creation timestamp. This is internal change tracking,
not the time of the last customer message.
Indexed candidate selection typically lags writes by a few seconds. Returned
fields are enriched from live data; single and bulk details use live data too.
An organization without enabled/ready indexing uses the source query, which can
be slower. Existing filters and imported-history defaults are unchanged. Orbio's
per-business contractual boundary always uses the conversation's creation
date, even with sortBy=updatedAt or imported=all. Messages in an allowed
conversation are not separately truncated by that boundary.
For incremental polling, use an upper bound behind the present time, keep the same bounds and page size while paging, and overlap the next window. Upsert by UUID, deduplicate repeated results, and periodically reconcile: numbered pages are not a snapshot, indexing can take longer during backlog, and changes to related records are not a guaranteed exhaustive change feed.
POST /rest/v1/conversations/bulk takes { "ids": ["conversation-uuid", "..."] }
with 1–200 UUIDs and returns { "data": [...], "unavailableIds": [...] }.
Each detail has the same fields as GET /rest/v1/conversations/{id}. Repeated
IDs appear once, in request order. Missing, foreign-organization and pre-cutoff
IDs are indistinguishable in unavailableIds. Database reads are batched across
the authorized IDs, not implemented as 200 individual detail requests.
All authenticated REST v1 routes share 500 requests per 60-second window per
organization, across API keys. Bulk details additionally allow 2,000
requested IDs per 60-second window per organization, including duplicate and
unavailable IDs. Rejected batches do not consume the bulk-ID budget. A bulk
request returns 429 with Retry-After: 60 when that budget is exhausted or
its limiter is unavailable. Retry with backoff and avoid parallel bursts.
What you can access
Email inbox settings support listing connected inboxes and their provider types, reading settings, and patching signatures, response delays and email handling controls. Connection setup and provider credentials remain outside these endpoints.
The API is organized into resource groups. Most are read-only; the configuration resources (Workflows, Articles, Bot Rules) also support create / update / delete with version history and restore, mirroring what you can do in the dashboard.
| Resource group | What it covers |
|---|---|
| Conversations | List and read conversations with full message history, AI classifications, and agent notes. Add and remove tags, filter by tag, read and write internal notes, add events, set metadata, send outbound email and SMS, and import history from another system. |
| Customers | List customer profiles and contact info; update a customer and set customer metadata. |
| Custom Data | AI-classified data extracted from conversations, for analytics and reporting. |
| Businesses | List the businesses in your organization (use their slugs when calling business-scoped endpoints). |
| Products | Search a business's product catalogue — typed queries, search-as-you-type suggestions, voice, photos, and similar products. See below. |
| Users & Teams | List the team members, agents, and team structure in your organization. |
| Macros | Response templates used by human agents. |
| Workflows | Manage the multi-step playbooks that guide the bot — create, update, delete, list version history, and restore. |
| Articles | Manage knowledge base articles the AI answers from — full CRUD with versioning and soft-delete / restore. |
| Documents | Upload text or real files, inspect indexing readiness, archive and restore knowledge documents. See Knowledge Documents for automated refreshes. |
| Bot Rules | Manage the custom instructions that steer bot behavior — full CRUD with versioning and restore. |
| Bot Conversations | Start a conversation with the bot and send it messages programmatically (useful for automated testing or embedding). |
| Conversation API | Two-way messaging for a channel you own: push customer messages in and receive every bot or agent reply on a webhook. See below. |
| Google Sheets | Append, cursor-page, exactly match, and conditionally update rows in a Google Sheet (share the sheet with the service account shown in the reference as an editor). |
| Campaigns | Add recipients to a continuous phone campaign and read each recipient's calls and results. See Adding recipients. |
| Phone Line Routers | Manage, test and audit the Python that decides inbound calls before they're answered. See Routers. |
| Browser Sessions | Record a purchase against a browser session for custom sale attribution. No API key required — rate limited by IP. |
Two-way messaging over HTTP
If you own the channel the customer is on — your own WhatsApp number through your own provider, an in-house app, a system we have no integration with — you can run the whole conversation over HTTP. You push the customer's messages in; every reply comes back on a webhook.
Two endpoints write, and the existing GET /rest/v1/conversations/{id} reads.
POST /rest/v1/conversations starts a thread:
curl -X POST https://api.octocom.ai/rest/v1/conversations \
-H "X-API-Key: $OCTOCOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"businessSlug": "your-business",
"customer": { "name": "Maria Gonzalez", "phone": "+52 55 1234 5678" },
"messages": [
{ "sender": "bot", "content": "Hi Maria, your order #A-33915 has shipped...", "timestamp": "2026-08-18T08:55:10Z" },
{ "sender": "customer", "content": "When will it arrive?", "timestamp": "2026-08-18T09:12:44Z" }
],
"externalId": "your-msg-8891",
"tags": ["whatsapp"],
"metadata": { "orderNumber": "A-33915" }
}'It answers 201 immediately with conversationId, publicId and messageIds. You never wait for the bot — its reply arrives on your webhook.
POST /rest/v1/conversations/{id}/messages appends every reply after the first, and answers 202 with the stored messageId and whether a human currently owns the conversation.
Three things worth knowing:
Seed the message the customer is replying to. When someone answers a transactional message you sent from your own system, their reply often makes no sense alone — "when will it arrive?" needs the order confirmation for context. Include that original message in messages with "sender": "bot" and the bot can see what is being answered. Seeded messages are context only: they never trigger a bot turn and are never sent anywhere. It is optional, but it noticeably improves answers, and it gives your agents the full thread if the conversation is handed off. The last entry in messages must be from the customer.
Tag conversations to route them. tags and metadata are both accepted at creation and are what webhook filters match on, so one integration's traffic can be routed to its own endpoint without inspecting payloads at your end.
Supply externalId and retries are free. It is your own id for the message you are pushing. Replaying one returns the ids from the first call and stores nothing, so a retry after a timeout can't produce a duplicate.
Handoff is transparent. A handed-off conversation keeps accepting messages exactly as before, and replies keep arriving on the same webhook — only message.sender changes from bot to agent. You do not need to detect it or do anything differently.
The webhook is the intended way to receive replies and you should not need to poll. GET /rest/v1/conversations/{id} exists so you can verify state, recover if a delivery was missed, and debug while building.
Finding conversations by tag
GET /rest/v1/conversations?tag=escalated-web returns only conversations
carrying a tag with that exact title. Use it instead of paging every open
conversation to find the handful you tagged.
curl -H "X-API-Key: $OCTOCOM_API_KEY" \
"https://api.octocom.ai/rest/v1/conversations?tag=escalated-web&status=open"Tags are written with POST /rest/v1/conversations/tags and removed with
DELETE /rest/v1/conversations/tags, both taking { "conversationId": "...", "tag": "..." }. Removing only unlinks the tag from that conversation — the tag
itself stays available to your organization and on any other conversation.
Removing a tag the conversation does not have returns 404, so a removal that
did nothing is never reported as success.
A tag-driven queue should remove its tag as part of processing. Without that, the same conversations come back on every poll.
To be notified as tags change rather than polling for them, use the Tag Modified event handler or a webhook.
Internal notes
Notes are visible to your agents in the Octocom dashboard and are never shown to the customer.
| Endpoint | What it does |
|---|---|
POST /rest/v1/conversations/{id}/notes | Add a note ({ "text": "..." }) |
GET /rest/v1/conversations/{id}/notes | List every note, newest first |
{id} accepts either the conversation UUID or its public ID.
Notes created this way are attributed to api rather than to a user, and appear
in the timeline labelled as such, so an agent can tell them from one a colleague
typed. Each returned note carries authorType (user, api, or automation)
and authorName.
From Python, use
add_conversation_note
and
get_conversation_notes
instead of calling these directly.
Importing conversation history
POST /rest/v1/conversations/import brings conversations that happened somewhere else into Octocom — ticket history you're migrating off another helpdesk, or support handled in a system of your own that you want searchable alongside everything else. See the reference for the request format.
Send up to 200 conversations per batch, with at most 5,000 messages plus internal
notes in total and a 10 MiB JSON body. Imports allow 60 requests per minute per
organization, plus budgets of 6,000 conversations and 30,000 messages/notes per
minute per organization and globally. Two batches can execute concurrently across
the service, with one active batch per organization; tickets within each batch are
processed sequentially. These are
safety ceilings, not a promised processing rate. Start with small batches and
increase gradually while checking latency and per-item results. Honor
Retry-After on 429 or 503, use backoff with jitter, and keep each ticket's
externalId unchanged when retrying. Successful tickets in a partial batch are
skipped as duplicates on retry. Keep the import resumable using per-item results. Batches
larger than 50 temporarily return 503 until the online customer phone index is
ready; smaller batches remain available.
Two things to know before you use it:
Imported conversations are archive records, not live tickets. The bot never answers them, they're never routed into an agent queue, and they're read-only in the dashboard. They're also excluded from bot quality scoring and automation metrics, so migrating years of history won't distort your reporting. If you need a thread your agents can actually keep working, don't import it — resolve it in your old system and let the customer's next reply open a normal Octocom conversation.
They're hidden from GET /rest/v1/conversations by default. Pass imported=yes to list only imported conversations, or imported=all for both. This applies whether or not you use the import endpoint yourself.
Every timestamp you supply is stored as given, so history keeps its real dates. Give each record an externalId from the source system and retries are safe — re-importing the same one returns status: "duplicate" instead of creating a second copy. Batches are applied per item rather than all-or-nothing, so read the per-item results rather than relying on the status code alone.
Product search
If Storefront Search is set up for a business, the same search pipeline is available over REST — for a storefront you render server-side, a mobile app, or anywhere our browser SDK doesn't fit. Six POST endpoints, each taking the business's businessSlug:
| Endpoint | What it does | Rate limit |
|---|---|---|
/products/search | Full search for a typed query. | 60/min |
/products/search/suggest | Search-as-you-type panel: query completions, a few top hits, and category/brand facets. | 600/min |
/products/search/panel | Full search answered in the suggest panel's shape — results and facets, count, refinements. | 60/min |
/products/search/voice | Transcribe a recorded spoken query and search for it, in one request. | 30/min |
/products/search/image | Search by photo. | 30/min |
/products/search/similar | "More like this" for a product page. | 300/min |
/panel is /search and /suggest in one request. It runs the complete pipeline — so its ranking is /search's, not the keyword probe's — and returns the same fields /suggest does: products, categories, brands, totalCount, suggestions, plus relaxed. Use it for a submitted query when you render the surrounding furniture (a facet rail, a result count, "narrow your search" chips) yourself: the alternative is calling /search and /suggest together, which costs a wasted search and produces chips describing a differently-ranked result set. It costs the same as /search; it is not a substitute for /suggest on a keystroke.
Note that totalCount is the number of products matching the query at all, not the length of products — that's capped by topK.
Every ranking endpoint answers with the same results array — { id, score, product }, where product is the untouched product object as ingested from your store feed. You render it with the fields you already have; we don't impose a product schema on you. Rate limits are per organization, and the business must belong to the organization the API key is scoped to.
A typed search:
curl https://api.octocom.ai/rest/v1/products/search \
-H "X-API-Key: $OCTOCOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"businessSlug":"my-store","query":"black leather office chair","topK":20}'The same query with the panel data, for a results page that shows facets and a count alongside:
curl https://api.octocom.ai/rest/v1/products/search/panel \
-H "X-API-Key: $OCTOCOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"businessSlug":"my-store","query":"black leather office chair","topK":20}'{
"data": {
"products": [
{
"id": "...",
"score": 0.82,
"product": { "...": "your feed's product" }
}
],
"suggestions": [
{ "text": "black leather office chair swivel", "role": "attribute" }
],
"categories": [
{ "text": "Office Chairs", "url": "https://...", "count": 34 }
],
"brands": [],
"totalCount": 41,
"relaxed": false
}
}Facet chips are only returned for values your feed gives a navigable URL for, so categories and brands come back empty for feeds that carry names but no links. That vocabulary still reaches you through suggestions, which need no URL.
Voice
Send one short recording — a single spoken query. This is not a streaming endpoint: detect end-of-speech on the client, then post the clip. The transcript is fed to the same pipeline as a typed query, so you get the transcript and its results in one round trip.
Audio goes in the audio field, base64-encoded (a data: URL works too), up to 1 MB decoded — about 15 seconds, far more than a search query needs. audio/webm, ogg, mp4, mpeg, wav, aac and flac are accepted.
curl https://api.octocom.ai/rest/v1/products/search/voice \
-H "X-API-Key: $OCTOCOM_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg audio "$(base64 -i query.webm)" \
'{businessSlug: "my-store", audio: $audio, mimeType: "audio/webm"}')"{
"data": {
"transcript": "black leather office chair",
"noSpeech": false,
"language": "en",
"results": [{ "id": "SKU-12345", "score": 0.83, "product": { "…": "…" } }],
"relaxed": false
}
}noSpeech: truemeans the recording carried nothing recognizable — a normal outcome, and answered as200with no results. Tell the shopper "didn't catch that" rather than "no products match".- Language is detected between a small candidate set rather than freely, which is much more accurate on a three-word utterance. Omit
languageto use the business's configured voice languages; pass one code ("el") to state it outright, or a comma-separated list ("lt,en,ru") to detect between them. The language actually heard comes back in the response. - Recognition is biased with your catalogue's own mined vocabulary, so brand and product names survive.
- Pass
"mode": "transcribe"to get only the transcript and run the search yourself later — for a dictate-and-review flow where the shopper edits before submitting. - Unlike the storefront widget's mic, this endpoint doesn't require the dashboard's Voice search switch. That toggle governs what shoppers get on your storefront; this surface is your own authenticated backend.
Image
The photo is read by a vision model whose understanding drives retrieval directly. Base64 in the image field (or a data: URL), up to 6 MB decoded, as image/jpeg, png, webp or gif. Downscale big photos to around 1024px on the long edge first — it costs nothing in quality and is most of the upload latency.
curl https://api.octocom.ai/rest/v1/products/search/image \
-H "X-API-Key: $OCTOCOM_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg image "$(base64 -i chair.jpg)" \
'{businessSlug: "my-store", image: $image, mimeType: "image/jpeg", query: "in blue"}')"The optional query refines the photo ("in blue", "cheaper") rather than replacing it. The response adds saw — what the model read off the photo:
{ "data": { "results": [], "relaxed": false, "saw": ["office chair"] } }Show saw to the shopper ("Showing results for: office chair"). A misread photo then reads as a misread instead of looking like an empty catalogue. This endpoint can't start retrieving until the photo is understood, so it's the slowest of the five.
saw is the item type read off the photo, not the query that was run: retrieval also uses the colour, material, size and setting the model read, which is what distinguishes the photographed product from the rest of its category. The full derived query is shown in the dashboard's Storefront Search preview if you need to debug a result.
Similar products
A "you may also like" row, keyed by the product's id as it appears in your feed — the same id the other endpoints return.
curl https://api.octocom.ai/rest/v1/products/search/similar \
-H "X-API-Key: $OCTOCOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"businessSlug":"my-store","productId":"SKU-12345","topK":8}'{
"data": {
"results": [{ "id": "SKU-67890", "score": 0.71, "product": { "…": "…" } }],
"source": { "id": "SKU-12345", "name": "Aera Office Chair" }
}
}Results never include the source product, and are reranked using its own indexed type and category — which is what keeps a phone's cases out of its neighbours. source is null when the id isn't in the catalogue index; that's different from a product with no neighbours, and usually worth handling differently. Costing no model or embedding call, this is the cheapest of the five and safe to call on every product-page view.
Pagination, rate limits, and errors
- Pagination — list endpoints accept a
pagequery parameter (1-based). - Rate limiting — requests are rate limited; exceeding the limit returns
429 Too Many Requests. Back off and retry. - Errors — the API uses standard HTTP status codes:
| Status | Meaning |
|---|---|
200 | Success |
400 | Malformed request — e.g. an unreadable, oversized, or unsupported upload |
401 | Invalid or missing API key |
404 | Resource not found |
429 | Rate limit exceeded |
500 | Server error |
REST API vs. MCP
Octocom offers two programmatic surfaces, and they're complementary:
- REST API — a fixed, versioned HTTP contract. Best for backend integrations, data pipelines, scheduled jobs, dashboards, and anything you want to script deterministically.
- MCP — the same capabilities (and more) exposed to AI agents like Claude and ChatGPT, so an agent can configure, debug, and test your bot in natural language. Best for hands-on, exploratory, and "do this for me" work.
If you're writing code, start with the REST API. If you're driving an AI agent, use MCP.
Need access or help?
API keys are available from the dashboard today. If anything in the reference is unclear or you need an endpoint that isn't there yet, reach out — we're expanding the API and use your requests to prioritize.
Starting an email conversation for the bot
POST /rest/v1/conversations/outbound-email sends the opening text as a bot
message. Set handedOff to false to let the bot answer customer replies.
closeAfterSend defaults to true: the notification is closed after the send
(or queueing for asynchronous providers), and a customer reply reopens it:
{
"businessSlug": "your-business",
"to": "[email protected]",
"customerName": "Customer",
"subject": "An update on your order",
"text": "Your order has shipped. Reply if you need help.",
"handedOff": false,
"closeAfterSend": true,
"tags": ["gift_campaign", "prevent_notif_of_received_request"]
}Omitting handedOff preserves the existing notification behavior: the
conversation is handed off and closed after sending, and customer replies
reopen it for human handling.
Use closeAfterSend: false when the outbound conversation should stay open.
This option is independent of handedOff, which controls who handles replies.
Closing prevents inactivity follow-ups while the conversation is closed; normal
follow-up rules can resume after the customer replies and the bot answers.
tags are added to the Octocom conversation and, for Zendesk, to the external
ticket at creation. Use a stable campaign tag to find the same sequence in both
systems. prevent_notif_of_received_request is an example of a merchant-specific
Zendesk trigger suppression tag, not a universal Zendesk setting.
For Zendesk, the business needs an enabled email integration and a bot agent. The opening message creates a Zendesk ticket with a public comment authored by that agent, assigned to the bot and scoped to the configured brand and required tag. If discovery uses an inbox allowlist, the first non-blocked allowed address is used as the ticket's support address. Zendesk's requester-notification triggers must be enabled to deliver the email. Octocom records the ticket and message IDs so replies picked up by the Zendesk poller join the original thread. Merchant handoff, exclusion and assignment rules still apply.
This endpoint sends a real email and does not accept an idempotency key; avoid blindly repeating requests after an ambiguous timeout.
Starting an SMS conversation
POST /rest/v1/conversations/outbound-sms texts a customer from one of a
business's SMS numbers and creates the conversation. The customer's reply
threads into it. The text is sent first, so a refused number creates nothing
and returns 400 with a code such as INVALID_RECIPIENT or
PROVIDER_REJECTED:
{
"businessSlug": "your-business",
"to": "+12025550187",
"text": "Your order has shipped. Reply here if you need help.",
"from": "+18884407931",
"handedOff": false,
"tags": ["shipping_update"]
}from must be one of the business's Twilio SMS numbers with receiving turned
on; omit it to use the first. A to number without a country code is read as
being in the sender's country. Unlike outbound email, the conversation stays
open by default (closeAfterSend: false), because a reply to a closed SMS
conversation starts a new one.
This endpoint sends a real text and does not accept an idempotency key; avoid blindly repeating requests after an ambiguous timeout.
Ticket activity events
Ticket activity records are separate from webhook and automation trigger types.
Conversation detail responses (REST and MCP) keep the envelope
{ id, event, createdAt, metadata }. Treat metadata according to event;
new general-purpose records use event: "event" and versioned metadata:
{
"id": "00000000-0000-4000-8000-000000000001",
"event": "event",
"createdAt": "2026-10-17T09:05:00Z",
"metadata": {
"version": 1,
"text": "Snoozed until Oct 19, 10:00",
"details": "Resumes Oct 19, 2026 10:00 UTC (UTC)",
"initiator": {
"type": "user",
"id": "00000000-0000-4000-8000-000000000002",
"name": "Maria Smith"
}
}
}V1 requires text and initiator; subtext and details are optional plain
text. Initiator types are user, customer, system, integration, and
unknown. Known initiators have a stable id and a display-name snapshot
(name); unknown has only a name. System identifiers describe their source,
such as snooze-scheduler; they are not user IDs. Historical actors are not
inferred from the current assignee.
REST integrations store the API key's name at the time of the event, its stable
key ID, and initiator.source: "rest-api". Renaming or rotating a key does not
rewrite earlier events. MCP ticket actions store the OAuth client as the
integration, with source: "mcp" and the connected user's ID/name snapshot in
onBehalfOf, for example:
{
"type": "integration",
"id": "mcp_example_client",
"name": "Support assistant",
"source": "mcp",
"onBehalfOf": { "id": "example-user", "name": "Maria Smith" }
}source and onBehalfOf are optional V1 fields. Records without them remain
valid. If the client has no registered name, MCP events use MCP.
Existing raw records contain text in metadata. Other historical types,
such as close, snooze, and merge, retain their original metadata. Clients
must handle mixed histories, unknown event types, and future metadata versions
without failing the whole response. IDs and timestamps are unchanged. If an
unknown version has readable metadata.text, clients can show that text as a
fallback without assuming the rest of the version's structure.
To create an event, existing requests still work:
POST /rest/v1/conversations/events
X-API-Key: YOUR_API_KEY
Content-Type: application/json{
"conversationId": "exampleTicket",
"event": "Refund processed",
"subtext": "Order #1234",
"details": "The payment provider confirmed the refund."
}event in this request is the text (1-1000 characters), not the stored type.
subtext is limited to 1000 characters and details to 12000 characters.
The server determines the initiator from authentication; clients cannot supply
another user's identity. The response remains
{ "success": true, "message": "Event created successfully", "eventId": "..." }.