Texts
Your code sends SMS through your own carrier account, and we carry them. Whether you may text someone is yours to decide. We record what you send, and refuse a short list of things.
Using this doesn't make you compliant with anything. Texting people is regulated, and it needs your own consent, your own carrier registration and your own legal review. Nothing here gives you those.
| We always do | Yours to do |
|---|---|
Honor STOP: every scope revoked, the number on Do Not Call | Have consent to text someone, and show it later |
Refuse a number on Do Not Call, or with texts revoked | Register your numbers and use cases with your carrier |
| Keep one row per attempt, refusals included | The hours you text in, and how often |
| Keep test mode away from any carrier | What the messages say, and identifying yourself in them |
Stop outbound when billing does. Inbound and STOP go on | Act on opt-outs in free text, such as "please stop texting" |
We're a bridge to your carrier account: the account, what you send through it and what its bills and fines say are yours. We don't refuse a number for having no grant, and we don't check the hour. If your rules say a Text shouldn't go, don't send it: hcc.leads.consents and the Lead's timezone are there for your code to look at.
const text = await hcc.texts.send({
to: '+14155550123',
body: 'Your order shipped',
from: '+12025550142', // Without one, the caller ID default
});
text.status; // 'accepted', or 'unknown' if the carrier gave no clear answer
await hcc.texts.get(text.id);
for await (const thread of hcc.texts.threads({ phone: '+14155550123' })) {
thread.possibleOptOutAt; // The last time a Text here read like an opt-out
}A key needs texts:write to send and texts:read to read, and a developer's live key can't have texts:write. Test mode runs on the simulated carrier.
Send live
Connecting a carrier account asks an owner or admin to accept that every call and Text on it is the workspace's responsibility, including making sure you're allowed to reach each person. We keep who accepted and when.
Live Texts go out on your own Twilio account, from a number you've connected. We add no carrier charge of our own: Twilio bills you for each message and for any registration fee or fine. We meter one thing, a row of ours, at the price on the pricing page.
- Register in Twilio. A brand and a campaign for a local number, or a verification for a toll-free one. You do this in Twilio's console, and it's between you and the carriers. It can take weeks.
- Turn texting on, per number, on the dashboard's Numbers page. That points the number's inbound SMS webhook and its fallback at us, and leaves its calls alone. If something of yours is already there, we show you what it is and ask first. We keep it, and put it back when you turn texting off or remove the number.
- Check the status, on the same page. We read it from Twilio once a day, or when you ask, and link you to where you register. We don't submit anything to Twilio.
| Status | What a live Text gets |
|---|---|
| Off | messaging_unavailable |
| Not registered | number_not_registered |
| In review | number_not_registered |
| Rejected | number_not_registered |
| Ready | It's sent |
| Status unknown | It's sent, and Twilio decides. We couldn't read the status from Twilio |
We send from the number you give us as From, not through a Messaging Service, and ask for no validity period. If the number's Messaging Service sends inbound to its own webhook, the page warns you: a STOP still reaches us from Twilio's message list, within minutes rather than at once.
A status of Ready is what Twilio's API says about a registration. It isn't a statement that your messages are allowed, and carriers can still filter them.
Twilio answers a STOP itself and blocks the number at its end, under the default opt-out handling on your account. If you've turned that off or use Advanced Opt-Out, what you've set is yours to keep right.
Telnyx and other carriers can't send Texts here yet: the Numbers page doesn't offer it, and a live key gets messaging_unavailable.
Refusals
A refused Text throws, and its row stays. error.details.textId names it.
error.code | Status | Why |
|---|---|---|
do_not_call | 422 | The number is on your Do Not Call list |
opted_out | 422 | The number has texts revoked |
caller_id_not_owned, no_caller_id | 422 | from isn't yours, or there's no number |
messaging_unavailable | 422 | Texting is off for the number, or its carrier has none |
number_not_registered | 422 | The number's on, and Twilio doesn't show it as registered |
billing_required, billing_past_due, billing_disputed, usage_limit_reached, plan_has_no_seats | 402 | The billing stop |
test_cap_reached | 429 | This month's Test limit is used up |
Each workspace has a limit on attempts an hour, as it does on requests. Past it you get rate_limited and a 429 with no row: nothing was attempted. And past a billing stop, a workspace gets ten refused rows a day: after those it gets the same refusal with no row and no textId.
A send with no clear answer stays unknown and is never sent again by us, as a carrier may have sent it. Retrying yourself can text someone twice. hcc.texts.send sends an Idempotency-Key, so pass your own in options to make a retry of yours safe on our side.
Keywords and opt-outs
STOP,STOPALL,UNSUBSCRIBE,CANCEL,END,QUIT,REVOKEandOPT OUT, as the whole message, in any case. Each revokes every scope and adds the number to Do Not Call, with the sourcetext_stop, in one transaction with the Text's row. A person who said stop to a Text has said stop, so calls stop too.STARTandUNSTOPrecord a grant oftexts. They take nothing off Do Not Call, so a number that saidSTOPstays refused until you remove it. That's a decision for you to make.HELPandINFOare recorded as keywords. We send no reply.- Free text is yours.
please stop texting merevokes nothing: the Text haspossibleOptOut, its Thread haspossibleOptOutAt, and you gettext.possible_opt_out. The rules on opt-outs don't stop at keywords, so give this a handler or a person. It's a wide net and it can be wrong. - A carrier saying it blocks a number counts as a
STOPwe never heard, and the consent row'singestsayscarrier_block. On Twilio that's error 21610, and nothing else. - Past 1,000 inbound Texts an hour to one number, we keep only keywords and drop the rest, so that a flood stays bounded. A
STOPis always kept. - English only. A
STOPin another language is free text, if it's flagged at all.
The texts scope stands alone otherwise. Revoking only texts doesn't touch calls or the list, and a voice grant doesn't cover Texts.
Events
text.sent, text.delivered, text.failed, text.refused, text.received, text.opted_out, text.opted_in and text.possible_opt_out, plus thread.assigned and thread.closed from the desk's Inbox, which name the Thread's channel, contactAddress and ourAddress (and phone and ourNumber, null on a Channel with no phone). None carries the body: read it with hcc.texts.get. text.opted_out says how in via: keyword, carrier_block, or person when a rep marked the number at the desk.
Retention
A Text's body is kept for 5 years by default, as the setting on the dashboard's Settings page says, from 1 day to 20 years. After that the body is cleared and the row stays. Removing a Lead clears the bodies of their Texts, apart from a keyword's. Do Not Call and consent rows never expire.
Testing
Test mode never reaches a carrier. You play both the phone and the carrier:
// The phone
await hcc.simulator.text({ from: '+14155550123', to: '+12025550142', body: 'STOP' });
// The carrier: what became of a Text you sent
await hcc.simulator.textStatus(text.id, { status: 'delivered' });
// The carrier: how it answers sends to this number from now on
await hcc.simulator.textCarrier('+14155550123', 'recipient_blocked');textCarrier takes a failure reason, no_answer for a send that stays unknown, or null to go back to accepting.