---
name: marzsms-integration
description: >-
  Integrate MarzSms APIs: send SMS, check balance, SMS history, mobile-money
  top-up, airtime/data (MTN, Airtel, Lyca), and optional recipient name confirmation
  before airtime/data purchases. Use when building Uganda SMS or airtime features with MarzSMS.
---

# MarzSms — Full API Integration Skill

Complete reference for integrating the **MarzSMS merchant SMS, airtime, and data API**.

**Market:** Uganda (`UG` / UGX). Default numbers are `+256…`. International sending requires an International package.

- **Docs:** https://sms.wearemarz.com/docs
- **API base:** `https://sms.wearemarz.com/api/v1`
- **Auth:** HTTP Basic — `Authorization: Basic base64(api_key:api_secret)`
- **Content-Type:** `application/json`
- **Integration tools:** playground, OpenAPI, Postman, PDF — `https://sms.wearemarz.com/docs/integration-tools`

---

## Product catalog

| Product | Primary endpoints | Notes |
|---------|-------------------|-------|
| **Send SMS** | `POST /sms/send` | 1 recipient = sync `200`; ≥2 recipients = queued `202` |
| **Account balance** | `GET /account/balance` | Balance + cost per SMS |
| **SMS history** | `GET /sms/history` | Paginated (`page`, `per_page` max 100) |
| **Account top-up** | `POST /account/topup` | MTN/Airtel MoMo prompt |
| **Top-up status** | `GET /account/topup/{reference}` | Poll by UUID reference |
| **Airtime catalog** | `GET /airtime/catalog` | Networks + data bundles |
| **Detect network** | `GET /airtime/detect-network` | Preview MTN/Airtel/Lyca |
| **Buy airtime/data** | `POST /airtime` | Airtime or data bundle (shared wallet) |
| **Airtime status** | `GET /airtime/{reference}` | Poll pending Airtel data |
| **Airtime history** | `GET /airtime` | Paginated purchases |
| **Confirm recipient name** | `POST /phone-verification/verify` | Airtime/data add-on — confirm registered name |

Currency is **UGX** throughout. SMS and airtime/data share the same `account_balance`.

---

## Setup

### Environment variables

```env
MARZSMS_API_BASE=https://sms.wearemarz.com/api/v1
MARZSMS_API_KEY=sk_your_api_key
MARZSMS_API_SECRET=your_api_secret
```

### Authentication header

```http
Authorization: Basic base64(api_key:api_secret)
Accept: application/json
Content-Type: application/json
```

`Basic` value = `base64_encode("api_key:api_secret")`

API keys look like `sk_` + 32 characters. Create them in the dashboard → **Settings & Tools → API Keys**. The secret is shown once.

### Prerequisites

1. MarzSMS business account
2. API key pair from the dashboard
3. Positive account balance to send SMS (top up via dashboard or `POST /account/topup`)
4. Uganda numbers by default; upgrade to International package for non-`+256` destinations

---

## 1. Send SMS

`POST /sms/send`

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `recipient` | string | Yes | One number, or comma-separated list |
| `message` | string | Yes | SMS body (GSM-7 / Unicode length rules apply) |

### Behaviour

- **Single recipient:** sends immediately → HTTP `200`
- **Two or more recipients:** queues a bulk dispatch → HTTP `202` with `dispatch_id`
- Cost = segments × recipients × `cost_per_sms`
- 160 GSM characters ≈ 1 SMS unit; longer messages bill multiple units

### Success (single) — `200`

```json
{
  "success": true,
  "message": "SMS sent to 1/1 recipient(s)",
  "data": {
    "total_recipients": 1,
    "successful": 1,
    "failed": 0,
    "total_cost": 50,
    "currency": "UGX",
    "remaining_balance": 9950,
    "results": [
      {
        "recipient": "+256700000000",
        "status": "sent",
        "transaction_id": "uuid",
        "message_id": "AT_…",
        "cost": 50
      }
    ]
  }
}
```

### Queued (bulk) — `202`

```json
{
  "success": true,
  "queued": true,
  "message": "SMS queued for 3 recipient(s)",
  "data": {
    "dispatch_id": "uuid",
    "total_recipients": 3,
    "estimated_cost": 150,
    "currency": "UGX",
    "remaining_balance": 9850
  }
}
```

### Common errors

| HTTP | `error` | Meaning |
|------|---------|---------|
| 401 | `missing_authentication` / `invalid_credentials` | Bad or missing Basic Auth |
| 402 | `insufficient_balance` | Need more UGX |
| 403 | `international_not_allowed` | Non-UG number without International package |
| 422 | `invalid_phone_numbers` / `invalid_recipients` | Bad numbers |
| 409 | `duplicate_transaction` | Retry collision |
| 500 | `send_failed` / `queue_failed` | Provider / queue failure |

### cURL

```bash
curl -X POST "https://sms.wearemarz.com/api/v1/sms/send" \
  -u "$MARZSMS_API_KEY:$MARZSMS_API_SECRET" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"recipient":"+256700000000","message":"Hello from MarzSMS"}'
```

---

## 2. Account balance

`GET /account/balance`

```json
{
  "success": true,
  "data": {
    "balance": 10000,
    "currency": "UGX",
    "cost_per_sms": 50
  }
}
```

---

## 3. SMS history

`GET /sms/history?page=1&per_page=20`

| Query | Default | Max |
|-------|---------|-----|
| `page` | 1 | — |
| `per_page` | 20 | 100 |

Each item includes `uuid`, `recipient`, `message`, `status`, `cost`, `provider`, `sent_at`, `created_at`.

---

## 4. Account top-up (mobile money)

`POST /account/topup`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount` | number | Yes | Credit amount in UGX (`500`–`10000000`) |
| `phone_number` | string | Yes | MoMo phone: `+256…`, `256…`, or `0…` |
| `reference` | uuid | No | Client-supplied UUID (generated if omitted) |
| `description` | string | No | Max 255 chars |

A service charge (business %) is added; customer pays `total_amount` on the MoMo prompt. Credit applied after payment succeeds.

### Success — `200`

```json
{
  "success": true,
  "message": "Top-up initiated successfully. Complete the mobile money prompt to finish payment.",
  "data": {
    "transaction_id": "uuid",
    "reference": "uuid",
    "status": "pending",
    "amount": 10000,
    "charge": 300,
    "total_amount": 10300,
    "currency": "UGX",
    "phone_number": "+256700000000",
    "collection_uuid": "uuid-or-null"
  }
}
```

### Top-up status

`GET /account/topup/{reference}`

Poll until `status` is `completed` or `failed`.

---

## 5. Airtime & data

Buy MTN, Airtel, or Lyca Uganda airtime and data bundles. **Same shared wallet** as SMS.

### Networks

| Network | Airtime | Data bundles | Notes |
|---------|---------|--------------|-------|
| MTN | yes | yes | Usually immediate |
| Airtel | yes | yes | Data may be `pending` (`202`) — poll status |
| Lyca | yes | **no** | Airtime only |

Network is auto-detected from MSISDN on the provider side. You do not send a `network` field on purchase.

### Catalog

`GET /airtime/catalog` — returns networks and data bundles. Use each item's `product_id` as `bundle_id`.

### Detect network

`GET /airtime/detect-network?msisdn=256771234567` — optional preview of MTN / Airtel / Lyca.

### Purchase airtime

```json
{
  "purchase_type": "airtime",
  "msisdn": "256771234567",
  "amount": 5000,
  "reference": "optional-uuid"
}
```

### Purchase data bundle

```json
{
  "purchase_type": "bundle",
  "msisdn": "256771234567",
  "bundle_id": "RACT_UG_Data_201"
}
```

Do **not** send `amount` for bundles — price comes from the catalog.

### Status & history

- `GET /airtime/{reference}` — poll until `completed`, `failed`, or `refunded`
- `GET /airtime?page=1&per_page=20` — history

---

## 6. Confirm recipient name (airtime/data add-on)

Optional step before buying airtime or data: look up the name registered on the phone number so you can confirm it is the right recipient. Not required to purchase. **Free**.

`GET /phone-verification/service` — add-on status (`cost_ugx` is always `0`).

`POST /phone-verification/verify`

```json
{
  "phone_number": "256771234567"
}
```

Success returns `data.full_name`, `data.first_name`, `data.last_name`, and `verification_status`.

---

## Error envelope

```json
{
  "success": false,
  "message": "Human-readable explanation",
  "error": "error_code"
}
```

HTTP codes used: `401`, `402`, `403`, `404` (`topup_not_found`), `409`, `422`, `500`.

---

## Integration checklist

1. Create API keys in the dashboard; store in env — never commit secrets
2. Call `GET /account/balance` to verify auth
3. Send a test SMS to a Uganda number
4. Handle `402 insufficient_balance` and top up via API or dashboard
5. For bulk (≥2 numbers), handle `202` + `dispatch_id`
6. For airtime/data: `GET /airtime/catalog` → `POST /airtime` → poll `GET /airtime/{reference}` if pending
7. Optionally confirm the recipient name with `POST /phone-verification/verify` before airtime/data purchase
8. Import OpenAPI / Postman from `https://sms.wearemarz.com/docs/integration-tools`
9. Or open the API playground and try endpoints with your keys

---

## Code snippets

### PHP

```php
$ch = curl_init(https://sms.wearemarz.com/api/v1 . '/sms/send');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD => $apiKey . ':' . $apiSecret,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'recipient' => '+256700000000',
        'message' => 'Hello from MarzSMS',
    ]),
]);
$response = curl_exec($ch);
```

Airtime purchase (same auth pattern):

```php
$ch = curl_init(https://sms.wearemarz.com/api/v1 . '/airtime');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD => $apiKey . ':' . $apiSecret,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'purchase_type' => 'airtime',
        'msisdn' => '256771234567',
        'amount' => 5000,
    ]),
]);
$response = curl_exec($ch);
```

### Node.js

```js
const auth = Buffer.from(`${apiKey}:${apiSecret}`).toString('base64');
const res = await fetch(`${baseUrl}/sms/send`, {
  method: 'POST',
  headers: {
    Authorization: `Basic ${auth}`,
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify({
    recipient: '+256700000000',
    message: 'Hello from MarzSMS',
  }),
});
```

Airtime purchase:

```js
const airtimeRes = await fetch(`${baseUrl}/airtime`, {
  method: 'POST',
  headers: {
    Authorization: `Basic ${auth}`,
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify({
    purchase_type: 'airtime',
    msisdn: '256771234567',
    amount: 5000,
  }),
});
```

### Python

```python
import requests
from requests.auth import HTTPBasicAuth

r = requests.post(
    f"{base_url}/sms/send",
    auth=HTTPBasicAuth(api_key, api_secret),
    json={"recipient": "+256700000000", "message": "Hello from MarzSMS"},
)
```

Airtime purchase:

```python
r = requests.post(
    f"{base_url}/airtime",
    auth=HTTPBasicAuth(api_key, api_secret),
    json={"purchase_type": "airtime", "msisdn": "256771234567", "amount": 5000},
)
```

---

## Do / Don't

**Do**
- Use HTTPS and Basic Auth on every request
- Normalize Uganda numbers to `+256…` when possible
- Treat bulk sends as async (`202`)
- Poll top-up status with the same `reference` you sent
- For airtime/data: load catalog first; poll purchase status when response is `pending` / `202`
- Use catalog `product_id` as `bundle_id`; omit `amount` for bundles
- Optionally confirm the registered name on a number before airtime/data purchase

**Don't**
- Hardcode API secrets in client-side JS
- Assume international sending without the International package
- Ignore `402` — top up before retrying
- Forget that multi-recipient sends are queued, not immediate
- Send `amount` together with `bundle_id`, or send `network` on purchase

---

## Support

- Docs: https://sms.wearemarz.com/docs
- Email: info@sms.wearemarz.com
- Phone: +256 759 983 853 / +256 781 230 949