# Send individual (personalized bulk)

> Send up to 1000 personalized SMS in one request — a distinct message and sender per recipient. Accepted recipients get message_id; automatic encoding, atomic validation, cost summary. POST /sms/send-individual.
> Source: https://docs.23telecom.co.uk/sms/send-individual/

Instructions for LLMs: This is one page of the 23 Telecom messaging API docs
(SMS today; more channels planned). Base URL: https://restlink23telecom.com/api/v1,
auth via the X-API-Key header. Match errors on the error_code field, never on
description text. Full docs: https://docs.23telecom.co.uk/llms-full.txt · Schemas: https://docs.23telecom.co.uk/openapi.yaml

Send a **personalized bulk** of SMS in a single request: every recipient gets its
own message text and its own sender ID. This is the difference from
[`/sms/send`](https://docs.23telecom.co.uk/sms/send), which fans **one** message out to many recipients.

`POST /api/v1/sms/send-individual` (permission: `sms.send`)

Each accepted recipient is queued, billed and tracked exactly like a normal send,
and gets its **own** `message_id` in the response — so you can correlate delivery
by `message_id` or by phone number via [GET /sms/messages](https://docs.23telecom.co.uk/sms/messages).
A `blocked_country` recipient is returned in `results[]` without a `message_id`
and is not queued. Missing local pricing does not reject a valid live recipient:
it is accepted and queued with a provisional local cost.

## Request

```
POST https://restlink23telecom.com/api/v1/sms/send-individual
Header: X-API-Key: <your key>
Header: Idempotency-Key: send-20260627-batch-001
```
*(The web page shows this example in cURL, Node.js, Python, PHP, Ruby, Java, Go and .NET.)*

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` header | string | Yes | Stable key for this logical batch. Reuse it after a timeout or 5xx. Completed responses are retained for 24 hours and replayed byte-for-byte with `Idempotent-Replayed: true`; a replay is not a new send. A key used with a different body returns `409 IDEMPOTENCY_KEY_REUSED` |
| `messages` | object[] | Yes | 1 to 1000 messages |
| `messages[].to` | string | Yes | Recipient phone number in E.164 format (`+` optional, 6-15 digits) |
| `messages[].message` | string | Yes | Message text for **this** recipient — encoding is detected automatically; max 10 SMS segments |
| `messages[].sender_id` | string | Yes | Sender ID for **this** recipient: 3-11 ASCII alphanumeric characters, first character cannot be a number |

The API trims leading and trailing whitespace from each `to`, `message` and
`sender_id` before validation. Values that are blank after trimming, malformed
phone numbers, over-10-segment messages, and invalid senders return `400`.

## Response

The response shape is identical to [`/sms/send`](https://docs.23telecom.co.uk/sms/send): each accepted
recipient appears in both `messages` and `results` with its own `message_id`.
When a completed request is retried with the same `Idempotency-Key`, the API
returns the stored response, does not consume batch tokens again, and includes
`Idempotent-Replayed: true`.

```json title="200 OK"
{
  "status": true,
  "messages": [
    {"dnis": "+14155551234", "message_id": "api_42_1743667200123456789_a3f8b2c1d9e45f67", "segment_num": 1},
    {"dnis": "+447911123456", "message_id": "api_42_1743667200123456789_b7c4e8f1a2d3690b", "segment_num": 1}
  ],
  "results": [
    {"dnis": "+14155551234", "message_id": "api_42_1743667200123456789_a3f8b2c1d9e45f67", "segments": 1, "status": "accepted"},
    {"dnis": "+447911123456", "message_id": "api_42_1743667200123456789_b7c4e8f1a2d3690b", "segments": 1, "status": "accepted"}
  ],
  "summary": {
    "total_recipients": 2,
    "total_segments": 2,
    "total_cost": 0.02,
    "encoding": "GSM-7",
    "accepted_count": 2,
    "blocked_count": 0,
    "unpriced_count": 0,
    "queue_error_count": 0,
    "config_error_count": 0,
    "db_error_count": 0
  }
}
```

| Field | Description |
| --- | --- |
| `messages` | Accepted messages only (one entry per recipient) |
| `results` | **All** recipients with their individual status |
| `summary` | Totals: accepted, blocked, errors, cost and batch encoding. `encoding` is `GSM-7` or `UCS-2` when at least one recipient is accepted, and can be empty when no recipient is accepted |

### Per-recipient statuses

| `results[].status` | Meaning |
| --- | --- |
| `accepted` | Queued for delivery |
| `blocked_country` | Recipient's country is in your blocked list |
| `unpriced` | Legacy/sandbox non-acceptance status retained for compatibility. Current live pricing misses are accepted and queued with a provisional local cost |
| `queue_error` | Sandbox simulator storage failure (`sk_test_` only); live Redis enqueue failures stay accepted and are recovered from the durable outbox |
| `config_error` | SMS sending not configured on the account |
| `db_error` | The recipient's outbox row could not be written |

  Field validation is **atomic**: a single malformed entry (missing `to`,
  `message` or `sender_id`) rejects the **whole** batch with a `400` that names
  the offending index (e.g. `messages[42].message is required`) — nothing is
  sent. Fix the entry and resubmit with a fresh `Idempotency-Key`. Persistence
  is atomic for recipients that pass business checks: either all accepted
  recipients are queued, or none are. `blocked_country` recipients are not sent;
  valid live recipients remain accepted when local pricing is missing.

## Errors

| HTTP | Code | Description |
| --- | --- | --- |
| 400 | `INVALID_BODY` | Cannot parse request body |
| 400 | `INVALID_MESSAGES` | Missing or empty `messages` array |
| 400 | `IDEMPOTENCY_KEY_REQUIRED` / `INVALID_IDEMPOTENCY_KEY` | Missing or invalid `Idempotency-Key` header |
| 400 | `TOO_MANY_MESSAGES` | Over 1000 messages |
| 400 | `INVALID_TO` | A `messages[i].to` is missing or not E.164-style |
| 400 | `INVALID_MESSAGE` | A `messages[i].message` is missing or exceeds 10 SMS segments |
| 400 | `INVALID_SENDER` | A `messages[i].sender_id` is missing or not 3-11 ASCII alphanumeric characters |
| 403 | `NO_SMS_ACCESS` | SMS not enabled on your account |
| 403 | `CONFIG_ERROR` | SMS credentials incomplete — contact support |
| 403 | `WORKSPACE_NOT_AVAILABLE` | Target workspace was deleted — use a live workspace |
| 409 | `IDEMPOTENCY_KEY_REUSED` | Same key was already used with a different request body |
| 409 | `IDEMPOTENCY_REQUEST_IN_PROGRESS` | Same key is still processing; retry later with the same key |
| 429 | `RATE_LIMIT_EXCEEDED` | The batch exceeds your per-second message budget — see the note below |
| 500 | `DB_ERROR` | Could not queue (atomic rollback) — retry the whole request |
| 503 | `SANDBOX_UNAVAILABLE` / `RATE_LIMIT_UNAVAILABLE` / `QUEUE_UNAVAILABLE` | Sandbox simulator not enabled (`sk_test_`), rate limiter unavailable, or SMS queue unavailable — retry shortly with the same `Idempotency-Key` |

  New live processing does not create `PRICING_UNAVAILABLE` or a send-time `402`
  when Portal's local pricing or balance data is missing. A valid recipient
  is accepted with a `message_id`; the local cost can be provisional until the
  delivery report arrives. Never retry an accepted `message_id` solely because
  its immediate cost is `0`.

  Each message in the batch counts against your per-second message budget, so a
  single request consumes `messages.length` rate-limit tokens. The hard request
  cap is 1000 messages, but the effective max for your account is
  `min(1000, burst_limit)`. A batch larger than your account's burst limit is
  rejected with `429` — split it into smaller requests or ask support to raise
  your limit.

  Sandbox keys (`sk_test_`) validate the same request shape and return the same
  response fields, but they simulate delivery without live pricing, balance or
  blocked-country enforcement. Live keys can return `blocked_country`; missing
  Portal pricing or balance data does not reject a valid live recipient.

## Delivery tracking

The send response confirms **acceptance**, not delivery. Because every recipient
gets a distinct `message_id`, you can track each one precisely:

- **Webhooks (recommended):** receive a [delivery report](https://docs.23telecom.co.uk/webhooks/delivery) per
  recipient the moment the carrier reports it.
- **Per message:** call [GET /sms/status/:message_id](https://docs.23telecom.co.uk/sms/status) for any
  `message_id` returned above.
- **By phone:** call [GET /sms/messages](https://docs.23telecom.co.uk/sms/messages) with `phone=`, `from=` and
  `to=` to pull every message to a number over a date range and match results by
  recipient.