# API overview

> Base URLs, authentication, the two response conventions, error shapes, and how billing and delivery actually work — read this before writing a client.

The Sendai API is plain JSON over HTTPS, authenticated with an API key. This page covers the
conventions that apply to every endpoint; each operation then has its own page with the exact
request and response.

## Base URLs

| Host | Use |
| --- | --- |
| `https://api.sendai.co.zw` | Production |
| `https://staging-api.sendai.co.zw` | Staging — a build target for integration work |

All paths in this reference are relative to the production host.

## Authentication

One mechanism: an **API key** as a bearer token on every request.

```http
Authorization: Bearer sk_live_4c8e…
```

Create keys in the dashboard under **Settings → Developer**, or with
[`POST /api/v1/api-tokens`](/api/api-tokens). A key is valid until you revoke it. Full detail,
including zero-downtime rotation, is in [Authentication](/guides/authentication).

## Two response conventions

This is the one thing to get right before you write a client. **`POST /api/v1/sms` returns the
message object bare**, with no wrapper:

```json
{
  "id": "0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10",
  "to": "263771000000",
  "status": "enqueued",
  "delivered_at": null
}
```

**Every other endpoint wraps its payload** in an envelope, with the payload under `data`:

```json
{
  "status": "success",
  "message": "request successful",
  "data": { }
}
```

So `GET /api/v1/sms/{id}` gives you `data.status`, while `POST /api/v1/sms` gives you `status`
at the top level. Unwrapping `.data` on a send returns `undefined`.

## Errors

Branch on the HTTP status code — the error body comes in two shapes depending on the endpoint.
The two SMS read/write endpoints (`POST /api/v1/sms`, `GET /api/v1/sms/{id}`) return an
`error.type`:

```json
{
  "status": "",
  "message": "not found",
  "error": { "type": "err_not_found", "description": "the resource does not exist" }
}
```

…while bulk sends, the key endpoints, and every authentication failure return an `error.code`:

```json
{
  "status": "error",
  "message": "missing authentication",
  "error": { "code": "UNAUTHORIZED", "description": "missing authentication" }
}
```

| Status | Meaning |
| --- | --- |
| `400` | Malformed body, invalid id, or a `from` that is not an approved sender ID. Validation failures list fields in `error.validation_errors`. |
| `401` | Missing, malformed, or revoked API key. |
| `404` | No such resource on your account. |
| `422` | `insufficient balance` — your prepaid balance does not cover the send (documented on the [bulk operation](/api/sms#send-bulk-sms)). |

## Sender IDs

The `from` on a message is a **sender ID** — the name the recipient sees. Sender IDs are
approved before use, so you cannot invent one at send time; an unapproved value is rejected
with `400 unknown or unapproved sender id`. Yours are captured at registration and approved on
the network-operator side, usually in about 30 minutes. Manage them in the dashboard, or list
them over the API with [Sender IDs](/api/sender-ids).

## Billing and segments

Accounts are **prepaid**. Each send places a hold on your balance, and a send that your balance
will not cover is rejected outright — never half-delivered. Check what you hold with
[Balances](/api/balances).

SMS is billed **per segment**, not per message:

| Encoding | When | Single segment | Per segment when concatenated |
| --- | --- | --- | --- |
| GSM-7 | Every character is in the GSM-7 alphabet | 160 chars | 153 chars |
| UCS-2 | **Any** character outside GSM-7 (an emoji, a curly quote, an accent) | 70 chars | 67 chars |

One stray character switches the *whole* message to UCS-2 and can more than double its cost.

The API returns the amount held as `charge`, in ten-thousandths of `charge_currency`
(`10000` == 1.00, so `450` == 0.045). Note the type differs by endpoint: `POST /api/v1/sms`
returns `charge` as a **string** (`"450"`), while `POST /api/v1/sms/bulk` returns it as an
**integer** (`450`). Coerce on the way in.

## Knowing whether it arrived

A `202` means Sendai accepted and queued your message — not that it arrived. Two fields track
the journey:

- **`status`** — `created` → `enqueued` → `processing` → `success`, or `failed`.
- **`delivered_at`** — `null` until the carrier confirms delivery, then a timestamp.

`status: success` means the send pipeline completed. **Confirmed handset delivery is
`delivered_at` being non-null** — that is the only delivery signal the API exposes. Carrier
confirmation is asynchronous and can take seconds to hours, so poll
[`GET /api/v1/sms/{id}`](/api/sms#retrieve-an-sms), or register a [webhook](/guides/webhooks) and let
Sendai call you.

## The resources

The reference is organised by resource — one page each, with the object first and one
section per operation:

| Operation | Call |
| --- | --- |
| [Send an SMS](/api/sms#send-an-sms) | `POST /api/v1/sms` |
| [Retrieve an SMS](/api/sms#retrieve-an-sms) | `GET /api/v1/sms/{id}` |
| [Send bulk SMS](/api/sms#send-bulk-sms) | `POST /api/v1/sms/bulk` |
| [API tokens](/api/api-tokens) | `POST` / `GET` / `DELETE /api/v1/api-tokens` |
| [Balances](/api/balances) | `GET /api/v1/accounts/{id}/balances` |
| [Sender IDs](/api/sender-ids) | `GET /api/v1/accounts/{id}/channel-identities` |

Prefer a machine-readable contract? [Download the OpenAPI document](/openapi.yaml) — the
same operations, ready for codegen and tooling.
