HCC Docs

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 doYours to do
Honor STOP: every scope revoked, the number on Do Not CallHave consent to text someone, and show it later
Refuse a number on Do Not Call, or with texts revokedRegister your numbers and use cases with your carrier
Keep one row per attempt, refusals includedThe hours you text in, and how often
Keep test mode away from any carrierWhat the messages say, and identifying yourself in them
Stop outbound when billing does. Inbound and STOP go onAct 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.

  1. 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.
  2. 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.
  3. 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.
StatusWhat a live Text gets
Offmessaging_unavailable
Not registerednumber_not_registered
In reviewnumber_not_registered
Rejectednumber_not_registered
ReadyIt's sent
Status unknownIt'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.codeStatusWhy
do_not_call422The number is on your Do Not Call list
opted_out422The number has texts revoked
caller_id_not_owned, no_caller_id422from isn't yours, or there's no number
messaging_unavailable422Texting is off for the number, or its carrier has none
number_not_registered422The number's on, and Twilio doesn't show it as registered
billing_required, billing_past_due, billing_disputed, usage_limit_reached, plan_has_no_seats402The billing stop
test_cap_reached429This 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, REVOKE and OPT OUT, as the whole message, in any case. Each revokes every scope and adds the number to Do Not Call, with the source text_stop, in one transaction with the Text's row. A person who said stop to a Text has said stop, so calls stop too.
  • START and UNSTOP record a grant of texts. They take nothing off Do Not Call, so a number that said STOP stays refused until you remove it. That's a decision for you to make.
  • HELP and INFO are recorded as keywords. We send no reply.
  • Free text is yours. please stop texting me revokes nothing: the Text has possibleOptOut, its Thread has possibleOptOutAt, and you get text.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 STOP we never heard, and the consent row's ingest says carrier_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 STOP is always kept.
  • English only. A STOP in 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.