10DLC

US carriers only deliver texts from a 10-digit number (10DLC) once the business behind it, and what it sends, are registered.

To register:

  1. Fill in your business profile once.
  2. Describe each kind of message you send as a use case.
  3. Submit the use case.

Stream reviews it, registers your brand and campaign with the carrier registry, and assigns your numbers to it.

Register

import { Client } from "@stream-io/calypso";

const api = new Client();

await api.put("/v1/phone/business-profile", {
  body: {
    legal_business_name: "Acme Dental LLC",
    legal_entity_type: "llc",
    organization_type: "private",
    tax_id: "12-3456789",
    business_registration_country: "US",
    industry: "healthcare",
    website_url: "https://acmedental.example",
    privacy_policy_url: "https://acmedental.example/privacy",
    registered_address: {
      street: "1 Main St",
      city: "Austin",
      state: "TX",
      postal_code: "78701",
      country: "US",
    },
    authorized_contact_first_name: "Jo",
    authorized_contact_last_name: "Smith",
    authorized_contact_email: "jo@acmedental.example",
    authorized_contact_phone: "+15125550100",
  },
});

const useCase = await api.post("/v1/phone/use-cases", {
  body: {
    name: "Appointment reminders",
    use_case_type: "ACCOUNT_NOTIFICATION",
    description:
      "Reminders and confirmations for appointments patients booked with us.",
    message_flow:
      "Patients tick an SMS consent box when booking at https://acmedental.example/book.",
    message_samples: [
      "Acme Dental: your cleaning is tomorrow at 3pm. Reply C to confirm or STOP to opt out.",
      "Acme Dental: thanks, your appointment on May 2 is confirmed.",
    ],
    help_message:
      "Acme Dental: call (512) 555-0100 for help. Reply STOP to opt out.",
    opt_in_message:
      "Acme Dental: you will get appointment reminders. Reply STOP to opt out.",
    opt_out_message:
      "Acme Dental: you will get no more messages. Reply START to opt back in.",
    numbers: ["+15125551234"],
  },
});

await api.post("/v1/phone/use-cases/{id}/submit", { path: { id: useCase.id } });

You can save a profile with fields still empty and finish it later. Submitting checks every field, and a 400 names the ones still missing.

When you submit:

  • description and message_flow need at least 40 characters each.
  • message_samples needs two to five samples, each at least 20 characters.

You can do the same in the dashboard under Agents > Telephony > 10DLC.

Review

A use case moves through these statuses. To see each change, who made it and what the reviewer said, call GET /v1/phone/use-cases/{id}/reviews.

Status Meaning
draft Saved, not submitted. Editable.
submitted Waiting on Stream's review.
changes_requested Stream asked for changes, in the review notes. Edit and submit again.
rejected Final.
vendor_pending Stream approved it; the carrier registry is reviewing the brand and campaign.
approved Its numbers may send.
vendor_rejected The registry refused it. Edit and submit again.

You can edit a use case only in draft, changes_requested or vendor_rejected.

A number sends under the use case that lists it in numbers. A number that no use case lists sends under your default use case (is_default), which is your first one.

Sandbox

Until one of your use cases is approved, your app is in the sandbox. Agents can text and call only the numbers on your list:

  • At most 2 numbers.
  • At most 30 messages and 30 minutes of calls a day. The day resets at midnight UTC.

Anything else is refused. POST /v1/phone/calls returns 403.

await api.put("/v1/phone/sandbox/recipients", {
  body: { recipients: ["+15125550100", "+15125550101"] },
});
const sandbox = await api.get("/v1/phone/sandbox");
console.log(
  sandbox.sandboxed,
  sandbox.messages_today,
  sandbox.audio_seconds_today,
);

A self-hosted router sandboxes nobody unless ROUTER_SANDBOX_ENABLED=true; ROUTER_SANDBOX_RECIPIENTS, ROUTER_SANDBOX_MESSAGES_PER_DAY and ROUTER_SANDBOX_AUDIO_MINUTES_PER_DAY change the limits.

Opt-outs

Stream handles these replies itself. None of them reach the agent.

  • STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, OPTOUT or REVOKE: the person gets your opt_out_message and is opted out of that channel. The agent never texts or calls them again.
  • START or UNSTOP: the person is opted back in.
  • HELP or INFO: the person gets your help_message.

You can also add, list and remove opt-outs yourself:

await api.post("/v1/phone/opt-outs", {
  body: { recipient: "+15125550199", channel: "all" },
});
const { items } = await api.get("/v1/phone/opt-outs");
await api.delete("/v1/phone/opt-outs/{id}", { path: { id: items[0]!.id } });

channel is sms, voice, whatsapp, imessage or all. Opt-outs apply whether or not your app is in the sandbox.

Call every endpoint on this page from your backend only.