# SMS

Send SMS through the API and see the history, statistics, balance, templates, senders and numbers that do not receive.

Updated on 10 November 2026

## What you need

- An active SMS reseller account. Without it, the API answers `403` with the code `sms_reseller_required`.
- A key with the `sms:write` permission to send, and `sms:read` to look things up. See [Permissions](https://docs.interweb.ao/en/permissions.md).

## Sending

Use [`POST /v1/sms/messages`](https://docs.interweb.ao/en/sms.md) with the number and the text:

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/messages" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/messages');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
        'Content-Type: application/json',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
payload = {"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}
r = requests.post("https://api.interweb.ao/v1/sms/messages", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}),
});
console.log(response.status);
console.log(await response.text());
```


The number does not need the country code: the system can tell the country from the first digit, so `841234567` goes to Mozambique and `923456789` to Angola. It also works with the country code, with or without the `+` and with spaces: `+258 84 123 4567` and `258841234567` are the same number. If the number is not valid, the API responds with `422`, the code `validation_failed` and the reason `invalid_number` in `details.reason`.

Without `send_at`, the message goes out right away and the answer brings its real status: `sent` if it went out, `failed` if it failed, or `queued` if it has to wait its turn.

Send the `Idempotency-Key` header so a retry never sends twice. See [Retries](https://docs.interweb.ao/en/api-rules.md).

## The sender

The `sender` field is optional. If you do not send it, the message goes out with the free sender, **NETMSG**, which every account can use without asking.

To use another name, give an approved sender. The list is at [`GET /v1/sms/senders`](https://docs.interweb.ao/en/sms.md), with the free sender always first and the `free` field set to `true`.

## Sandbox and Production

- With a `sandbox_` key, or while the API is in preparation, the message is simulated: nothing is sent and no credits are used.
- With a `prod_` key, the message is sent and credits are taken from the SMS balance. The spending limit of the key, in currency, does not apply to these credits.

See [Sandbox and Production](https://docs.interweb.ao/en/api-rules.md).

## Scheduled messages

To send the message later, add the `send_at` field, with the date and time in ISO 8601 and the time zone, for example `2026-11-20T09:30:00+02:00` (Maputo time). The date must be in the future and at most 365 days from now. Otherwise, the API responds with `422` and the reason `invalid_send_at`, `send_at_in_past` or `send_at_too_far` in `details.reason`.

- The message gets the status `scheduled`, with the chosen time in `scheduled_for`.
- When you schedule, the API only checks that the balance is enough. The credits are only taken when the message goes out: until then, `charged` stays `false`, and [`GET /v1/sms/balance`](https://docs.interweb.ao/en/sms.md) shows in `scheduled_credits` the scheduled amount that has not been taken yet.
- If the balance is not enough at the chosen time, the message is not sent and ends up `failed`, with `error` set to `insufficient_credits`.

To cancel the message, use [`POST /v1/sms/messages/{id}/cancel`](https://docs.interweb.ao/en/sms.md) before the chosen time:

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
r = requests.post("https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
  },
});
console.log(response.status);
console.log(await response.text());
```


Only messages from your own account that are still `scheduled` can be cancelled. If the credits were already taken, they go back to the balance, and `credits_returned` says how many. If the message already went out, is going out or was already cancelled, the API responds with `409` and the reason `not_cancellable` in `details.reason`. With a `sandbox_` key, the API checks everything and changes nothing.

## See a message

Use [`GET /v1/sms/messages/{id}`](https://docs.interweb.ao/en/sms.md) with the identifier returned when sending. The response includes the status, the chosen time if the message is scheduled, and the `charged` field, which says whether the credits have already been taken. Simulated messages can only be seen with the key that created them.

## See the balance

Use [`GET /v1/sms/balance`](https://docs.interweb.ao/en/sms.md). The balance is real in both modes.

## See sent messages

[`GET /v1/sms/messages`](https://docs.interweb.ao/en/sms.md) returns the messages the account sent, in [pages](https://docs.interweb.ao/en/api-rules.md), with the `status`, `from`, `to` and `number` filters. The number comes masked. Account notices, verification codes and simulated messages are not listed.

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/messages" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/messages');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/messages", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/messages", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


## Statistics

[`GET /v1/sms/stats`](https://docs.interweb.ao/en/sms.md) gives the totals by status, the credits spent and one line per day with messages. Without dates, it counts the last 30 days; at most 366.

## Templates

- [`GET /v1/sms/templates`](https://docs.interweb.ao/en/sms.md) lists the templates of the account, and [`GET /v1/sms/templates/{id}`](https://docs.interweb.ao/en/sms.md) shows one.
- [`POST /v1/sms/templates`](https://docs.interweb.ao/en/sms.md) creates, [`PATCH /v1/sms/templates/{id}`](https://docs.interweb.ao/en/sms.md) changes and [`DELETE /v1/sms/templates/{id}`](https://docs.interweb.ao/en/sms.md) deletes.

They are the same templates as on the SMS page of the portal. A template sends nothing: it only keeps the text. The text accepts the variables `{$client_name}`, `{$client_first_name}`, `{$client_last_name}`, `{$client_company_name}`, `{$remetente}` and `{$data}`; any other variable answers `422`.

## New senders

[`POST /v1/sms/senders`](https://docs.interweb.ao/en/sms.md) makes the request, with the name, the entity, the country, the tax number, the purpose and a sample message. [`GET /v1/sms/senders/{sender}`](https://docs.interweb.ao/en/sms.md) says where it stands, by `id` or by name.

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/senders" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/senders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
        'Content-Type: application/json',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
payload = {"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}
r = requests.post("https://api.interweb.ao/v1/sms/senders", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/senders", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}),
});
console.log(response.status);
console.log(await response.text());
```


Our team reviews the request and handles the registration with the operators; approval depends on them. If it is rejected, `note` explains why and you can request it again with the data corrected. To add documents, make the request in the portal, at `documents_url`.

## Numbers that do not receive

[`POST /v1/sms/opt-outs`](https://docs.interweb.ao/en/sms.md) adds a number to the list of the account: from then on, sending to it answers `422` with `number_opted_out`. [`GET /v1/sms/opt-outs`](https://docs.interweb.ao/en/sms.md) shows the list. To take a number off, ask support.

## Help

For other questions, see the [Knowledge Base](https://www.meu.interweb.ao/knowledgebase?language=english&currency=2).


## Operations

16 operations in this area. Click an operation to see its parameters, examples and responses, or to try it in the Playground, without leaving the page.

### List sent messages

`GET https://api.interweb.ao/v1/sms/messages`

Needs `sms:read`. The messages this account sent, newest first. Account notices and verification codes are not listed, and messages simulated with a Sandbox key are not either. The number comes masked. If a message fails, `error` is `delivery_failed`. `charged` says whether the credits already left the balance.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `status` | query | string: scheduled, queued, sending, sent, simulated, failed, cancelled | no | Only this status. |
| `from` | query | string (date) | no | First day, `YYYY-MM-DD`. |
| `to` | query | string (date) | no | Last day, `YYYY-MM-DD`. |
| `number` | query | string | no | Only this number, with or without the country code. |
| `page` | query | integer | no | The page number. The first is 1. |
| `per_page` | query | integer | no | Results per page, from 1 to 100. The default is 10. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/messages" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/messages');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/messages", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/messages", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

List sent messages.

```json
{
  "data": [
    {
      "id": "128",
      "status": "sent",
      "simulated": false,
      "to": "25884****567",
      "sender": "NETMSG",
      "text": "A sua encomenda está pronta.",
      "parts": 1,
      "encoding": "GSM",
      "credits": 1.0,
      "campaign_id": null,
      "error": null,
      "scheduled_for": "2026-11-05T09:30:00+02:00",
      "created_at": "2026-11-05T09:30:00+02:00",
      "sent_at": "2026-11-05T09:30:04+02:00",
      "charged": true
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 10,
    "total": 1,
    "total_pages": 1
  }
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### Send an SMS

`POST https://api.interweb.ao/v1/sms/messages`

Needs `sms:write` and an active SMS reseller account. With a Sandbox key, or while the API is in preparation, the message is simulated: nothing is sent and no credits are used. With a Production key the message enters the queue and credits are taken from the SMS balance (the key spending limit, in currency, does not apply to these credits). Send `Idempotency-Key` so a retry never sends twice. Without `send_at` the message goes out right away and the answer brings its real status: `sent`, `failed`, or `queued` if it has to wait its turn. With `send_at` it stays `scheduled`: the balance is only checked now and the credits leave it when the message goes out (`charged` is `false` until then); if the balance is short at that time, the message is not sent and ends up `failed` with `error` set to `insufficient_credits`. A scheduled message can be cancelled with POST /v1/sms/messages/{id}/cancel.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:write`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | no | Any unique value, up to 255 visible characters. Valid for 24 hours per key. Repeating the same request returns the saved answer; a different body, or a first request still in progress, returns 409. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `number` | string | yes | With or without the country code, spaces allowed: +258841234567, 258 84 123 4567 or 84 123 4567. Mozambican and Angolan mobile numbers are recognised on their own; any other number without a code uses the country of your account. |
| `text` | string | yes |  |
| `sender` | string | no | Optional. An approved sender; if omitted, the free sender (see GET /v1/sms/senders). |
| `send_at` | string (date-time) | no | Optional. Schedules the message: ISO 8601 date and time with the time zone, such as `2026-11-20T09:30:00+02:00`. It must be in the future and at most 365 days from now. Without it, the message goes out right away. |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/messages" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/messages');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
        'Content-Type: application/json',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
payload = {"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}
r = requests.post("https://api.interweb.ao/v1/sms/messages", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"number": "+258841234567", "text": "Your order is ready.", "send_at": "2026-11-20T09:30:00+02:00"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

The message: sent, simulated, scheduled or waiting in the queue.

```json
{
  "data": {
    "id": "412",
    "status": "scheduled",
    "simulated": false,
    "to": "25884****567",
    "sender": "NETMSG",
    "parts": 1,
    "encoding": "GSM",
    "credits": 1.0,
    "charged": false,
    "mode": "production",
    "error": null,
    "scheduled_for": "2026-11-20T09:30:00+02:00",
    "created_at": "2026-11-10T12:00:00+02:00",
    "sent_at": null
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.id` | string | A number for messages in the queue, a UUID for simulated ones. |
| `data.status` | string: scheduled, queued, sending, sent, simulated, failed, cancelled | `scheduled`: waiting for the time in `scheduled_for`, and it can still be cancelled. `queued`: waiting its turn to go out. |
| `data.simulated` | boolean | True when no real SMS goes out. |
| `data.to` | string | The number, partly hidden. |
| `data.credits` | number | What the message costs in credits. See `charged` to know whether they already left the SMS balance. Always 0 when simulated by the API. |
| `data.would_cost` | number | Only on simulated messages: what a real send would cost. |
| `data.error` | string, null | `delivery_failed` when it could not be delivered; `insufficient_credits` when a scheduled message did not have enough balance when it was due to go out. |
| `data.charged` | boolean | True when the credits have left the SMS balance. A scheduled message is only charged when it goes out; failed, cancelled and simulated messages give the credits back. |
| `data.scheduled_for` | string, null (date-time) | The time chosen in `send_at`, while the message is scheduled or after it was cancelled; otherwise `null`. |

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |
| `X-API-Simulated` | Present and `true` when this write was simulated: nothing changed and nothing was charged. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `409`

The Idempotency-Key was used with a different request, or the first request is still in progress.

```json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was already used with a different request, or the first request is still in progress.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "different_request"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `sms_reseller_required`, `insufficient_scope`, `forbidden` | This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`). |
| 409 | `idempotency_conflict` | The Idempotency-Key was used with a different request, or the first request is still in progress. |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### See one SMS

`GET https://api.interweb.ao/v1/sms/messages/{id}`

Needs `sms:read`. Simulated messages can only be seen with the key that created them.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The `id` returned when sending. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

The message.

```json
{
  "data": {
    "id": "7f0c9d2e-3a51-4c7e-9b1a-2d4f6a8b0c11",
    "status": "simulated",
    "simulated": true,
    "to": "25884****567",
    "sender": "NETMSG",
    "parts": 1,
    "encoding": "GSM",
    "would_cost": 1,
    "mode": "sandbox",
    "credits": 0,
    "created_at": "2026-11-04T10:00:00+02:00",
    "error": null,
    "sent_at": null,
    "charged": false,
    "scheduled_for": null
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.id` | string | A number for messages in the queue, a UUID for simulated ones. |
| `data.status` | string: scheduled, queued, sending, sent, simulated, failed, cancelled | `scheduled`: waiting for the time in `scheduled_for`, and it can still be cancelled. `queued`: waiting its turn to go out. |
| `data.simulated` | boolean | True when no real SMS goes out. |
| `data.to` | string | The number, partly hidden. |
| `data.credits` | number | What the message costs in credits. See `charged` to know whether they already left the SMS balance. Always 0 when simulated by the API. |
| `data.would_cost` | number | Only on simulated messages: what a real send would cost. |
| `data.error` | string, null | `delivery_failed` when it could not be delivered; `insufficient_credits` when a scheduled message did not have enough balance when it was due to go out. |
| `data.charged` | boolean | True when the credits have left the SMS balance. A scheduled message is only charged when it goes out; failed, cancelled and simulated messages give the credits back. |
| `data.scheduled_for` | string, null (date-time) | The time chosen in `send_at`, while the message is scheduled or after it was cancelled; otherwise `null`. |

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `404`

Not found.

```json
{
  "error": {
    "code": "not_found",
    "message": "Not found.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `sms_reseller_required`, `insufficient_scope`, `forbidden` | This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`). |
| 404 | `not_found` | Not found. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### See the SMS balance

`GET https://api.interweb.ao/v1/sms/balance`

Needs `sms:read`. The balance is real in both modes.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/balance" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/balance');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/balance", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/balance", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Balance and daily limit.

```json
{
  "data": {
    "balance": 50,
    "unit": "credits",
    "daily_limit": 200,
    "sent_today": 3,
    "mode": "sandbox",
    "scheduled_credits": 0.0
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.scheduled_credits` | number | Credits of scheduled messages that were not charged yet. They are still in `balance` and leave it when each message goes out. |

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `sms_reseller_required`, `insufficient_scope`, `forbidden` | This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`). |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### List the SMS senders

`GET https://api.interweb.ao/v1/sms/senders`

Needs `sms:read`. The free sender always comes first, with `free: true`.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/senders" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/senders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/senders", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/senders", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

The senders of this account.

```json
{
  "data": [
    {
      "sender": "NETMSG",
      "status": "approved",
      "free": true
    },
    {
      "sender": "MINHAEMPRESA",
      "status": "pending",
      "free": false
    }
  ],
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `sms_reseller_required`, `insufficient_scope`, `forbidden` | This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`). |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### Request a sender

`POST https://api.interweb.ao/v1/sms/senders`

Needs `sms:write`. Up to 11 letters, numbers and spaces, at most 30 in the account. The team reviews the request and registers it with the operators; approval depends on them. After a rejection, send it again with the data corrected. Documents are sent in the account portal, when the request is made there. Limited to 20 changes per minute.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:write`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |
| `Idempotency-Key` | header | string | no | A unique identifier for this request, so it can be repeated without duplicating it. |

#### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `sender` | string | yes |  |
| `entity` | string | yes |  |
| `country` | string | yes |  |
| `tax_id` | string | yes |  |
| `purpose` | string | yes |  |
| `sample` | string | yes |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/senders" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/senders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
        'Content-Type: application/json',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
payload = {"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}
r = requests.post("https://api.interweb.ao/v1/sms/senders", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/senders", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"sender": "AMINHAMARCA", "entity": "A Minha Empresa, Lda", "country": "Moçambique", "tax_id": "123456789", "purpose": "Avisos de encomenda aos nossos clientes.", "sample": "A sua encomenda está pronta."}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Request a sender.

```json
{
  "data": {
    "id": 7,
    "sender": "AMINHAMARCA",
    "status": "pending",
    "free": false,
    "entity": "A Minha Empresa, Lda",
    "country": "Moçambique",
    "purpose": "Avisos de encomenda aos nossos clientes.",
    "sample": "A sua encomenda está pronta.",
    "note": null,
    "documents": 0,
    "documents_url": "https://my.example.com/sms-center.php?view=senders&language=portuguese&currency=5",
    "created_at": "2026-11-05T09:30:00+02:00",
    "updated_at": "2026-11-05T09:30:00+02:00",
    "simulated": true
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `409`

The operation cannot be done in the current state. See `details.reason`.

```json
{
  "error": {
    "code": "conflict",
    "message": "This operation only works on an active domain.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "domain_not_active"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 409 | `conflict` | The operation cannot be done in the current state. See `details.reason`. |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### See sending statistics

`GET https://api.interweb.ao/v1/sms/stats`

Needs `sms:read`. Totals and one line per day with messages. Without dates, the last 30 days; at most 366 days. Failed, cancelled and simulated messages do not count in `credits_spent`. Scheduled messages that were not charged yet do not count either.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `from` | query | string (date) | no | First day, `YYYY-MM-DD`. |
| `to` | query | string (date) | no | Last day, `YYYY-MM-DD`. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/stats" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/stats');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/stats", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/stats", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

See sending statistics.

```json
{
  "data": {
    "from": "2026-10-07",
    "to": "2026-11-05",
    "messages": 12,
    "parts": 14,
    "credits_spent": 13.0,
    "by_status": {
      "scheduled": 0,
      "queued": 0,
      "sending": 0,
      "sent": 11,
      "simulated": 0,
      "failed": 1,
      "cancelled": 0
    },
    "by_day": [
      {
        "date": "2026-11-05",
        "messages": 12,
        "sent": 11,
        "failed": 1,
        "credits": 13.0
      }
    ]
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### List templates

`GET https://api.interweb.ao/v1/sms/templates`

Needs `sms:read`. The templates of this account, by name.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `category` | query | string: general, notice, promotion, billing, confirmation | no | Only this category. |
| `language` | query | string: pt, en | no | Only this language. |
| `page` | query | integer | no | The page number. The first is 1. |
| `per_page` | query | integer | no | Results per page, from 1 to 100. The default is 10. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/templates" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/templates');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/templates", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/templates", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

List templates.

```json
{
  "data": [
    {
      "id": 41,
      "name": "Aviso de factura",
      "text": "Olá {$client_first_name}, a sua factura vence hoje.",
      "category": "notice",
      "language": "pt",
      "created_at": "2026-11-05T09:30:00+02:00",
      "updated_at": "2026-11-05T09:30:00+02:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 10,
    "total": 1,
    "total_pages": 1
  }
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### Create a template

`POST https://api.interweb.ao/v1/sms/templates`

Needs `sms:write`. The name must be unique in the account. The text accepts `{$client_name}`, `{$client_first_name}`, `{$client_last_name}`, `{$client_company_name}`, `{$remetente}` and `{$data}`. Limited to 20 changes per minute.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:write`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |
| `Idempotency-Key` | header | string | no | A unique identifier for this request, so it can be repeated without duplicating it. |

#### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |
| `text` | string | yes |  |
| `category` | string: general, notice, promotion, billing, confirmation | no |  |
| `language` | string: pt, en | no |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/templates" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"name": "Aviso de factura", "text": "Olá {$client_first_name}, a sua factura vence hoje.", "category": "notice", "language": "pt"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/templates');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"name": "Aviso de factura", "text": "Olá {$client_first_name}, a sua factura vence hoje.", "category": "notice", "language": "pt"}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
        'Content-Type: application/json',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
payload = {"name": "Aviso de factura", "text": "Olá {$client_first_name}, a sua factura vence hoje.", "category": "notice", "language": "pt"}
r = requests.post("https://api.interweb.ao/v1/sms/templates", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/templates", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"name": "Aviso de factura", "text": "Olá {$client_first_name}, a sua factura vence hoje.", "category": "notice", "language": "pt"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Create a template.

```json
{
  "data": {
    "id": 41,
    "name": "Aviso de factura",
    "text": "Olá {$client_first_name}, a sua factura vence hoje.",
    "category": "notice",
    "language": "pt",
    "created_at": "2026-11-05T09:30:00+02:00",
    "updated_at": "2026-11-05T09:30:00+02:00",
    "simulated": true
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `409`

The operation cannot be done in the current state. See `details.reason`.

```json
{
  "error": {
    "code": "conflict",
    "message": "This operation only works on an active domain.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "domain_not_active"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 409 | `conflict` | The operation cannot be done in the current state. See `details.reason`. |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### See a template

`GET https://api.interweb.ao/v1/sms/templates/{id}`

Needs `sms:read`. A template of this account.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The `id` of the template. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

See a template.

```json
{
  "data": {
    "id": 41,
    "name": "Aviso de factura",
    "text": "Olá {$client_first_name}, a sua factura vence hoje.",
    "category": "notice",
    "language": "pt",
    "created_at": "2026-11-05T09:30:00+02:00",
    "updated_at": "2026-11-05T09:30:00+02:00"
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `404`

Not found.

```json
{
  "error": {
    "code": "not_found",
    "message": "Not found.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 404 | `not_found` | Not found. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### Change a template

`PATCH https://api.interweb.ao/v1/sms/templates/{id}`

Needs `sms:write`. Send only the fields to change. Limited to 20 changes per minute.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:write`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The `id` of the template. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `text` | string | no |  |
| `category` | string: general, notice, promotion, billing, confirmation | no |  |
| `language` | string: pt, en | no |  |

#### Examples

**curl**

```bash
curl -X PATCH "https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Content-Type: application/json" \
  -d '{"text": "Olá {$client_first_name}, a sua factura vence amanhã."}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_POSTFIELDS => '{"text": "Olá {$client_first_name}, a sua factura vence amanhã."}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Content-Type: application/json',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
payload = {"text": "Olá {$client_first_name}, a sua factura vence amanhã."}
r = requests.patch("https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"text": "Olá {$client_first_name}, a sua factura vence amanhã."}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Change a template.

```json
{
  "data": {
    "id": 41,
    "name": "Aviso de factura",
    "text": "Olá {$client_first_name}, a sua factura vence hoje.",
    "category": "notice",
    "language": "pt",
    "created_at": "2026-11-05T09:30:00+02:00",
    "updated_at": "2026-11-05T09:30:00+02:00",
    "simulated": true
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `404`

Not found.

```json
{
  "error": {
    "code": "not_found",
    "message": "Not found.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `409`

The operation cannot be done in the current state. See `details.reason`.

```json
{
  "error": {
    "code": "conflict",
    "message": "This operation only works on an active domain.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "domain_not_active"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 404 | `not_found` | Not found. |
| 409 | `conflict` | The operation cannot be done in the current state. See `details.reason`. |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### Delete a template

`DELETE https://api.interweb.ao/v1/sms/templates/{id}`

Needs `sms:write`. Standard templates that are deleted do not come back. Limited to 20 changes per minute.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:write`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The `id` of the template. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl -X DELETE "https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.delete("https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/templates/00000000-0000-4000-8000-000000000000", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Delete a template.

```json
{
  "data": {
    "id": 41,
    "deleted": false,
    "simulated": true
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `404`

Not found.

```json
{
  "error": {
    "code": "not_found",
    "message": "Not found.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 404 | `not_found` | Not found. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### See a sender request

`GET https://api.interweb.ao/v1/sms/senders/{sender}`

Needs `sms:read`. By `id` or by name. `note` is the note from our team. The free sender always answers as `approved`.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `sender` | path | string | yes | The `id` of the request or the name of the sender. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/senders/VALUE" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/senders/VALUE');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/senders/VALUE", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/senders/VALUE", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

See a sender request.

```json
{
  "data": {
    "id": 7,
    "sender": "AMINHAMARCA",
    "status": "pending",
    "free": false,
    "entity": "A Minha Empresa, Lda",
    "country": "Moçambique",
    "purpose": "Avisos de encomenda aos nossos clientes.",
    "sample": "A sua encomenda está pronta.",
    "note": null,
    "documents": 0,
    "documents_url": "https://my.example.com/sms-center.php?view=senders&language=portuguese&currency=5",
    "created_at": "2026-11-05T09:30:00+02:00",
    "updated_at": "2026-11-05T09:30:00+02:00"
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `404`

Not found.

```json
{
  "error": {
    "code": "not_found",
    "message": "Not found.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 404 | `not_found` | Not found. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### List numbers that do not receive

`GET https://api.interweb.ao/v1/sms/opt-outs`

Needs `sms:read`. The numbers this account blocked, masked. Use `number` to check one.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:read`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `number` | query | string | no | Only this number, with or without the country code. |
| `page` | query | integer | no | The page number. The first is 1. |
| `per_page` | query | integer | no | Results per page, from 1 to 100. The default is 10. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl "https://api.interweb.ao/v1/sms/opt-outs" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/opt-outs');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
}
r = requests.get("https://api.interweb.ao/v1/sms/opt-outs", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/opt-outs", {
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

List numbers that do not receive.

```json
{
  "data": [
    {
      "number": "25884****567",
      "created_at": "2026-11-05T09:30:00+02:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 10,
    "total": 1,
    "total_pages": 1
  }
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### Stop sending to a number

`POST https://api.interweb.ao/v1/sms/opt-outs`

Needs `sms:write`. From now on, sending to this number answers `422` with `number_opted_out`. To take a number off the list, ask support. Limited to 20 changes per minute.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:write`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |
| `Idempotency-Key` | header | string | no | A unique identifier for this request, so it can be repeated without duplicating it. |

#### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `number` | string | yes |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/opt-outs" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"number": "+258841234567"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/opt-outs');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"number": "+258841234567"}',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
        'Content-Type: application/json',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
payload = {"number": "+258841234567"}
r = requests.post("https://api.interweb.ao/v1/sms/opt-outs", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/opt-outs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"number": "+258841234567"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Stop sending to a number.

```json
{
  "data": {
    "number": "25884****567",
    "created_at": "2026-11-05T09:30:00+02:00",
    "already_listed": false,
    "simulated": true
  },
  "meta": {}
}
```

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `422`

Some of the data is not valid.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Some of the data is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `forbidden`, `insufficient_scope`, `ip_not_allowed` | The account cannot use the API (`forbidden`), the key lacks the permission (`insufficient_scope`), or the IP address is not on the list of the key (`ip_not_allowed`). |
| 422 | `validation_failed` | Some of the data is not valid. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).


### Cancel a scheduled SMS

`POST https://api.interweb.ao/v1/sms/messages/{id}/cancel`

Needs `sms:write`. Only messages of this account that are still `scheduled` can be cancelled. If the credits were already charged they go back to the balance, and `credits_returned` says how many; a scheduled message is usually only charged when it goes out, so there is often nothing to return. A message that already went out, is going out, or was already cancelled returns `409` with `details.reason` set to `not_cancellable`. With a Sandbox key, or while the API is in preparation, it checks everything and changes nothing. Send `Idempotency-Key` so a retry gives the same answer.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `sms:write`.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The `id` returned when sending. |
| `Idempotency-Key` | header | string | no | Any unique value, up to 255 visible characters. Valid for 24 hours per key. Repeating the same request returns the saved answer; a different body, or a first request still in progress, returns 409. |
| `X-API-Mode` | header | string: sandbox, production | no | Optional. If it does not match the mode of the key, the request is refused with `invalid_request` and reason `mode_mismatch`. Use it to make sure a program never runs with the wrong key. |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RLX_API_KEY'),
        'Accept-Language: en',
        'Idempotency-Key: 9f1c2d3e-example',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status, PHP_EOL, $body, PHP_EOL;
```

**Python**

```python
import os
import requests

headers = {
    "Authorization": "Bearer " + os.environ["RLX_API_KEY"],
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
}
r = requests.post("https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.ao/v1/sms/messages/00000000-0000-4000-8000-000000000000/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
  },
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Cancel a scheduled SMS.

```json
{
  "data": {
    "id": "412",
    "status": "cancelled",
    "simulated": false,
    "to": "25884****567",
    "sender": "NETMSG",
    "parts": 1,
    "encoding": "GSM",
    "credits": 1.0,
    "charged": false,
    "mode": "production",
    "error": null,
    "scheduled_for": "2026-11-20T09:30:00+02:00",
    "created_at": "2026-11-10T12:00:00+02:00",
    "sent_at": null,
    "credits_returned": 0.0
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.id` | string | A number for messages in the queue, a UUID for simulated ones. |
| `data.status` | string: scheduled, queued, sending, sent, simulated, failed, cancelled | `scheduled`: waiting for the time in `scheduled_for`, and it can still be cancelled. `queued`: waiting its turn to go out. |
| `data.simulated` | boolean | True when no real SMS goes out. |
| `data.to` | string | The number, partly hidden. |
| `data.credits` | number | What the message costs in credits. See `charged` to know whether they already left the SMS balance. Always 0 when simulated by the API. |
| `data.would_cost` | number | Only on simulated messages: what a real send would cost. |
| `data.error` | string, null | `delivery_failed` when it could not be delivered; `insufficient_credits` when a scheduled message did not have enough balance when it was due to go out. |
| `data.charged` | boolean | True when the credits have left the SMS balance. A scheduled message is only charged when it goes out; failed, cancelled and simulated messages give the credits back. |
| `data.scheduled_for` | string, null (date-time) | The time chosen in `send_at`, while the message is scheduled or after it was cancelled; otherwise `null`. |
| `data.credits_returned` | number | Credits that went back to the SMS balance with this cancellation. |

| Header | Description |
|---|---|
| `X-Correlation-ID` | Identifier of this request. Give it to support when you report a problem. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute ends, as a Unix timestamp in seconds. |
| `X-API-Mode` | The mode of the key that made the request: `sandbox` or `production`. |
| `X-API-Simulated` | Present and `true` when this write was simulated: nothing changed and nothing was charged. |

##### `401`

The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `403`

This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`).

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This key does not have the required permission.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "required": "dns:read"
    }
  }
}
```

##### `404`

Not found.

```json
{
  "error": {
    "code": "not_found",
    "message": "Not found.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `409`

The operation cannot be done in the current state. See `details.reason`.

```json
{
  "error": {
    "code": "conflict",
    "message": "This operation only works on an active domain.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "domain_not_active"
    }
  }
}
```

##### `429`

Too many requests. Wait for the time in Retry-After.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please try again shortly.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "retry_after": 42
    }
  }
}
```

| Header | Description |
|---|---|
| `Retry-After` | Seconds to wait before trying again. |

##### `500`

An internal error occurred. Give the correlation_id to support.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An internal error occurred. Please give the correlation_id to support.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `503`

The API is turned off.

```json
{
  "error": {
    "code": "disabled",
    "message": "The API is currently turned off.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```


#### Errors

| Status | Code | Meaning |
|---|---|---|
| 401 | `unauthorized` | The key is missing, invalid, revoked or expired, or the user who created it no longer has access to the account. |
| 403 | `sms_reseller_required`, `insufficient_scope`, `forbidden` | This account does not have an active SMS reseller plan (`sms_reseller_required`), the key lacks the permission (`insufficient_scope`), or the account cannot use the API (`forbidden`). |
| 404 | `not_found` | Not found. |
| 409 | `conflict` | The operation cannot be done in the current state. See `details.reason`. |
| 429 | `rate_limited` | Too many requests. Wait for the time in Retry-After. |
| 500 | `internal_error` | An internal error occurred. Give the correlation_id to support. |
| 503 | `disabled` | The API is turned off. |

The error format is explained in [Errors](https://docs.interweb.ao/en/api-rules.md).
