OdyDocs

AI textingnew

Business AI Texting: let an AI agent answer the texts your numbers receive — grounded in your knowledge base, with hand-off built in — and take any thread over from code.

Business AI Texting lets one of your AI agents write back to the texts a business number receives. The reply is written server-side on every inbound text — grounded only in the workspace's knowledge base, in your team's voice (it learns from the replies your team has already sent), 24/7. When a text needs a person (a refund, a complaint, "can I talk to someone?", a question the knowledge doesn't cover) the agent says so, hands the thread to your team and goes quiet in that conversation. Anyone — a teammate in the inbox, or your code — can take over any conversation and hand it back.

This is the hosted alternative to running your own SMS agent: no LLM, no webhook server, no loop — three switches.

Where this shows up

Everything below is the same thing the app shows as the AI texting card on the AI agent screen, the How texts answer row in Settings → Numbers, and the I'll take it from here pill on a thread. Flip a switch here and a teammate sees it there, and vice versa.

#The three switches

A text is answered only when all three are on:

SwitchLevelEndpointWhat it controls
textingEnabledAgentPOST /agents/:id/textingWhether this agent may answer texts at all. Independent of enabled (the master switch — off means no calls and no texts).
textingAgentIdNumberPUT /numbers/:e164/textingWhich agent answers this line's texts. Independent of the line's voice routing — a line can ring the team for calls and still let an agent answer its texts. One agent per line.
aiTexting.pausedConversationPOST /conversations/:id/ai-textingA human owns this thread; the agent stays quiet in it until handed back. Set by your team, your code, or by the agent itself when it hands off.

Plus one workspace-level precondition you don't control per switch: texting must be live — the workspace's 10DLC campaign is approved, or the line is toll-free. Every response below reports it as textingReady; the agent starts answering automatically the moment it flips to true. Set everything up before that if you like.

None of this needs a publish: no carrier assistant is involved, so each change applies to the very next inbound text.

#What the agent will and won't do

  • Answers only from the knowledge base. Hours, prices, services, policies — if the texting AI can't read it in an article, the agent says your team will confirm and hands off. It never invents a fact. Control what it may read with the knowledge audience: texting (AI texting on every number) or texting:<e164> (one line only).
  • Never pretends to be human. Asked if it's a bot, it says it is your business's AI assistant and offers to bring in a teammate.
  • Never asks for card numbers, IDs, passwords or codes, and never promises a booking, payment or callback time unless the knowledge says exactly how that works.
  • Hands off on refunds, cancellations, disputes, complaints, legal, medical or safety topics, and whenever someone asks for a person — the run has handoff: true and the thread is paused for the agent from then on.
  • Stays out of the way of carrier keywords (STOP, START, HELP, YES…), of a teammate who already replied to that text, and of a paused thread.
  • Won't loop. At most 20 replies per conversation per hour, and exactly one reply per inbound text (the run is keyed on the inbound activity, so a redelivered webhook never double-replies).

#Pricing

30 AI credits per carrier segment of a reply, from the same credit wallet as the agent's calls (creditsPerSegment in every status response). A one- or two-sentence reply is usually one segment (30 credits); a longer one can be two (60). Only replies that are actually sent are charged — skipped texts and hand-offs without a reply cost nothing. When the wallet is empty the run is skipped with reason: "no_credits" and texts fall through to your team.

#The run object

Every inbound text the texting agent looks at produces one run — your audit trail and the answer to "why didn't the AI reply?". Read them per agent (GET /agents/:id/texting) or per thread (GET /conversations/:id/ai-texting/runs).

  • id uuid
  • agentId uuid | null, agentName string | null
  • conversationId uuid | null
  • inboundActivityId uuid | null — The customer's text that triggered the run.
  • replyActivityId uuid | null — The agent's reply, when status is replied. The same activity carries aiTextingAgentId in the thread; webhook payloads do not carry it — read the thread or the runs.
  • lineE164 string | null, contactE164 string | null
  • status "replied" | "skipped" | "failed"
  • reason string | null — Why no reply went out. texting_off no agent linked to the line · agent_off the agent or its texting switch is off · texting_not_enabled texting isn't live on the workspace yet · paused a human owns the thread · superseded / already_replied a teammate answered first · opt_out_keyword STOP/HELP/… · empty_message media-only text · rate_limited the 20-per-hour cap · no_credits · workspace_restricted · trial_locked · model_declined the agent chose to hand off without a reply · model_unavailable / not_configured / send_failed infrastructure.
  • detail string | null — Free text for failed runs.
  • groundingMode "vector" | "fulltext" | "inline" | "truncated" | "none" | null — How knowledge was retrieved.
  • groundingSources array — The knowledge articles the reply drew on (articleId, title).
  • handoff boolean — The agent handed the thread to the team.
  • segments integer | null, credits integer | null — For replied runs.
  • createdAt timestamp

#Texting status for an agent

GET/v1/api/agents/:id/texting

Everything about one agent's texting in one read.

Scope: agent:manage · Query: limit (runs, 1–100, default 20)

Returns

  • enabled boolean — The agent's AI texting switch.
  • agentEnabled boolean — The agent's master switch.
  • links { voiceNumbers, textingNumbers } — Lines that send this agent their calls, and lines that send it their texts.
  • textingReady boolean — Outbound texting is live on the workspace (10DLC approved or toll-free).
  • live boolean — enabled && agentEnabled && textingReady && links.textingNumbers.length > 0: the agent is answering texts right now. When false, the four fields above tell you which switch is missing.
  • creditsPerSegment integer — 30.
  • runs array of runs — Newest first.
curl https://api.ody.co/v1/api/agents/$AGENT_ID/texting \
  -H "Authorization: Bearer $ODY_API_KEY"
Response
{
  "enabled": true,
  "agentEnabled": true,
  "links": { "voiceNumbers": ["+15125550100"], "textingNumbers": ["+15125550100"] },
  "textingReady": true,
  "live": true,
  "creditsPerSegment": 30,
  "runs": [
    {
      "id": "7b1e…",
      "agentId": "0d9c…",
      "agentName": "Astra",
      "conversationId": "c4f1…",
      "inboundActivityId": "a1…",
      "replyActivityId": "a2…",
      "lineE164": "+15125550100",
      "contactE164": "+15125550123",
      "status": "replied",
      "reason": null,
      "detail": null,
      "groundingMode": "vector",
      "groundingSources": [{ "articleId": "9f…", "title": "Hours & location" }],
      "handoff": false,
      "segments": 1,
      "credits": 30,
      "createdAt": "2026-11-02T18:05:44.000Z"
    }
  ]
}

#Turn it on for an agent

POST/v1/api/agents/:id/texting

Sets the agent-level switch. Immediate — no publish. Lines linked to the agent stay linked when you turn it off and resume when you turn it back on.

Scope: agent:manage

  • enabled boolean required
  • unlinkLines boolean — With enabled: false: also clear the texting link on every line pointing at this agent. The released lines come back as released.

Returns { agent, links, textingReady, released? }. The agent only answers once a line is linked (below), its master switch enabled is on and textingReady is true.

curl -X POST https://api.ody.co/v1/api/agents/$AGENT_ID/texting \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":true}'
PUT/v1/api/numbers/:e164/texting

Chooses which agent answers this number's inbound texts — or null to send them to the team only. Encode + as %2B in the path. Full reference on the Numbers page.

Scope: agent:manage

  • agentId uuid | null required — One agent per line; linking another moves it.

Returns { ok, e164, textingAgentId, agentEnabled?, agentTextingEnabled?, textingReady } — the agent-side fields say whether the link is live or merely wired.

curl -X PUT https://api.ody.co/v1/api/numbers/%2B15125550100/texting \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agentId":"0d9c…"}'

#Take a thread over

POST/v1/api/conversations/:id/ai-texting

paused: true makes the agent stay quiet in this conversation until paused: false hands it back — every other thread keeps working. Read the current state from conversation.aiTexting on GET /conversations/:id. Full reference on the Conversations page.

Scope: conversations:write

await ody.conversations.setAiTexting(conversationId, true);
await ody.messages.send({ conversationId, body: "Hi, this is Sam from Acme — I've got it from here." });

#Recipes

Turn it on from scratch — create or pick an agent, give it knowledge, link the line, flip the switch:

TypeScript
const { agents } = await ody.agents.list();
const agent = agents[0];
await ody.knowledge.createArticle({ title: "Hours & location", content: "We're open Mon–Fri 9–6 at 12 Main St." });
await ody.numbers.setTextingAgent("+15125550100", agent.id);
const { textingReady } = await ody.agents.setTexting(agent.id, true);
// textingReady false? Finish 10DLC (POST /messaging/registration) — the agent starts automatically when it's approved.

Escalate on hand-off — subscribe to message.received; after each inbound text, read the thread's latest run. The agent answers (or decides not to) within a few seconds of the webhook, so give it a moment, then page a human when the run has handoff: true:

TypeScript
// in your webhook handler, a few seconds after message.received
if (event.type === "message.received") {
  const [run] = await ody.conversations.aiTextingRuns(event.data.conversationId, { limit: 1 });
  if (run?.inboundActivityId === event.data.activityId && run.handoff) await notifyTeam(event.data.conversationId);
}

Tell AI replies from human ones in any timeline: activity.aiTextingAgentId is set on the agent's messages and absent on yours.

Know why nothing was answered — read the thread's runs; the latest reason names the missing switch (texting_off, agent_off, texting_not_enabled, paused, no_credits, …).

Not exposed yet

The MCP server has no texting tools so far — use the REST endpoints (or the SDKs) above. The agent only answers; outbound, scheduled or proactive AI texts are not part of this feature.

Last updated Mon Nov 02 2026 00:00:00 GMT+0000 (Coordinated Universal Time)Something wrong on this page? Tell us