> For the complete documentation index, see [llms.txt](https://developers.textcus.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.textcus.com/overview/email-api.md).

# Email API

This endpoint is used for Email API when sending emails to a user on your platform.

Send transactional and bulk emails from your own applications. Authentication, the response envelope and error handling match the TextCus WhatsApp API, so an existing integration carries over almost unchanged.

**Base URL:** `https://api.textcus.com/api/v2`

***

### Authentication

Send your API key as a Bearer token on every request. You'll find it in your dashboard under **Settings**.

```
Authorization: Bearer YOUR_API_KEY
```

A missing or unknown key returns `401`.

### Response shape

Every response uses the same envelope:

```json
{ "status": true, "message": "Email accepted for delivery", "data": { } }
```

`status` is `false` for any failure, and `message` is safe to show to your own users.

***

### Before you can send

Two things must be true before an API call will deliver:

1. **The `from` address must be verified on your account.** Add it in the dashboard under **Email → Sender Emails**, then click the link we email to that address. Unverified addresses are rejected with `403`. `GET /email/senders` lists what you have.
2. **You need email credits.** One credit sends one email to one recipient, so a message to 50 recipients costs 50 credits. Buy credits in the dashboard under **Email → Buy Credits**, or check what you have with `GET /email/balance`.

Credits are taken when the request is accepted, not when each message lands. A campaign that fails at the gateway is visible on the status endpoint.

***

### POST /email/send

```bash
curl -X POST https://api.textcus.com/api/v2/email/send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "from_name": "Your Company",
    "to": ["kofi@example.com", "ama@example.com"],
    "subject": "Your order has shipped",
    "message": "<p>Hi %firstname%, your order is on its way.</p>"
  }'
```

| Field       | Required | Notes                                                                                 |
| ----------- | -------- | ------------------------------------------------------------------------------------- |
| `from`      | yes      | A verified sender address on your account.                                            |
| `from_name` | no       | Display name shown to the recipient.                                                  |
| `to`        | yes      | One address, a comma-separated string, an array of addresses, or an array of objects. |
| `subject`   | yes      | Up to 255 characters.                                                                 |
| `message`   | yes      | HTML or plain text. A plain-text alternative is generated automatically.              |
| `reply_to`  | no       | Overrides the reply address for this message.                                         |

#### Recipient formats

All three of these are accepted:

```json
"to": "kofi@example.com, ama@example.com"
```

```json
"to": ["kofi@example.com", "ama@example.com"]
```

```json
"to": [
  { "email": "kofi@example.com", "name": "Kofi Mensah" },
  { "email": "ama@example.com",  "name": "Ama Owusu"  }
]
```

Duplicates are removed and invalid addresses are dropped before you are charged.

#### Personalisation

These placeholders are replaced per recipient, in both the subject and the body:

| Placeholder   | Replaced with                 |
| ------------- | ----------------------------- |
| `%name%`      | The recipient's full name     |
| `%firstname%` | The first word of that name   |
| `%lastname%`  | The last word of that name    |
| `%email%`     | The recipient's email address |

Names only resolve when you pass recipients as objects. With plain addresses the placeholders resolve to an empty string, so write copy that reads correctly either way.

#### Response

```json
{
  "status": true,
  "message": "Email accepted for delivery",
  "data": {
    "message_id": 1042,
    "recipients": 2,
    "credits_used": 2,
    "email_balance": 4831
  }
}
```

Keep `message_id` — it is how you look the campaign up later.

#### Errors

| Code  | Meaning                                                      |
| ----- | ------------------------------------------------------------ |
| `401` | Missing or unknown API key.                                  |
| `403` | The `from` address is not a verified sender on your account. |
| `422` | Validation failed, or no valid recipient was supplied.       |
| `402` | Not enough email credits for the number of recipients.       |
| `503` | No email gateway is currently available. Retry shortly.      |

***

### GET /email/status/{message\_id}

```bash
curl https://api.textcus.com/api/v2/email/status/1042 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "status": true,
  "message": "Message status retrieved",
  "data": {
    "message_id": 1042,
    "subject": "Your order has shipped",
    "sender": "hello@yourdomain.com",
    "sent_at": "2026-09-09T10:14:22+00:00",
    "delivery_status": "Delivered",
    "recipients": [
      {
        "email": "kofi@example.com",
        "status": "Delivered",
        "delivered_at": "2026-09-09T10:14:31+00:00",
        "opened_at": "2026-09-09T11:02:07+00:00",
        "error": null
      },
      {
        "email": "ama@example.com",
        "status": "Failed",
        "delivered_at": null,
        "opened_at": null,
        "error": "Mailbox does not exist"
      }
    ],
    "summary": { "total": 2, "delivered": 1, "failed": 1, "opened": 1 }
  }
}
```

#### Delivery statuses

| Status       | Meaning                                                            |
| ------------ | ------------------------------------------------------------------ |
| `Pending`    | Queued, not yet handed to the gateway.                             |
| `Sent`       | Accepted by the gateway; the recipient's server has not confirmed. |
| `Delivered`  | The recipient's mail server accepted the message.                  |
| `Failed`     | Bounced or rejected. `error` says why.                             |
| `Complained` | The recipient marked it as spam.                                   |

Engagement fields (`opened_at`, and clicks on the dashboard) depend on the gateway reporting them, so treat an absent value as "not reported" rather than "did not happen".

The campaign-level `delivery_status` summarises the recipients: `Delivered` once every recipient has reported, `Failed` if all of them failed, and `Sent` while some are still open.

***

### GET /email/messages

Your campaign history, newest first.

```bash
curl "https://api.textcus.com/api/v2/email/messages?per_page=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "status": true,
  "message": "Messages retrieved",
  "data": [
    {
      "message_id": 1042,
      "subject": "Your order has shipped",
      "sender": "hello@yourdomain.com",
      "channel": "api",
      "recipients": 2,
      "delivered": 1,
      "failed": 1,
      "delivery_status": "Delivered",
      "sent_at": "2026-09-09T10:14:22+00:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 4, "per_page": 25, "total": 87 }
}
```

`channel` is `api` for messages sent through this API and `dashboard` for those composed in the web app.

***

### GET /email/senders

```bash
curl https://api.textcus.com/api/v2/email/senders \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "status": true,
  "message": "Sender addresses retrieved",
  "data": [
    {
      "id": 7,
      "email": "hello@yourdomain.com",
      "verified": true,
      "verified_at": "2026-08-30T09:12:44+00:00"
    }
  ]
}
```

Only addresses with `"verified": true` can be used as `from`. Adding and verifying a sender is done in the dashboard.

***

### GET /email/balance

```bash
curl https://api.textcus.com/api/v2/email/balance \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "status": true,
  "message": "Email balance retrieved",
  "data": { "email_balance": 4831, "email_rate": "0.0400" }
}
```

`email_balance` is the number of emails you can still send. `email_rate` is what one credit costs your account in GHS, and is what a custom top-up is priced against.

***

### Deliverability

The platform sends through a commercial gateway, but the reputation of the domain in `from` is yours. Two things matter most:

* **Authenticate your domain.** Publish SPF and DKIM records for the domain you send from. Without them, mailbox providers are far more likely to place your mail in spam.
* **Only send to people who asked.** Bounces and spam complaints are recorded per recipient and are visible on the status endpoint — watch them, and stop mailing addresses that hard bounce.

***

### A worked example

```php
<?php

$response = Http::withToken(config('services.textcus.key'))
    ->post('https://api.textcus.com/api/v2/email/send', [
        'from' => 'hello@yourdomain.com',
        'from_name' => 'Your Company',
        'to' => [
            ['email' => $order->customer_email, 'name' => $order->customer_name],
        ],
        'subject' => "Order {$order->reference} has shipped",
        'message' => view('emails.shipped', ['order' => $order])->render(),
    ]);

if (! $response->json('status')) {
    Log::warning('TextCus email rejected', ['message' => $response->json('message')]);
}

$messageId = $response->json('data.message_id');
```
