# 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

```ts label="JavaScript"
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 } });
```

```go label="Go"
api, err := stream.Backend{}.Client()
if err != nil {
	log.Fatal(err)
}
ptr := func(s string) *string { return &s }

_, err = api.SaveBusinessProfileWithResponse(ctx, acceleration.BusinessProfileRequest{
	LegalBusinessName:           ptr("Acme Dental LLC"),
	TaxId:                       ptr("12-3456789"),
	BusinessRegistrationCountry: ptr("US"),
	WebsiteUrl:                  ptr("https://acmedental.example"),
	AuthorizedContactEmail:      ptr("jo@acmedental.example"),
	// and the rest of the fields in the JavaScript example
})
if err != nil {
	log.Fatal(err)
}

samples := []string{
	"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.",
}
created, err := api.CreateUseCaseWithResponse(ctx, acceleration.UseCaseRequest{
	Name:           "Appointment reminders",
	UseCaseType:    ptr("ACCOUNT_NOTIFICATION"),
	Description:    ptr("Reminders and confirmations for appointments patients booked with us."),
	MessageFlow:    ptr("Patients tick an SMS consent box when booking at https://acmedental.example/book."),
	MessageSamples: &samples,
	HelpMessage:    ptr("Acme Dental: call (512) 555-0100 for help. Reply STOP to opt out."),
	OptOutMessage:  ptr("Acme Dental: you will get no more messages. Reply START to opt back in."),
})
if err != nil {
	log.Fatal(err)
}

if _, err := api.SubmitUseCaseWithResponse(ctx, created.JSON201.Id); err != nil {
	log.Fatal(err)
}
```

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`.

```ts label="JavaScript"
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,
);
```

```go label="Go"
_, err = api.SetSandboxRecipientsWithResponse(ctx, acceleration.SetSandboxRecipientsRequest{
	Recipients: []string{"+15125550100", "+15125550101"},
})
```

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:

```ts label="JavaScript"
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 } });
```

```go label="Go"
_, err = api.CreateOptOutWithResponse(ctx, acceleration.CreateOptOutRequest{
	Recipient: "+15125550199",
	Channel:   acceleration.OptOutChannelAll,
})
```

`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.

---

For the most recent version of this documentation, visit [https://getstream.io/agents/docs/php/telephony/10dlc/](https://getstream.io/agents/docs/php/telephony/10dlc/).