# Transactional email

Send email from your application through the API, with verified domains, templates, a log of every delivery, a suppression list, statistics and webhooks.

Updated on 12 November 2026

## What you need

- Email Center active on your account. Turn it on in the portal, on the Transactional email page. Without it, the API responds with `403`, the code `forbidden` and the reason `email_account_required` in `details.reason`.
- A key with the `email_center:write` permission to send and make changes, and `email_center:read` to look things up. See [Permissions](https://docs.interweb.co.mz/en/permissions.md). The `email:read` and `email:write` permissions are different: they are for the mailboxes of a hosting service.
- A verified sender domain. You can only send from addresses of that domain or of a subdomain.

## The sender domain

Add the domain with [`POST /v1/email/domains`](https://docs.interweb.co.mz/en/email-center.md):

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/domains" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/domains');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"domain": "example.com"}',
    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 = {"domain": "example.com"}
r = requests.post("https://api.interweb.co.mz/v1/email/domains", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/domains", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"domain": "example.com"}),
});
console.log(response.status);
console.log(await response.text());
```


The response has, in `dns_records`, the records to add to the DNS of the domain:

- three DKIM `CNAME` records, which sign your emails (required);
- an SPF `TXT` record, which authorises our sending (recommended);
- a DMARC `TXT` record, the policy of the domain (recommended).

Once they are in place, call [`POST /v1/email/domains/{domain}/verify`](https://docs.interweb.co.mz/en/email-center.md). DNS can take a few hours to spread. When the DKIM records are live, the domain becomes `verified`. The `found` field of each record says whether it has been found yet.

If you want the return path to use your domain too, set a subdomain with [`PUT /v1/email/domains/{domain}/mail-from`](https://docs.interweb.co.mz/en/email-center.md), for example `bounce`. An extra `MX` and `TXT` record then appear for you to add.

## Sending

Use [`POST /v1/email/messages`](https://docs.interweb.co.mz/en/email-center.md) with the sender, the recipients, the subject and the content in `html`, in `text` or in both:

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/messages" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/messages');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}',
    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 = {"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}
r = requests.post("https://api.interweb.co.mz/v1/email/messages", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/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({"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}),
});
console.log(response.status);
console.log(await response.text());
```


- `to`, `cc` and `bcc` take one address, a comma-separated list or an array. At most 50 recipients per email.
- Each recipient counts as one email and uses one credit.
- Without `send_at`, the email goes out right away and the response has its real status.
- Attachments go in `attachments`, in base64, up to 5 MB in total. Executable files are refused.

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

## Templates and variables

Create templates with [`POST /v1/email/templates`](https://docs.interweb.co.mz/en/email-center.md), with the subject and the HTML or the text. Write variables as `{{name}}`.

When sending, give the `template_id` and the values in `variables`. The subject, `html` and `text` you leave out come from the template. In the HTML, values are escaped, so a name with `<` or `&` shows as text.

## Scheduled and batch sending

To send the email later, add `send_at` in ISO 8601 with the time zone, for example `2026-11-20T09:30:00+02:00`, up to 30 days ahead. Credits are only taken when the email goes out. To change your mind, use [`POST /v1/email/messages/{id}/cancel`](https://docs.interweb.co.mz/en/email-center.md) before that time.

For many emails at once, use [`POST /v1/email/messages/batch`](https://docs.interweb.co.mz/en/email-center.md). There is no limit on the number of messages, only on the request size (8 MB). Fields outside `messages` apply to all of them, and each message has its own `to` and `variables`. `results` says what happened to each one.

## Statuses and events

[`GET /v1/email/messages/{id}`](https://docs.interweb.co.mz/en/email-center.md) shows the status and the events of each recipient:

- `sent`: it went out to the receiving servers;
- `delivered`: the recipient's server accepted it;
- `bounced`: it bounced. If the bounce is permanent, the address joins the suppression list;
- `complained`: the recipient marked it as spam, and the address joins the suppression list;
- `failed`: it did not go out, and the credits go back to the balance.

The content of emails is kept for 30 days. After that, `html` and `text` are `null` and only the log remains.

## Suppressions

Addresses on the suppression list never receive your emails: they are removed before sending, are not charged, and are listed in `suppressed` in the response. If no recipient is left, the API responds with `422` and the reason `all_recipients_suppressed`.

See the list with [`GET /v1/email/suppressions`](https://docs.interweb.co.mz/en/email-center.md), add addresses with [`POST /v1/email/suppressions`](https://docs.interweb.co.mz/en/email-center.md) and remove them with [`DELETE /v1/email/suppressions/{email}`](https://docs.interweb.co.mz/en/email-center.md), with the address URL-encoded in the path (`ana%40example.org`).

## Webhook notifications

Save an HTTPS address with [`PUT /v1/email/webhook`](https://docs.interweb.co.mz/en/email-center.md) and choose the events: `email.sent`, `email.delivered`, `email.bounced`, `email.complained` and `email.failed`. This webhook is different from the SMS webhook.

Each notification is signed in the same way as the SMS ones, in the `X-Webhook-Signature` header. See [the SMS webhook](https://docs.interweb.co.mz/en/sms.md) to check the signature.

## Sandbox and Production

- With a `sandbox_` key, nothing is sent or charged. The email is `simulated`, with the simulated events. Other changes (domains, templates, suppressions) are only simulated.
- To try bounces and complaints, send to `bounce@example.com` or `complaint@example.com`.
- While Email Center is in test mode, `prod_` keys also simulate: `GET /v1/email/balance` tells you so in `test_mode`.

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

## SMTP Relay

In the portal, on the SMTP page, you create usernames and passwords to send from your own program or server, on ports 587 (STARTTLS) and 465 (TLS). The password is shown only once. The SMTP server is still being set up: until then, use the API.

## Email marketing

Besides transactional email, Email Center sends campaigns to your contacts: the SMS contacts that have an email address and the email-only contacts. Each recipient uses one credit, like any other email.

1. Add contacts with [`POST /v1/email/contacts`](https://docs.interweb.co.mz/en/email-center.md), with their groups in `groups`. The groups are the same as for SMS contacts: see them at [`GET /v1/email/groups`](https://docs.interweb.co.mz/en/email-center.md).
2. Create the campaign with [`POST /v1/email/campaigns`](https://docs.interweb.co.mz/en/email-center.md), with the blocks in `blocks` (`header`, `text`, `image`, `button`, `divider` and `footer`) or with a ready template in `template`. If you like, ask the AI for a proposal first with [`POST /v1/email/campaigns/generate`](https://docs.interweb.co.mz/en/email-center.md).
3. Choose the audience in `target`: groups, who opened or clicked before, and the date they joined.
4. Check the result of [`GET /v1/email/campaigns/{id}/spam-check`](https://docs.interweb.co.mz/en/email-center.md) and send a test with [`POST /v1/email/campaigns/{id}/test`](https://docs.interweb.co.mz/en/email-center.md).
5. Send it now with [`POST /v1/email/campaigns/{id}/send`](https://docs.interweb.co.mz/en/email-center.md) or schedule it with [`POST /v1/email/campaigns/{id}/schedule`](https://docs.interweb.co.mz/en/email-center.md).

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/campaigns" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"name": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"name": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}',
    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": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}
r = requests.post("https://api.interweb.co.mz/v1/email/campaigns", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/campaigns", {
  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": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}),
});
console.log(response.status);
console.log(await response.text());
```


In the texts you can use `{{first_name}}`, `{{name}}`, `{{company}}` and `{{email}}` of each contact.

### Subscription and unsubscribing

Every campaign email ends with the address and an unsubscribe link, and carries the `List-Unsubscribe` header so email programs show their unsubscribe button. Whoever unsubscribes stops receiving your campaigns, and so do addresses on the suppression list. See and change the status with [`PUT /v1/email/contacts/{email}/subscription`](https://docs.interweb.co.mz/en/email-center.md).

### Statistics

[`GET /v1/email/campaigns/{id}/stats`](https://docs.interweb.co.mz/en/email-center.md) gives delivered emails, opens, clicks, unsubscribes and bounces. [`GET /v1/email/campaigns/{id}/recipients`](https://docs.interweb.co.mz/en/email-center.md) shows what each contact did. In test mode the numbers are examples and come with `simulated` set to `true`.


## Operations

43 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 emails

`GET https://api.interweb.co.mz/v1/email/messages`

Needs `email_center:read`. Newest first, without the content (see GET /v1/email/messages/{id}).

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `status` | query | string | no | One of: queued, scheduled, sending, sent, delivered, bounced, complained, failed, simulated, cancelled. |
| `from` | query | string | no | From this day, YYYY-MM-DD. |
| `to` | query | string | no | Up to this day, YYYY-MM-DD. |
| `recipient` | query | string | no | Only emails to this address (to, cc or bcc). |
| `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.co.mz/v1/email/messages" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/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.co.mz/v1/email/messages", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List emails.

```json
{
  "data": [
    {
      "id": "5120",
      "status": "delivered",
      "simulated": false,
      "from": "notifications@example.com",
      "to": [
        "ana@example.org"
      ],
      "cc": [],
      "bcc": [],
      "reply_to": [
        "support@example.com"
      ],
      "subject": "Your order 1234 is ready",
      "recipients": 1,
      "credits": 1.0,
      "charged": true,
      "source": "api",
      "template_id": 7,
      "attachments": [],
      "error": null,
      "scheduled_for": null,
      "created_at": "2026-11-12T10:30:00+02:00",
      "sent_at": "2026-11-12T10:30:01+02:00",
      "from_name": "Example Ltd"
    }
  ],
  "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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Send an email

`POST https://api.interweb.co.mz/v1/email/messages`

Needs `email_center:write`. Each recipient (to, cc and bcc, up to 50) is one email and one credit. Addresses on the suppression list are removed and listed in `suppressed`; if none is left, the API answers 422 `all_recipients_suppressed`. Without `send_at` the email goes out straight away and the answer has the real status. With a sandbox key, or while Email Center is in test mode, nothing is sent or charged: the email is `simulated` and the simulated events are recorded (`bounce@example.com` simulates a bounce, `complaint@example.com` a complaint). Send `Idempotency-Key` so a retry never sends twice.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `from` | string | yes | Sender at a verified domain (or a subdomain of it): `notifications@example.com` or `Example Ltd <notifications@example.com>`. |
| `from_name` | string | no | Optional display name. |
| `to` | any | yes | One address, a comma-separated list or an array. |
| `cc` | any | no |  |
| `bcc` | any | no |  |
| `reply_to` | any | no | Up to 5 addresses. |
| `subject` | string | no | Up to 998 characters. May use {{variables}}. |
| `html` | string | no | HTML body. Values of {{variables}} are HTML-escaped. |
| `text` | string | no | Plain text body. |
| `template_id` | integer | no | A template of this account; empty subject, html and text come from it. |
| `variables` | object | no | Values for {{name}} placeholders, as an object. |
| `attachments` | array<object> | no | Base64 files; 5 MB in total by default, executables refused. |
| `send_at` | string | no | Optional, ISO 8601 with time zone, up to 30 days ahead by default. |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/messages" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/messages');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}',
    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 = {"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}
r = requests.post("https://api.interweb.co.mz/v1/email/messages", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/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({"from": "Example Ltd <notifications@example.com>", "to": ["ana@example.org"], "reply_to": "support@example.com", "template_id": 7, "variables": {"name": "Ana", "order": "1234"}}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Send an email.

```json
{
  "data": {
    "id": "5120",
    "status": "sent",
    "simulated": false,
    "from": "notifications@example.com",
    "to": [
      "ana@example.org"
    ],
    "cc": [],
    "bcc": [],
    "reply_to": [
      "support@example.com"
    ],
    "subject": "Your order 1234 is ready",
    "recipients": 1,
    "credits": 1.0,
    "charged": true,
    "source": "api",
    "template_id": 7,
    "attachments": [],
    "error": null,
    "scheduled_for": null,
    "created_at": "2026-11-12T10:30:00+02:00",
    "sent_at": "2026-11-12T10:30:01+02:00",
    "from_name": "Example Ltd",
    "suppressed": []
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.recipients` | integer | Recipients after the suppression list; each one is one credit. |
| `data.source` | string | portal, api or smtp. |

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

##### `400`

The request is not valid, for example the body is not JSON or the connection is not HTTPS.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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 |
|---|---|---|
| 400 | `invalid_request` | The request is not valid, for example the body is not JSON or the connection is not HTTPS. |
| 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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Send emails in a batch

`POST https://api.interweb.co.mz/v1/email/messages/batch`

Needs `email_center:write`. No limit on the number of messages, only on the request size (8 MB). Fields outside `messages` (from, subject, html, text, template_id, reply_to, send_at) apply to all; each message can change them and has its own `to` and `variables`. Each one follows the rules of POST /v1/email/messages and `results` says what happened to it (`error` uses the same reasons). Up to 20 go out straight away; the rest join the queue (every minute). Without credits or at the daily limit, the rest are rejected.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `from` | string | no | Sender at a verified domain (or a subdomain of it): `notifications@example.com` or `Example Ltd <notifications@example.com>`. |
| `from_name` | string | no | Optional display name. |
| `to` | any | no | One address, a comma-separated list or an array. |
| `cc` | any | no |  |
| `bcc` | any | no |  |
| `reply_to` | any | no | Up to 5 addresses. |
| `subject` | string | no | Up to 998 characters. May use {{variables}}. |
| `html` | string | no | HTML body. Values of {{variables}} are HTML-escaped. |
| `text` | string | no | Plain text body. |
| `template_id` | integer | no | A template of this account; empty subject, html and text come from it. |
| `variables` | object | no | Values for {{name}} placeholders, as an object. |
| `attachments` | array<object> | no | Base64 files; 5 MB in total by default, executables refused. |
| `send_at` | string | no | Optional, ISO 8601 with time zone, up to 30 days ahead by default. |
| `messages` | array<object> | yes |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/messages/batch" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"from": "Example Ltd <notifications@example.com>", "subject": "Hello {{name}}", "text": "Hi {{name}}, your invoice is ready.", "messages": [{"to": "ana@example.org", "variables": {"name": "Ana"}}, {"to": "rui@example.org", "variables": {"name": "Rui"}}, {"to": "not-an-email"}]}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/messages/batch');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"from": "Example Ltd <notifications@example.com>", "subject": "Hello {{name}}", "text": "Hi {{name}}, your invoice is ready.", "messages": [{"to": "ana@example.org", "variables": {"name": "Ana"}}, {"to": "rui@example.org", "variables": {"name": "Rui"}}, {"to": "not-an-email"}]}',
    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 = {"from": "Example Ltd <notifications@example.com>", "subject": "Hello {{name}}", "text": "Hi {{name}}, your invoice is ready.", "messages": [{"to": "ana@example.org", "variables": {"name": "Ana"}}, {"to": "rui@example.org", "variables": {"name": "Rui"}}, {"to": "not-an-email"}]}
r = requests.post("https://api.interweb.co.mz/v1/email/messages/batch", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/messages/batch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"from": "Example Ltd <notifications@example.com>", "subject": "Hello {{name}}", "text": "Hi {{name}}, your invoice is ready.", "messages": [{"to": "ana@example.org", "variables": {"name": "Ana"}}, {"to": "rui@example.org", "variables": {"name": "Rui"}}, {"to": "not-an-email"}]}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Send emails in a batch.

```json
{
  "data": {
    "total": 3,
    "accepted": 2,
    "rejected": 1,
    "credits": 2.0,
    "simulated": false,
    "results": [
      {
        "index": 0,
        "id": "5121",
        "status": "sent",
        "recipients": 1,
        "suppressed": []
      },
      {
        "index": 1,
        "id": "5122",
        "status": "sent",
        "recipients": 1,
        "suppressed": []
      },
      {
        "index": 2,
        "id": null,
        "status": "rejected",
        "error": "invalid_recipient",
        "field": "to"
      }
    ]
  },
  "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`. |

##### `400`

The request is not valid, for example the body is not JSON or the connection is not HTTPS.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {}
  }
}
```

##### `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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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 |
|---|---|---|
| 400 | `invalid_request` | The request is not valid, for example the body is not JSON or the connection is not HTTPS. |
| 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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### See an email

`GET https://api.interweb.co.mz/v1/email/messages/{id}`

Needs `email_center:read`. With the events of each recipient and the content (kept for 30 days by default; then `html` and `text` are null).

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The `id` returned when the email was sent. |
| `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.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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`

See an email.

```json
{
  "data": {
    "id": "5120",
    "status": "delivered",
    "simulated": false,
    "from": "notifications@example.com",
    "to": [
      "ana@example.org"
    ],
    "cc": [],
    "bcc": [],
    "reply_to": [
      "support@example.com"
    ],
    "subject": "Your order 1234 is ready",
    "recipients": 1,
    "credits": 1.0,
    "charged": true,
    "source": "api",
    "template_id": 7,
    "attachments": [],
    "error": null,
    "scheduled_for": null,
    "created_at": "2026-11-12T10:30:00+02:00",
    "sent_at": "2026-11-12T10:30:01+02:00",
    "from_name": "Example Ltd",
    "html": "<p>Hello Ana, your order 1234 is ready.</p>",
    "text": "Hello Ana, your order 1234 is ready.",
    "events": [
      {
        "type": "sent",
        "recipient": null,
        "detail": null,
        "simulated": false,
        "created_at": "2026-11-12T10:30:01+02:00"
      },
      {
        "type": "delivered",
        "recipient": "ana@example.org",
        "detail": null,
        "simulated": false,
        "created_at": "2026-11-12T10:30:03+02:00"
      }
    ]
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.recipients` | integer | Recipients after the suppression list; each one is one credit. |
| `data.source` | string | portal, api or smtp. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Cancel a scheduled email

`POST https://api.interweb.co.mz/v1/email/messages/{id}/cancel`

Needs `email_center:write`. Only `scheduled` emails that have not gone out yet; credits already taken go back (`credits_returned`). Otherwise 409 `not_cancellable`.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The `id` returned when the email was sent. |
| `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.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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 email.

```json
{
  "data": {
    "id": "5120",
    "status": "cancelled",
    "simulated": false,
    "from": "notifications@example.com",
    "to": [
      "ana@example.org"
    ],
    "cc": [],
    "bcc": [],
    "reply_to": [
      "support@example.com"
    ],
    "subject": "Your order 1234 is ready",
    "recipients": 1,
    "credits": 1.0,
    "charged": true,
    "source": "api",
    "template_id": 7,
    "attachments": [],
    "error": null,
    "scheduled_for": "2026-11-20T09:30:00+02:00",
    "created_at": "2026-11-12T10:30:00+02:00",
    "sent_at": null,
    "from_name": "Example Ltd",
    "credits_returned": 0.0
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.recipients` | integer | Recipients after the suppression list; each one is one credit. |
| `data.source` | string | portal, api or smtp. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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 | `forbidden`, `insufficient_scope` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### See the email balance

`GET https://api.interweb.co.mz/v1/email/balance`

Needs `email_center:read`. One credit is one email to one recipient. `test_mode` is true while Email Center does not send for real.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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.co.mz/v1/email/balance" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/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.co.mz/v1/email/balance", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

See the email balance.

```json
{
  "data": {
    "balance": 9850.0,
    "unit": "emails",
    "scheduled_credits": 0.0,
    "daily_limit": 500,
    "sent_today": 42,
    "mode": "production",
    "test_mode": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### See email statistics

`GET https://api.interweb.co.mz/v1/email/stats`

Needs `email_center:read`. By status and by day. Delivered, bounced and complained count recipients; the rates are over the recipients. Default: the last 30 days; at most 366.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `from` | query | string | no | YYYY-MM-DD. |
| `to` | query | string | no | 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.co.mz/v1/email/stats" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/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.co.mz/v1/email/stats", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

See email statistics.

```json
{
  "data": {
    "from": "2026-10-14",
    "to": "2026-11-12",
    "messages": 320,
    "recipients": 340,
    "credits_spent": 338.0,
    "by_status": {
      "delivered": 300,
      "bounced": 4,
      "complained": 0,
      "sent": 14,
      "failed": 2
    },
    "events": {
      "delivered": 330,
      "bounced": 4,
      "complained": 0
    },
    "delivery_rate": 97.06,
    "bounce_rate": 1.18,
    "complaint_rate": 0.0,
    "by_day": [
      {
        "date": "2026-11-12",
        "messages": 12,
        "recipients": 12,
        "delivered": 12,
        "bounced": 0,
        "complained": 0,
        "credits": 12.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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### List sender domains

`GET https://api.interweb.co.mz/v1/email/domains`

Needs `email_center:read`. In alphabetical order.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `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.co.mz/v1/email/domains" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/domains');
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.co.mz/v1/email/domains", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List sender domains.

```json
{
  "data": [
    {
      "id": 12,
      "domain": "example.com",
      "status": "verified",
      "mail_from": null,
      "verified_at": "2026-11-12T10:00:00+02:00",
      "last_checked_at": "2026-11-12T10:00:00+02:00",
      "created_at": "2026-11-12T09:40: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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Add a sender domain

`POST https://api.interweb.co.mz/v1/email/domains`

Needs `email_center:write`. Add the records of `dns_records` to the DNS of the domain: three DKIM CNAME records (required), SPF and DMARC (recommended) and, with MAIL FROM, an MX and a TXT. Then call verify; DNS can take a few hours. Only verified domains can send, from any address of the domain or of a subdomain. Free mailbox domains (gmail.com and others) are refused. With a sandbox key nothing is saved.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `domain` | string | yes |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/domains" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/domains');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"domain": "example.com"}',
    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 = {"domain": "example.com"}
r = requests.post("https://api.interweb.co.mz/v1/email/domains", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/domains", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"domain": "example.com"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Add a sender domain.

```json
{
  "data": {
    "id": 12,
    "domain": "example.com",
    "status": "pending",
    "mail_from": null,
    "verified_at": null,
    "last_checked_at": null,
    "created_at": "2026-11-12T09:40:00+02:00",
    "dns_records": [
      {
        "type": "CNAME",
        "name": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a._domainkey.example.com",
        "value": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a.dkim.example-brand.com",
        "priority": null,
        "purpose": "dkim",
        "required": true,
        "found": null
      },
      {
        "type": "TXT",
        "name": "example.com",
        "value": "v=spf1 include:_spf.example-brand.com ~all",
        "priority": null,
        "purpose": "spf",
        "required": false,
        "found": null
      },
      {
        "type": "TXT",
        "name": "_dmarc.example.com",
        "value": "v=DMARC1; p=none; adkim=r; aspf=r",
        "priority": null,
        "purpose": "dmarc",
        "required": false,
        "found": null
      }
    ],
    "test_mode_accepted": false,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### See a sender domain

`GET https://api.interweb.co.mz/v1/email/domains/{domain}`

Needs `email_center:read`. With the DNS records to add and whether each one was found at the last check.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `domain` | path | string | yes | The domain name (example.com) or its `id`. |
| `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.co.mz/v1/email/domains/VALUE" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/domains/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.co.mz/v1/email/domains/VALUE", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/domains/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 domain.

```json
{
  "data": {
    "id": 12,
    "domain": "example.com",
    "status": "verified",
    "mail_from": null,
    "verified_at": "2026-11-12T10:00:00+02:00",
    "last_checked_at": "2026-11-12T10:00:00+02:00",
    "created_at": "2026-11-12T09:40:00+02:00",
    "dns_records": [
      {
        "type": "CNAME",
        "name": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a._domainkey.example.com",
        "value": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a.dkim.example-brand.com",
        "priority": null,
        "purpose": "dkim",
        "required": true,
        "found": true
      },
      {
        "type": "TXT",
        "name": "example.com",
        "value": "v=spf1 include:_spf.example-brand.com ~all",
        "priority": null,
        "purpose": "spf",
        "required": false,
        "found": true
      },
      {
        "type": "TXT",
        "name": "_dmarc.example.com",
        "value": "v=DMARC1; p=none; adkim=r; aspf=r",
        "priority": null,
        "purpose": "dmarc",
        "required": false,
        "found": false
      }
    ],
    "test_mode_accepted": false,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Delete a sender domain

`DELETE https://api.interweb.co.mz/v1/email/domains/{domain}`

Needs `email_center:write`. The domain can no longer send. Emails already sent stay in the log.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `domain` | path | string | yes | The domain name (example.com) or its `id`. |
| `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.co.mz/v1/email/domains/VALUE" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/domains/VALUE');
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.co.mz/v1/email/domains/VALUE", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/domains/VALUE", {
  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 sender domain.

```json
{
  "data": {
    "id": 12,
    "domain": "example.com",
    "deleted": true,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Verify a sender domain

`POST https://api.interweb.co.mz/v1/email/domains/{domain}/verify`

Needs `email_center:write`. Checks the DKIM records now and returns the new status. With a sandbox key the result is not saved.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `domain` | path | string | yes | The domain name (example.com) or its `id`. |
| `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. |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/domains/VALUE/verify" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/domains/VALUE/verify');
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.co.mz/v1/email/domains/VALUE/verify", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/domains/VALUE/verify", {
  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`

Verify a sender domain.

```json
{
  "data": {
    "id": 12,
    "domain": "example.com",
    "status": "verified",
    "mail_from": null,
    "verified_at": "2026-11-12T10:00:00+02:00",
    "last_checked_at": "2026-11-12T10:00:00+02:00",
    "created_at": "2026-11-12T09:40:00+02:00",
    "dns_records": [
      {
        "type": "CNAME",
        "name": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a._domainkey.example.com",
        "value": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a.dkim.example-brand.com",
        "priority": null,
        "purpose": "dkim",
        "required": true,
        "found": true
      },
      {
        "type": "TXT",
        "name": "example.com",
        "value": "v=spf1 include:_spf.example-brand.com ~all",
        "priority": null,
        "purpose": "spf",
        "required": false,
        "found": true
      },
      {
        "type": "TXT",
        "name": "_dmarc.example.com",
        "value": "v=DMARC1; p=none; adkim=r; aspf=r",
        "priority": null,
        "purpose": "dmarc",
        "required": false,
        "found": false
      }
    ],
    "test_mode_accepted": false,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Set the MAIL FROM subdomain

`PUT https://api.interweb.co.mz/v1/email/domains/{domain}/mail-from`

Needs `email_center:write`. Optional: a subdomain of yours for the return path (for example `bounce`), so SPF aligns with your domain for DMARC. An empty value goes back to ours. Add the MX and TXT records that appear in `dns_records`.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `domain` | path | string | yes | The domain name (example.com) or its `id`. |
| `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 |
|---|---|---|---|
| `subdomain` | string, null | yes |  |

#### Examples

**curl**

```bash
curl -X PUT "https://api.interweb.co.mz/v1/email/domains/VALUE/mail-from" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Content-Type: application/json" \
  -d '{"subdomain": "bounce"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/domains/VALUE/mail-from');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_POSTFIELDS => '{"subdomain": "bounce"}',
    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 = {"subdomain": "bounce"}
r = requests.put("https://api.interweb.co.mz/v1/email/domains/VALUE/mail-from", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/domains/VALUE/mail-from", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"subdomain": "bounce"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Set the MAIL FROM subdomain.

```json
{
  "data": {
    "id": 12,
    "domain": "example.com",
    "status": "verified",
    "mail_from": "bounce.example.com",
    "verified_at": "2026-11-12T10:00:00+02:00",
    "last_checked_at": "2026-11-12T10:00:00+02:00",
    "created_at": "2026-11-12T09:40:00+02:00",
    "dns_records": [
      {
        "type": "CNAME",
        "name": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a._domainkey.example.com",
        "value": "k3jd8s0a1b2c3d4e5f6a7b8c9d0e1f2a.dkim.example-brand.com",
        "priority": null,
        "purpose": "dkim",
        "required": true,
        "found": true
      },
      {
        "type": "TXT",
        "name": "example.com",
        "value": "v=spf1 include:_spf.example-brand.com ~all",
        "priority": null,
        "purpose": "spf",
        "required": false,
        "found": true
      },
      {
        "type": "TXT",
        "name": "_dmarc.example.com",
        "value": "v=DMARC1; p=none; adkim=r; aspf=r",
        "priority": null,
        "purpose": "dmarc",
        "required": false,
        "found": false
      }
    ],
    "test_mode_accepted": false,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `404`

Not found.

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

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 404 | `not_found` | Not found. |
| 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.co.mz/en/api-rules.md).


### List email templates

`GET https://api.interweb.co.mz/v1/email/templates`

Needs `email_center:read`. By name.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `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.co.mz/v1/email/templates" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/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.co.mz/v1/email/templates", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List email templates.

```json
{
  "data": [
    {
      "id": 7,
      "name": "Order ready",
      "subject": "Your order {{order}} is ready",
      "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>",
      "text": "Hello {{name}}, your order {{order}} is ready.",
      "variables": [
        "order",
        "name"
      ],
      "created_at": "2026-11-12T09:00:00+02:00",
      "updated_at": "2026-11-12T09:00: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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Create an email template

`POST https://api.interweb.co.mz/v1/email/templates`

Needs `email_center:write`. Subject and html or text, with {{name}} placeholders filled from `variables` when sending.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `name` | string | yes | Up to 100 characters, unique in the account. |
| `subject` | string | yes |  |
| `html` | string | no |  |
| `text` | string | no |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/templates" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"name": "Order ready", "subject": "Your order {{order}} is ready", "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>", "text": "Hello {{name}}, your order {{order}} is ready."}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/templates');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"name": "Order ready", "subject": "Your order {{order}} is ready", "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>", "text": "Hello {{name}}, your order {{order}} is ready."}',
    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": "Order ready", "subject": "Your order {{order}} is ready", "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>", "text": "Hello {{name}}, your order {{order}} is ready."}
r = requests.post("https://api.interweb.co.mz/v1/email/templates", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/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": "Order ready", "subject": "Your order {{order}} is ready", "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>", "text": "Hello {{name}}, your order {{order}} is ready."}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Create an email template.

```json
{
  "data": {
    "id": 7,
    "name": "Order ready",
    "subject": "Your order {{order}} is ready",
    "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>",
    "text": "Hello {{name}}, your order {{order}} is ready.",
    "variables": [
      "order",
      "name"
    ],
    "created_at": "2026-11-12T09:00:00+02:00",
    "updated_at": "2026-11-12T09:00: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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### See an email template

`GET https://api.interweb.co.mz/v1/email/templates/{id}`

Needs `email_center:read`. With the variables it uses.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The template `id` (the same as `template_id` 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.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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 an email template.

```json
{
  "data": {
    "id": 7,
    "name": "Order ready",
    "subject": "Your order {{order}} is ready",
    "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>",
    "text": "Hello {{name}}, your order {{order}} is ready.",
    "variables": [
      "order",
      "name"
    ],
    "created_at": "2026-11-12T09:00:00+02:00",
    "updated_at": "2026-11-12T09:00: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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Change an email template

`PATCH https://api.interweb.co.mz/v1/email/templates/{id}`

Needs `email_center:write`. Only the fields sent change.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The template `id` (the same as `template_id` 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. |

#### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | Up to 100 characters, unique in the account. |
| `subject` | string | no |  |
| `html` | string | no |  |
| `text` | string | no |  |

#### Examples

**curl**

```bash
curl -X PATCH "https://api.interweb.co.mz/v1/email/templates/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Order {{order}}: ready for pickup"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/templates/00000000-0000-4000-8000-000000000000');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_POSTFIELDS => '{"subject": "Order {{order}}: ready for pickup"}',
    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 = {"subject": "Order {{order}}: ready for pickup"}
r = requests.patch("https://api.interweb.co.mz/v1/email/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.co.mz/v1/email/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({"subject": "Order {{order}}: ready for pickup"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Change an email template.

```json
{
  "data": {
    "id": 7,
    "name": "Order ready",
    "subject": "Your order {{order}} is ready",
    "html": "<p>Hello {{name}}, your order {{order}} is ready.</p>",
    "text": "Hello {{name}}, your order {{order}} is ready.",
    "variables": [
      "order",
      "name"
    ],
    "created_at": "2026-11-12T09:00:00+02:00",
    "updated_at": "2026-11-12T09:00: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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Delete an email template

`DELETE https://api.interweb.co.mz/v1/email/templates/{id}`

Needs `email_center:write`. Emails already sent are not affected.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The template `id` (the same as `template_id` 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 -X DELETE "https://api.interweb.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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.co.mz/v1/email/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 an email template.

```json
{
  "data": {
    "id": 7,
    "deleted": true,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### List suppressed addresses

`GET https://api.interweb.co.mz/v1/email/suppressions`

Needs `email_center:read`. Addresses that never receive emails from this account: permanent bounces and complaints enter on their own; you can add others.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `reason` | query | string | no | bounce, complaint or manual. |
| `email` | query | string | no | One address. |
| `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.co.mz/v1/email/suppressions" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/suppressions');
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.co.mz/v1/email/suppressions", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List suppressed addresses.

```json
{
  "data": [
    {
      "email": "ana@example.org",
      "reason": "manual",
      "note": "Asked to stop",
      "created_at": "2026-11-12T10:00: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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Suppress an address

`POST https://api.interweb.co.mz/v1/email/suppressions`

Needs `email_center:write`. Emails to it are removed before sending and never charged.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `email` | string | yes |  |
| `note` | string | no |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/suppressions" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"email": "ana@example.org", "note": "Asked to stop"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/suppressions');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"email": "ana@example.org", "note": "Asked to stop"}',
    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 = {"email": "ana@example.org", "note": "Asked to stop"}
r = requests.post("https://api.interweb.co.mz/v1/email/suppressions", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/suppressions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"email": "ana@example.org", "note": "Asked to stop"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Suppress an address.

```json
{
  "data": {
    "email": "ana@example.org",
    "reason": "manual",
    "note": "Asked to stop",
    "created_at": "2026-11-12T10:00:00+02:00",
    "already_listed": false,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Remove a suppressed address

`DELETE https://api.interweb.co.mz/v1/email/suppressions/{email}`

Needs `email_center:write`. The address goes in the path, URL-encoded (`ana%40example.org`).

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `email` | path | string | yes | The address, URL-encoded. |
| `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.co.mz/v1/email/suppressions/VALUE" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/suppressions/VALUE');
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.co.mz/v1/email/suppressions/VALUE", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

Remove a suppressed address.

```json
{
  "data": {
    "email": "ana@example.org",
    "deleted": true,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Get the email webhook

`GET https://api.interweb.co.mz/v1/email/webhook`

Needs `email_center:read`. The address, the events and the last delivery; never the secret. We POST `{"event": "email.delivered", "created_at", "data": {"id", "event_id", "recipient", "detail", "from", "subject", "simulated"}}` to your HTTPS address on port 443, one call per event and recipient, only for events after the webhook was saved. Check `X-Webhook-Signature: t=<unix time>,v1=<hex>`: v1 is HMAC-SHA256 of `t + "." + raw body` with your secret. Answer 2xx within 5 seconds; otherwise we retry after 1 min, 5 min, 30 min, 2 h and 6 h. This webhook is separate from the SMS one.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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.co.mz/v1/email/webhook" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/webhook');
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.co.mz/v1/email/webhook", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

Get the email webhook.

```json
{
  "data": {
    "url": "https://example.com/webhooks/email",
    "events": [
      "email.delivered",
      "email.bounced",
      "email.complained"
    ],
    "created_at": "2026-11-12T10:30:00+02:00",
    "updated_at": "2026-11-12T10:30:00+02:00",
    "last_delivery_at": null,
    "last_status_code": null,
    "last_error": null
  },
  "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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Save the email webhook

`PUT https://api.interweb.co.mz/v1/email/webhook`

Needs `email_center:write`. The `secret` is shown only when the webhook is created or with `rotate_secret: true`. We POST `{"event": "email.delivered", "created_at", "data": {"id", "event_id", "recipient", "detail", "from", "subject", "simulated"}}` to your HTTPS address on port 443, one call per event and recipient, only for events after the webhook was saved. Check `X-Webhook-Signature: t=<unix time>,v1=<hex>`: v1 is HMAC-SHA256 of `t + "." + raw body` with your secret. Answer 2xx within 5 seconds; otherwise we retry after 1 min, 5 min, 30 min, 2 h and 6 h. This webhook is separate from the SMS one.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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. |

#### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes |  |
| `events` | array<string: email.sent, email.delivered, email.bounced, email.complained, email.failed> | no |  |
| `rotate_secret` | boolean | no |  |

#### Examples

**curl**

```bash
curl -X PUT "https://api.interweb.co.mz/v1/email/webhook" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhooks/email", "events": ["email.delivered", "email.bounced", "email.complained"]}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/webhook');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_POSTFIELDS => '{"url": "https://example.com/webhooks/email", "events": ["email.delivered", "email.bounced", "email.complained"]}',
    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 = {"url": "https://example.com/webhooks/email", "events": ["email.delivered", "email.bounced", "email.complained"]}
r = requests.put("https://api.interweb.co.mz/v1/email/webhook", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/webhook", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"url": "https://example.com/webhooks/email", "events": ["email.delivered", "email.bounced", "email.complained"]}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Save the email webhook.

```json
{
  "data": {
    "url": "https://example.com/webhooks/email",
    "events": [
      "email.delivered",
      "email.bounced",
      "email.complained"
    ],
    "created_at": "2026-11-12T10:30:00+02:00",
    "updated_at": "2026-11-12T10:30:00+02:00",
    "last_delivery_at": null,
    "last_status_code": null,
    "last_error": null,
    "secret": "whsec_8c1f0e2d4b6a7c9e1f3a5b7d9e0c2a4b6d8f0a1c3e5b7d9f",
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Delete the email webhook

`DELETE https://api.interweb.co.mz/v1/email/webhook`

Needs `email_center:write`. Pending deliveries are cancelled.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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. |

#### Examples

**curl**

```bash
curl -X DELETE "https://api.interweb.co.mz/v1/email/webhook" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/webhook');
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.co.mz/v1/email/webhook", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/webhook", {
  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 the email webhook.

```json
{
  "data": {
    "deleted": true,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Test the email webhook

`POST https://api.interweb.co.mz/v1/email/webhook/test`

Needs `email_center:write`. Sends a signed `webhook.test` event to the saved address now and returns what your server answered.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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. |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/webhook/test" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/webhook/test');
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.co.mz/v1/email/webhook/test", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/webhook/test", {
  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`

Test the email webhook.

```json
{
  "data": {
    "delivered": true,
    "status_code": 200,
    "error": null
  },
  "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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### List campaigns

`GET https://api.interweb.co.mz/v1/email/campaigns`

Needs `email_center:read`. Newest first, without the blocks.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `status` | query | string | no | draft, scheduled, sending, sent, cancelled or automation. |
| `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.co.mz/v1/email/campaigns" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns');
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.co.mz/v1/email/campaigns", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List campaigns.

```json
{
  "data": [
    {
      "id": 41,
      "name": "November news",
      "type": "campaign",
      "status": "draft",
      "subject": "New arrivals for you",
      "preheader": "Special prices until the end of the month",
      "from": "news@example.com",
      "from_name": "Example Ltd",
      "reply_to": null,
      "recipients": 0,
      "credits": 0.0,
      "simulated": false,
      "scheduled_for": null,
      "sent_at": null,
      "created_at": "2026-11-12T10:00:00+02:00",
      "updated_at": "2026-11-12T10:05:00+02:00",
      "blocks": null,
      "target": null
    }
  ],
  "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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Create a campaign

`POST https://api.interweb.co.mz/v1/email/campaigns`

Needs `email_center:write`. Creates a draft. Send `blocks`, or `template` for a ready template. Variables: {{first_name}}, {{name}}, {{company}}, {{email}}. With a sandbox key nothing is saved.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `name` | string | no |  |
| `subject` | string | no |  |
| `preheader` | string | no | Text shown after the subject in the inbox. |
| `from` | string | no | An address at a verified domain. |
| `from_name` | string | no |  |
| `reply_to` | string | no |  |
| `blocks` | array<object> | no |  |
| `template` | string | no | Instead of blocks: a ready template by sector (restauracao, comercio, servicos, educacao, saude, eventos, imobiliario, tecnologia). |
| `target` | object | no |  |
| `target.groups` | array<integer> | no | Contact group ids; empty means all contacts. |
| `target.opened` | string, null | no | yes or no: opened any campaign before. |
| `target.clicked` | string, null | no | yes or no. |
| `target.joined_from` | string, null | no | YYYY-MM-DD. |
| `target.joined_to` | string, null | no | YYYY-MM-DD. |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/campaigns" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"name": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"name": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}',
    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": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}
r = requests.post("https://api.interweb.co.mz/v1/email/campaigns", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/campaigns", {
  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": "November news", "template": "comercio", "from": "news@example.com", "from_name": "Example Ltd", "target": {"groups": [3]}}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Create a campaign.

```json
{
  "data": {
    "id": 41,
    "name": "November news",
    "type": "campaign",
    "status": "draft",
    "subject": "New arrivals for you",
    "preheader": "Special prices until the end of the month",
    "from": "news@example.com",
    "from_name": "Example Ltd",
    "reply_to": null,
    "recipients": 0,
    "credits": 0.0,
    "simulated": false,
    "scheduled_for": null,
    "sent_at": null,
    "created_at": "2026-11-12T10:00:00+02:00",
    "updated_at": "2026-11-12T10:05:00+02:00",
    "blocks": [
      {
        "type": "header",
        "title": "New arrivals for you",
        "logo": "",
        "color": "#af2835"
      },
      {
        "type": "text",
        "text": "Hi {{first_name}},\n\nNew products have arrived, with **special prices** until the end of the month."
      },
      {
        "type": "button",
        "text": "See what is new",
        "url": "https://example.com",
        "color": "#af2835"
      },
      {
        "type": "divider"
      },
      {
        "type": "footer",
        "text": "You are receiving this email because you subscribed to our news."
      }
    ],
    "target": {
      "groups": [
        3
      ],
      "opened": null,
      "clicked": null,
      "joined_from": null,
      "joined_to": null
    }
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.target.groups` | array<integer> | Contact group ids; empty means all contacts. |
| `data.target.opened` | string, null | yes or no: opened any campaign before. |
| `data.target.clicked` | string, null | yes or no. |
| `data.target.joined_from` | string, null | YYYY-MM-DD. |
| `data.target.joined_to` | string, null | YYYY-MM-DD. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Generate a campaign with AI

`POST https://api.interweb.co.mz/v1/email/campaigns/generate`

Needs `email_center:write`. Describe what you want to send; the answer has a subject, a preheader and blocks, which you can then send to POST /v1/email/campaigns. Nothing is saved. 503 `ai_unavailable` if AI is off; counts towards the daily AI limit.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `description` | string | yes | 10 to 1500 characters. |
| `sector` | string | no |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/campaigns/generate" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"description": "20% off all plans until Friday, for existing customers.", "sector": "tecnologia"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/generate');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"description": "20% off all plans until Friday, for existing customers.", "sector": "tecnologia"}',
    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 = {"description": "20% off all plans until Friday, for existing customers.", "sector": "tecnologia"}
r = requests.post("https://api.interweb.co.mz/v1/email/campaigns/generate", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/campaigns/generate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"description": "20% off all plans until Friday, for existing customers.", "sector": "tecnologia"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Generate a campaign with AI.

```json
{
  "data": {
    "subject": "Our November offers",
    "preheader": "20% off until Friday",
    "blocks": [
      {
        "type": "header",
        "title": "New arrivals for you",
        "logo": "",
        "color": "#af2835"
      },
      {
        "type": "text",
        "text": "Hi {{first_name}},\n\nNew products have arrived, with **special prices** until the end of the month."
      },
      {
        "type": "button",
        "text": "See what is new",
        "url": "https://example.com",
        "color": "#af2835"
      },
      {
        "type": "divider"
      },
      {
        "type": "footer",
        "text": "You are receiving this email because you subscribed to our news."
      }
    ]
  },
  "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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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": {}
  }
}
```

##### `502`

The service behind the API did not respond as expected.

```json
{
  "error": {
    "code": "upstream_error",
    "message": "The service did not respond as expected. Please try again later.",
    "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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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. |
| 502 | `upstream_error` | The service behind the API did not respond as expected. |
| 503 | `disabled` | The API is turned off. |

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


### See a campaign

`GET https://api.interweb.co.mz/v1/email/campaigns/{id}`

Needs `email_center:read`. With the blocks and the audience.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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 campaign.

```json
{
  "data": {
    "id": 41,
    "name": "November news",
    "type": "campaign",
    "status": "draft",
    "subject": "New arrivals for you",
    "preheader": "Special prices until the end of the month",
    "from": "news@example.com",
    "from_name": "Example Ltd",
    "reply_to": null,
    "recipients": 0,
    "credits": 0.0,
    "simulated": false,
    "scheduled_for": null,
    "sent_at": null,
    "created_at": "2026-11-12T10:00:00+02:00",
    "updated_at": "2026-11-12T10:05:00+02:00",
    "blocks": [
      {
        "type": "header",
        "title": "New arrivals for you",
        "logo": "",
        "color": "#af2835"
      },
      {
        "type": "text",
        "text": "Hi {{first_name}},\n\nNew products have arrived, with **special prices** until the end of the month."
      },
      {
        "type": "button",
        "text": "See what is new",
        "url": "https://example.com",
        "color": "#af2835"
      },
      {
        "type": "divider"
      },
      {
        "type": "footer",
        "text": "You are receiving this email because you subscribed to our news."
      }
    ],
    "target": {
      "groups": [
        3
      ],
      "opened": null,
      "clicked": null,
      "joined_from": null,
      "joined_to": null
    }
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.target.groups` | array<integer> | Contact group ids; empty means all contacts. |
| `data.target.opened` | string, null | yes or no: opened any campaign before. |
| `data.target.clicked` | string, null | yes or no. |
| `data.target.joined_from` | string, null | YYYY-MM-DD. |
| `data.target.joined_to` | string, null | YYYY-MM-DD. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Change a campaign

`PATCH https://api.interweb.co.mz/v1/email/campaigns/{id}`

Needs `email_center:write`. Only drafts and automation emails; only the fields sent change.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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 |  |
| `subject` | string | no |  |
| `preheader` | string | no | Text shown after the subject in the inbox. |
| `from` | string | no | An address at a verified domain. |
| `from_name` | string | no |  |
| `reply_to` | string | no |  |
| `blocks` | array<object> | no |  |
| `template` | string | no | Instead of blocks: a ready template by sector (restauracao, comercio, servicos, educacao, saude, eventos, imobiliario, tecnologia). |
| `target` | object | no |  |
| `target.groups` | array<integer> | no | Contact group ids; empty means all contacts. |
| `target.opened` | string, null | no | yes or no: opened any campaign before. |
| `target.clicked` | string, null | no | yes or no. |
| `target.joined_from` | string, null | no | YYYY-MM-DD. |
| `target.joined_to` | string, null | no | YYYY-MM-DD. |

#### Examples

**curl**

```bash
curl -X PATCH "https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Last days of our offers"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_POSTFIELDS => '{"subject": "Last days of our offers"}',
    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 = {"subject": "Last days of our offers"}
r = requests.patch("https://api.interweb.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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({"subject": "Last days of our offers"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Change a campaign.

```json
{
  "data": {
    "id": 41,
    "name": "November news",
    "type": "campaign",
    "status": "draft",
    "subject": "New arrivals for you",
    "preheader": "Special prices until the end of the month",
    "from": "news@example.com",
    "from_name": "Example Ltd",
    "reply_to": null,
    "recipients": 0,
    "credits": 0.0,
    "simulated": false,
    "scheduled_for": null,
    "sent_at": null,
    "created_at": "2026-11-12T10:00:00+02:00",
    "updated_at": "2026-11-12T10:05:00+02:00",
    "blocks": [
      {
        "type": "header",
        "title": "New arrivals for you",
        "logo": "",
        "color": "#af2835"
      },
      {
        "type": "text",
        "text": "Hi {{first_name}},\n\nNew products have arrived, with **special prices** until the end of the month."
      },
      {
        "type": "button",
        "text": "See what is new",
        "url": "https://example.com",
        "color": "#af2835"
      },
      {
        "type": "divider"
      },
      {
        "type": "footer",
        "text": "You are receiving this email because you subscribed to our news."
      }
    ],
    "target": {
      "groups": [
        3
      ],
      "opened": null,
      "clicked": null,
      "joined_from": null,
      "joined_to": null
    }
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.target.groups` | array<integer> | Contact group ids; empty means all contacts. |
| `data.target.opened` | string, null | yes or no: opened any campaign before. |
| `data.target.clicked` | string, null | yes or no. |
| `data.target.joined_from` | string, null | YYYY-MM-DD. |
| `data.target.joined_to` | string, null | YYYY-MM-DD. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Delete a campaign

`DELETE https://api.interweb.co.mz/v1/email/campaigns/{id}`

Needs `email_center:write`. Only drafts and automation emails.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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 campaign.

```json
{
  "data": {
    "id": 41,
    "deleted": true,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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 | `forbidden`, `insufficient_scope` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Check a campaign for spam

`GET https://api.interweb.co.mz/v1/email/campaigns/{id}/spam-check`

Needs `email_center:read`. Simple rules (subject in capitals, spam words, little text, only images, shortened links, sender not verified). From `blocks_sending_at` points the campaign cannot be sent.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/spam-check" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/spam-check');
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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/spam-check", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

Check a campaign for spam.

```json
{
  "data": {
    "score": 1,
    "max": 10,
    "blocks_sending_at": 8,
    "warnings": [
      {
        "code": "preheader_missing"
      }
    ]
  },
  "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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Send a campaign now

`POST https://api.interweb.co.mz/v1/email/campaigns/{id}/send`

Needs `email_center:write`. Only subscribed contacts with an email address, not on the suppression list, receive it; each one is one credit (taken now and returned for those that do not go out). Every email carries an unsubscribe link and the `List-Unsubscribe` header, plus a tracking pixel and signed redirect links for the statistics. A campaign whose spam check score reaches the limit is refused (422 `spam_score_too_high`). With a sandbox key, or while Email Center is in test mode, nothing is sent or charged and the numbers are simulated (`simulated: true`).

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/send" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/send');
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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/send", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/send", {
  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`

Send a campaign now.

```json
{
  "data": {
    "id": 41,
    "name": "November news",
    "type": "campaign",
    "status": "sending",
    "subject": "New arrivals for you",
    "preheader": "Special prices until the end of the month",
    "from": "news@example.com",
    "from_name": "Example Ltd",
    "reply_to": null,
    "recipients": 1200,
    "credits": 1200.0,
    "simulated": false,
    "scheduled_for": null,
    "sent_at": null,
    "created_at": "2026-11-12T10:00:00+02:00",
    "updated_at": "2026-11-12T10:05:00+02:00",
    "blocks": null,
    "target": null,
    "stats": {
      "recipients": 1200,
      "sent": 0,
      "delivered": 0,
      "bounced": 12,
      "opened": 0,
      "clicked": 0,
      "unsubscribed": 4,
      "open_rate": 43.77,
      "click_rate": 11.03,
      "unsubscribe_rate": 0.34,
      "bounce_rate": 1.0,
      "by_status": {
        "enviado": 1200
      },
      "top_links": [
        {
          "url": "https://example.com",
          "clicks": 131
        }
      ],
      "simulated": false
    }
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.target.groups` | array<integer> | Contact group ids; empty means all contacts. |
| `data.target.opened` | string, null | yes or no: opened any campaign before. |
| `data.target.clicked` | string, null | yes or no. |
| `data.target.joined_from` | string, null | YYYY-MM-DD. |
| `data.target.joined_to` | string, null | YYYY-MM-DD. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Schedule a campaign

`POST https://api.interweb.co.mz/v1/email/campaigns/{id}/schedule`

Needs `email_center:write`. `send_at` in ISO 8601 with the time zone, up to 30 days ahead. Only subscribed contacts with an email address, not on the suppression list, receive it; each one is one credit (taken now and returned for those that do not go out). Every email carries an unsubscribe link and the `List-Unsubscribe` header, plus a tracking pixel and signed redirect links for the statistics. A campaign whose spam check score reaches the limit is refused (422 `spam_score_too_high`). With a sandbox key, or while Email Center is in test mode, nothing is sent or charged and the numbers are simulated (`simulated: true`).

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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 |
|---|---|---|---|
| `send_at` | string | yes |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/schedule" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"send_at": "2026-11-20T09:00:00+02:00"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/schedule');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"send_at": "2026-11-20T09:00: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 = {"send_at": "2026-11-20T09:00:00+02:00"}
r = requests.post("https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/schedule", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/schedule", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"send_at": "2026-11-20T09:00:00+02:00"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Schedule a campaign.

```json
{
  "data": {
    "id": 41,
    "name": "November news",
    "type": "campaign",
    "status": "scheduled",
    "subject": "New arrivals for you",
    "preheader": "Special prices until the end of the month",
    "from": "news@example.com",
    "from_name": "Example Ltd",
    "reply_to": null,
    "recipients": 1200,
    "credits": 1200.0,
    "simulated": false,
    "scheduled_for": "2026-11-20T09:00:00+02:00",
    "sent_at": null,
    "created_at": "2026-11-12T10:00:00+02:00",
    "updated_at": "2026-11-12T10:05:00+02:00",
    "blocks": null,
    "target": null,
    "stats": {
      "recipients": 1200,
      "sent": 0,
      "delivered": 0,
      "bounced": 12,
      "opened": 0,
      "clicked": 0,
      "unsubscribed": 4,
      "open_rate": 43.77,
      "click_rate": 11.03,
      "unsubscribe_rate": 0.34,
      "bounce_rate": 1.0,
      "by_status": {
        "enviado": 1200
      },
      "top_links": [
        {
          "url": "https://example.com",
          "clicks": 131
        }
      ],
      "simulated": false
    }
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.target.groups` | array<integer> | Contact group ids; empty means all contacts. |
| `data.target.opened` | string, null | yes or no: opened any campaign before. |
| `data.target.clicked` | string, null | yes or no. |
| `data.target.joined_from` | string, null | YYYY-MM-DD. |
| `data.target.joined_to` | string, null | YYYY-MM-DD. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Cancel a campaign

`POST https://api.interweb.co.mz/v1/email/campaigns/{id}/cancel`

Needs `email_center:write`. Scheduled or still sending: what has not gone out does not go, and its credits come back (`credits_returned`).

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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.co.mz/v1/email/campaigns/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 campaign.

```json
{
  "data": {
    "id": 41,
    "name": "November news",
    "type": "campaign",
    "status": "cancelled",
    "subject": "New arrivals for you",
    "preheader": "Special prices until the end of the month",
    "from": "news@example.com",
    "from_name": "Example Ltd",
    "reply_to": null,
    "recipients": 0,
    "credits": 0.0,
    "simulated": false,
    "scheduled_for": null,
    "sent_at": null,
    "created_at": "2026-11-12T10:00:00+02:00",
    "updated_at": "2026-11-12T10:05:00+02:00",
    "blocks": null,
    "target": null,
    "credits_returned": 1200.0
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.target.groups` | array<integer> | Contact group ids; empty means all contacts. |
| `data.target.opened` | string, null | yes or no: opened any campaign before. |
| `data.target.clicked` | string, null | yes or no. |
| `data.target.joined_from` | string, null | YYYY-MM-DD. |
| `data.target.joined_to` | string, null | YYYY-MM-DD. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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 | `forbidden`, `insufficient_scope` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### See campaign statistics

`GET https://api.interweb.co.mz/v1/email/campaigns/{id}/stats`

Needs `email_center:read`. Delivered, opens (pixel), clicks (signed links), unsubscribes and bounces; rates over delivered emails. `simulated` marks example numbers from test mode.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/stats" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/stats", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

See campaign statistics.

```json
{
  "data": {
    "recipients": 1200,
    "sent": 1200,
    "delivered": 1188,
    "bounced": 12,
    "opened": 520,
    "clicked": 131,
    "unsubscribed": 4,
    "open_rate": 43.77,
    "click_rate": 11.03,
    "unsubscribe_rate": 0.34,
    "bounce_rate": 1.0,
    "by_status": {
      "enviado": 1200
    },
    "top_links": [
      {
        "url": "https://example.com",
        "clicks": 131
      }
    ],
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### List campaign recipients

`GET https://api.interweb.co.mz/v1/email/campaigns/{id}/recipients`

Needs `email_center:read`. What each contact did: opened, clicked, unsubscribed or bounced.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/recipients" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/recipients');
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.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/recipients", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List campaign recipients.

```json
{
  "data": [
    {
      "email": "ana@example.org",
      "status": "sent",
      "simulated": false,
      "sent_at": "2026-11-12T10:30:00+02:00",
      "opened_at": "2026-11-12T11:02:00+02:00",
      "opens": 2,
      "clicked_at": null,
      "clicks": 0,
      "unsubscribed_at": null,
      "bounced_at": null
    }
  ],
  "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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `404`

Not found.

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

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 404 | `not_found` | Not found. |
| 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.co.mz/en/api-rules.md).


### Send a test of a campaign

`POST https://api.interweb.co.mz/v1/email/campaigns/{id}/test`

Needs `email_center:write`. One email to one address, with example variables and «[Test]» in the subject; it costs one credit, like any email.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The campaign `id`. |
| `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 |
|---|---|---|---|
| `to` | string | yes |  |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/test" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"to": "ana@example.org"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/test');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"to": "ana@example.org"}',
    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 = {"to": "ana@example.org"}
r = requests.post("https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/test", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/campaigns/00000000-0000-4000-8000-000000000000/test", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"to": "ana@example.org"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Send a test of a campaign.

```json
{
  "data": {
    "id": "5130",
    "status": "sent"
  },
  "meta": {}
}
```

| Field | Type | Description |
|---|---|---|
| `data.recipients` | integer | Recipients after the suppression list; each one is one credit. |
| `data.source` | string | portal, api or smtp. |

| 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `404`

Not found.

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

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 404 | `not_found` | Not found. |
| 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.co.mz/en/api-rules.md).


### List email contacts

`GET https://api.interweb.co.mz/v1/email/contacts`

Needs `email_center:read`. The SMS contacts that have an email address and the email-only contacts, without repeated addresses, with the subscription status.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `group` | query | string | no | Group id. |
| `subscription` | query | string | no | subscribed, unsubscribed or suppressed. |
| `opened` | query | string | no | yes or no. |
| `clicked` | query | string | no | yes or no. |
| `joined_from` | query | string | no | YYYY-MM-DD. |
| `joined_to` | query | string | no | YYYY-MM-DD. |
| `q` | query | string | no | Search. |
| `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.co.mz/v1/email/contacts" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/contacts');
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.co.mz/v1/email/contacts", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List email contacts.

```json
{
  "data": [
    {
      "email": "ana@example.org",
      "name": "Ana",
      "company": "Example Ltd",
      "source": "email_contact",
      "subscription": "subscribed",
      "created_at": "2026-11-12T09:00: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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Add an email contact

`POST https://api.interweb.co.mz/v1/email/contacts`

Needs `email_center:write`. If the address already belongs to an SMS contact, that contact is used and joins the groups. You confirm that the person agreed to receive your emails.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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 |
|---|---|---|---|
| `email` | string | yes |  |
| `name` | string | no |  |
| `company` | string | no |  |
| `groups` | array<integer> | no |  |
| `subscription` | string | no | subscribed (default) or unsubscribed. |

#### Examples

**curl**

```bash
curl -X POST "https://api.interweb.co.mz/v1/email/contacts" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Idempotency-Key: 9f1c2d3e-example" \
  -H "Content-Type: application/json" \
  -d '{"email": "ana@example.org", "name": "Ana", "groups": [3]}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/contacts');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => '{"email": "ana@example.org", "name": "Ana", "groups": [3]}',
    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 = {"email": "ana@example.org", "name": "Ana", "groups": [3]}
r = requests.post("https://api.interweb.co.mz/v1/email/contacts", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/contacts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Idempotency-Key": "9f1c2d3e-example",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"email": "ana@example.org", "name": "Ana", "groups": [3]}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `201`

Add an email contact.

```json
{
  "data": {
    "email": "ana@example.org",
    "name": "Ana",
    "company": "Example Ltd",
    "source": "email_contact",
    "subscription": "subscribed",
    "created_at": "2026-11-12T09:00:00+02:00",
    "created": true,
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### See an email contact

`GET https://api.interweb.co.mz/v1/email/contacts/{email}`

Needs `email_center:read`. With the latest campaigns it received and what it did.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `email` | path | string | yes | The address, URL-encoded. |
| `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.co.mz/v1/email/contacts/VALUE" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/contacts/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.co.mz/v1/email/contacts/VALUE", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

See an email contact.

```json
{
  "data": {
    "email": "ana@example.org",
    "name": "Ana",
    "company": "Example Ltd",
    "source": "email_contact",
    "subscription": "subscribed",
    "created_at": "2026-11-12T09:00:00+02:00",
    "campaigns": [
      {
        "campaign_id": 41,
        "sent_at": "2026-11-12T10:30:00+02:00",
        "opened": true,
        "clicked": false,
        "unsubscribed": false,
        "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).


### Change a subscription

`PUT https://api.interweb.co.mz/v1/email/contacts/{email}/subscription`

Needs `email_center:write`. `subscribed` or `unsubscribed`. Contacts who unsubscribed through the link should only be subscribed again with their consent.

#### Authentication

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

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `email` | path | string | yes | The address, URL-encoded. |
| `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 |
|---|---|---|---|
| `status` | string | yes |  |

#### Examples

**curl**

```bash
curl -X PUT "https://api.interweb.co.mz/v1/email/contacts/VALUE/subscription" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en" \
  -H "Content-Type: application/json" \
  -d '{"status": "unsubscribed"}'
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/contacts/VALUE/subscription');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_POSTFIELDS => '{"status": "unsubscribed"}',
    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 = {"status": "unsubscribed"}
r = requests.put("https://api.interweb.co.mz/v1/email/contacts/VALUE/subscription", headers=headers, json=payload, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

```javascript
const response = await fetch("https://api.interweb.co.mz/v1/email/contacts/VALUE/subscription", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.RLX_API_KEY}`,
    "Accept-Language": "en",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"status": "unsubscribed"}),
});
console.log(response.status);
console.log(await response.text());
```


#### Responses

##### `200`

Change a subscription.

```json
{
  "data": {
    "email": "ana@example.org",
    "subscription": "unsubscribed",
    "simulated": 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`

The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `404`

Not found.

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

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 404 | `not_found` | Not found. |
| 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.co.mz/en/api-rules.md).


### List contact groups

`GET https://api.interweb.co.mz/v1/email/groups`

Needs `email_center:read`. The same groups as the SMS contacts, with how many have an email address.

#### Authentication

Needs a valid key in the `Authorization: Bearer` header. Required permission: `email_center: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.co.mz/v1/email/groups" \
  -H "Authorization: Bearer $RLX_API_KEY" \
  -H "Accept-Language: en"
```

**PHP**

```php
<?php
$ch = curl_init('https://api.interweb.co.mz/v1/email/groups');
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.co.mz/v1/email/groups", headers=headers, timeout=30)
print(r.status_code)
print(r.text)
```

**JavaScript**

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


#### Responses

##### `200`

List contact groups.

```json
{
  "data": [
    {
      "id": 3,
      "name": "Customers",
      "contacts": 1200
    }
  ],
  "meta": {
    "total": 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 does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`).

```json
{
  "error": {
    "code": "forbidden",
    "message": "This account does not have Email Center active. Turn it on in the portal.",
    "correlation_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "details": {
      "reason": "email_account_required"
    }
  }
}
```

##### `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` | The account does not have Email Center active (`forbidden`, `details.reason` = `email_account_required`), or the key lacks the permission (`insufficient_scope`). |
| 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.co.mz/en/api-rules.md).
