# SMS cost reduction and deliverability

> Remove invalid, duplicate and unreachable numbers, and route messages people expect over the channels they use, before paying for SMS.

Canonical: https://mobilevalidate.com/use-cases/sms-cost-reduction · Last updated: 2026-09-25

![Messages pass through a filter so only reachable numbers are sent to; the cost chart falls after checking.](https://mobilevalidate.com/images/sms-cost-reduction.svg)

*Remove unreachable numbers before sending, so fewer messages are wasted.*


SMS spend drops when you stop paying for messages that can't arrive and move expected messages to cheaper channels the recipient already uses. A check before sending finds invalid and duplicate numbers for free. It also finds lines that can't take SMS, and it tells you which opted-in contacts can be reached on a messenger instead.

## Where does SMS money get wasted?

Most wasted spend falls into four groups:

- **Badly formatted or impossible numbers.** Typos, missing country codes and numbers that can't exist. The API parses each one and marks it `invalid_number`, at no charge.
- **Duplicates.** The same person entered twice in different formats, for example `07700 900001` and `+447700900001`. After [E.164](/glossary/e164) normalization they are the same number, and the repeat is marked `duplicate`, also free.
- **Lines that don't take SMS.** Fixed lines and some special number types. The [carrier lookup](/services/carrier-lookup) reports `line_type`, and for US and Canadian numbers there is a [bulk carrier lookup](/services/us-carrier-lookup).
- **Messages that could go elsewhere.** Some opted-in contacts prefer a messenger such as [WhatsApp](/services/whatsapp-number-check), [Viber](/services/viber-number-check) or [RCS](/services/rcs-capability-check), where the per-message price may be lower than your SMS rate.

The first two are free to find. The last two cost a check each, so they're worth it where your SMS rates are high.

## How does the workflow look?

For list cleaning, use bulk jobs:

1. Call `POST /v1/jobs/estimate` with the list and the checks you plan to run. It's free and shows how many rows are valid, invalid, duplicate, already cached, unsupported or suppressed, plus the maximum cost.
2. Create the job with `POST /v1/jobs` and pass the estimate's amount as `max_cost`.
3. Wait with `GET /v1/jobs/{id}?wait=30`, or subscribe to the `job.completed` webhook.
4. Download the results as CSV or NDJSON (`GET /v1/jobs/{id}/download?format=csv`). The file keeps your original row order and adds one column per check.
5. Remove invalid rows, set fixed lines to voice or e-mail, and route contacts to the channels they opted into.

For messages sent one at a time, such as order updates, a real-time lookup before each send does the same job.

## What should you do with each result?

| Result | Suggested action |
|---|---|
| `number_status: invalid_number` | Remove the number or ask the customer to correct it |
| `number_status: duplicate` | Keep one row per person |
| `line_type: fixed_line` | Don't send SMS; use voice or e-mail |
| `line_type: mobile` | Send SMS as usual |
| Messenger `registered: true` and the contact opted in to it | Consider sending on that channel |
| Messenger `registered: false` | Keep SMS for this contact |
| Any check `unknown` | Keep your current routing. Unknown results are free, so you lose nothing but the check |

## How much does it cost?

Bulk checks cost less per check than real-time lookups, and invalid, duplicate and suppressed rows are never charged. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). See [pricing](/pricing) for current rates.

To work out whether it pays off, multiply the checks you plan to run by the bulk price. Then compare that with the SMS spend you would avoid: messages to invalid and duplicate numbers, messages to fixed lines, and messages you can move to a cheaper channel. Checks of the same number inside the freshness window are served free from your account's cache, so running an updated list again doesn't bill the same numbers twice.

## Example request

First, a free estimate. The list below holds two valid test numbers, one duplicate and one invalid entry:

```bash
curl https://api.mobilevalidate.com/v1/jobs/estimate \
  -H "Authorization: Bearer $MOBILEVALIDATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers": ["+447700900001", "+447700900002", "+447700900002", "12345"], "checks": ["whatsapp", "viber"]}'
```

Response (test mode):

```json
{
  "object": "estimate",
  "total": 4,
  "valid": 2,
  "invalid": 1,
  "duplicate": 1,
  "cached": 0,
  "unsupported": 0,
  "suppressed": 0,
  "checks": ["whatsapp.registered", "viber.registered"],
  "checks_total": 8,
  "billable_max": 0,
  "max_cost": {"amount": "0", "currency": "USD"}
}
```

Test keys are never charged, so `billable_max` and `max_cost` are zero here. With a live key they show the most the job could cost. Only the two valid numbers would be checked. The same checks for one-off sends run in real time:

```bash
curl https://api.mobilevalidate.com/v1/lookup \
  -H "Authorization: Bearer $MOBILEVALIDATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers": ["+447700900001", "+447700900002"], "checks": ["whatsapp", "viber"]}'
```

Response (excerpt, test mode: the `checks` of the second item, a number with no accounts):

```json
{
  "whatsapp.registered": {
    "service": "whatsapp.registered", "status": "completed", "registered": false, "attributes": null,
    "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-25T14:28:31.050Z",
    "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
  },
  "viber.registered": {
    "service": "viber.registered", "status": "completed", "registered": false, "attributes": null,
    "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-25T14:28:31.050Z",
    "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null
  }
}
```

## What limits and rules apply?

A job takes up to 50,000 numbers and e-mails and up to 20 checks. The total of rows × checks can't exceed 100,000 per job. Requests that look like sequential number ranges or generated e-mail lists are rejected. Each account also has a daily cap on numbers, which `GET /v1/limits` reports.

Clean only lists you collected lawfully, such as your own customers and contacts who opted in. Channel checks help you reach people who want your messages more cheaply. They are not a way to find new people to message, and our [acceptable use policy](/legal/acceptable-use) forbids unsolicited bulk messaging.

## Frequently asked questions

### How much can I save?

It depends on your list quality and your SMS rates. Run a free estimate on a sample to see how many numbers are invalid or duplicate, then compare the price of a bulk check with your SMS price for the destinations you send to.

### Is the estimate really free?

Yes. POST /v1/jobs/estimate counts valid, invalid, duplicate, cached, unsupported and suppressed rows and returns the maximum cost without checking anything or charging you.

### Can I move marketing messages to WhatsApp because a number has an account?

Only for people who opted in to hear from you on that channel. Having an account is not consent, and messaging platforms require opt-in for business messages.

### Should I use real-time or bulk checks for list cleaning?

Bulk jobs. They take up to 50,000 numbers and e-mails per job, run every active service including bulk-only ones, and cost less per check than real-time lookups.

### Do duplicate or invalid numbers cost anything?

No. They are flagged per row with number_status invalid_number or duplicate and are never checked or charged.
