{
  "info": {
    "_postman_id": "6f6c9b9e-6b7a-4f0a-9a8a-mosms-api-0001",
    "name": "MoSMS Developer API",
    "description": "# MoSMS Developer API\n\nSend SMS (and soon WhatsApp) programmatically through MoSMS — a multi-tenant SMS/WhatsApp messaging platform for Tanzania.\n\n## Base URL\n\n- `https://mosms.co.tz/api`\n\nSet the collection variable **`baseUrl`** if you ever need to point requests elsewhere.\n\n## Authentication\n\nMoSMS uses **Bearer token** authentication (Laravel Sanctum personal access tokens) — there is no separate `api_key` parameter.\n\n1. Call **Auth → Login** (or **Auth → Register** for a brand-new account) with your email/password.\n2. Copy the `token` from the response.\n3. Send it on every subsequent request as a header:\n\n   ```\n   Authorization: Bearer <token>\n   ```\n\nThis collection is pre-wired to do this automatically: **Login** and **Register** each run a test script that saves the returned token into the collection variable `token`, and every other request inherits Bearer auth from the collection root using `{{token}}`. Just run Login once, then everything else works.\n\n## Permissions (RBAC)\n\nEach tenant user has a granular permission set (returned as `permissions` by **Login**/**Me**). Calling an endpoint your token lacks permission for returns `403`. Common permissions referenced below: `messages.send`, `bulk_sms.send`, `group_sms.send`, `messages`, `messages.view`, `messages.check_status`, `messages.cancel_scheduled`, `messages.delete`, `dashboard`, `contacts*`, `groups*`, `templates*`, `sms_logs`, `delivery_reports`, `purchase_sms*`, `events`, `events.add`, `events.view`, `events.edit`, `events.delete`, `events.manage_invitees`, `events.manage_payments`, `events.send_sms`.\n\n## Rate limits\n\nThe three **Send SMS** endpoints (`/sms/send`, `/sms/send-bulk`, `/sms/send-group/{group}`) are throttled to **60 requests/minute per tenant**. Exceeding this returns `429 Too Many Requests`. No other endpoint is currently rate-limited.\n\n## Phone numbers\n\nSend numbers in any reasonable local or international format — `0712345678`, `+255712345678`, or `255712345678` all work; MoSMS normalizes to the `255XXXXXXXXX` MSISDN format server-side.\n\n## Credit / segment cost\n\nSMS is billed per **segment**: 1 credit per 160 characters for plain GSM-7 text, or 1 credit per 70 characters once the message contains any Unicode (emoji, non-Latin script). Sends that would exceed your remaining balance return `422` with the shape:\n```json\n{ \"message\": \"Insufficient SMS balance...\", \"balance\": 0, \"required\": 2 }\n```\n\n## Common error format\n\nValidation errors follow Laravel's default shape:\n```json\n{\n  \"message\": \"The given data was invalid.\",\n  \"errors\": { \"to\": [\"The to field is required.\"] }\n}\n```\nOther errors are `{ \"message\": \"...\" }` with an appropriate HTTP status (`401`, `403`, `404`, `422`, `429`, `500`, `502`).\n\n## Folders in this collection\n\n1. **Auth** — register, login, password reset, profile\n2. **Dashboard & Balance** — account overview, SMS credit balance\n3. **Send SMS** — single, bulk, and group sends\n4. **Messages** — history, status polling, cancel, delete\n5. **Delivery Reports & Logs** — raw gateway-side records\n6. **Contacts** — CRUD + CSV/vCard import\n7. **Groups** — CRUD + membership management\n8. **Templates** — reusable message bodies with placeholders\n9. **SMS Packages & Purchases** — buying more credits\n10. **Events** — invitees, pledge/payment tracking, and multi-stage SMS/WhatsApp campaigns\n11. **Webhooks (reference only)** — inbound callbacks MoSMS itself receives; not callable by clients\n12. **Coming Soon — WhatsApp Send API** — planned endpoints, not yet live\n",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{token}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://mosms.co.tz/api",
      "type": "string"
    },
    {
      "key": "token",
      "value": "",
      "type": "string"
    },
    {
      "key": "group_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "message_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "contact_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "template_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "purchase_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "event_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "invitee_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "payment_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "stage_id",
      "value": "1",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Auth",
      "description": "Account creation, login, and password management. All endpoints here except **Get Current User / Profile / Logout** are public (no token required).",
      "item": [
        {
          "name": "Register (Create Account)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/register",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "register"
              ]
            },
            "description": "Self-service sign-up. Creates a new **tenant organization** plus its first admin user, and returns a Bearer token you can use immediately — no separate login call needed.\n\nThe `org_name` is slugified to derive a unique organization slug; if the slug or email is already taken you get a `422`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"org_name\": \"Acme Traders\",\n  \"phone\": \"255712345678\",\n  \"name\": \"Jane Doe\",\n  \"email\": \"jane@acme.co.tz\",\n  \"password\": \"SecurePass123\",\n  \"password_confirmation\": \"SecurePass123\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) {",
                  "    const json = pm.response.json();",
                  "    if (json.token) { pm.collectionVariables.set('token', json.token); }",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Registration successful.\",\n  \"token\": \"1|abcdEFGH1234567890tokenExample\",\n  \"user_type\": \"user\",\n  \"user\": {\n    \"id\": 12,\n    \"name\": \"Jane Doe\",\n    \"email\": \"jane@acme.co.tz\",\n    \"role\": \"admin\",\n    \"tenant\": {\n      \"id\": 7,\n      \"name\": \"Acme Traders\",\n      \"slug\": \"acme-traders\"\n    }\n  }\n}"
            },
            {
              "name": "422 Organization exists",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"An organization with this name already exists.\"\n}"
            }
          ]
        },
        {
          "name": "Login",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "login"
              ]
            },
            "description": "Unified login for both tenant (developer) accounts and platform Super Admins — it checks the admin table first, then falls back to tenant users. Returns a **Sanctum Bearer token** to use on every subsequent request as `Authorization: Bearer <token>`, plus the caller's `permissions` list (granular RBAC — check this before assuming an endpoint is reachable).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"jane@acme.co.tz\",\n  \"password\": \"SecurePass123\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "    const json = pm.response.json();",
                  "    if (json.token) { pm.collectionVariables.set('token', json.token); }",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"token\": \"1|abcdEFGH1234567890tokenExample\",\n  \"user_type\": \"user\",\n  \"user\": {\n    \"id\": 12,\n    \"name\": \"Jane Doe\",\n    \"email\": \"jane@acme.co.tz\",\n    \"role\": \"admin\",\n    \"tenant\": {\n      \"id\": 7,\n      \"name\": \"Acme Traders\",\n      \"slug\": \"acme-traders\"\n    }\n  },\n  \"permissions\": [\n    \"dashboard\",\n    \"messages\",\n    \"messages.send\",\n    \"bulk_sms.send\",\n    \"group_sms.send\",\n    \"contacts\",\n    \"groups\",\n    \"templates\"\n  ]\n}"
            },
            {
              "name": "401 Invalid credentials",
              "originalRequest": {},
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Invalid credentials.\"\n}"
            },
            {
              "name": "403 Organization inactive",
              "originalRequest": {},
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Your organization is inactive. Contact support.\"\n}"
            }
          ]
        },
        {
          "name": "Forgot Password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/forgot-password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forgot-password"
              ]
            },
            "description": "Requests a password-reset email. Always returns `200` regardless of whether the email exists, to avoid leaking which emails are registered.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"jane@acme.co.tz\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"If that email exists, a reset link has been sent.\"\n}"
            }
          ]
        },
        {
          "name": "Reset Password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/reset-password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reset-password"
              ]
            },
            "description": "Completes a password reset using the `token` emailed by **Forgot Password**. Tokens expire after 60 minutes.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"the-token-from-the-email\",\n  \"email\": \"jane@acme.co.tz\",\n  \"password\": \"NewSecurePass456\",\n  \"password_confirmation\": \"NewSecurePass456\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Your password has been reset. You can now log in.\"\n}"
            },
            {
              "name": "422 Invalid/expired token",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"This reset link is invalid or has already been used.\"\n}"
            }
          ]
        },
        {
          "name": "Get Current User",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "me"
              ]
            },
            "description": "Returns the authenticated user's profile, tenant, and permission list. Useful right after login to confirm the token works and to know what the caller is allowed to do."
          }
        },
        {
          "name": "Get Profile",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/profile",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "profile"
              ]
            },
            "description": "Returns the authenticated user's profile (id, name, email, role, tenant)."
          }
        },
        {
          "name": "Update Profile",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/profile",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "profile"
              ]
            },
            "description": "Updates the caller's name/email, and optionally changes password (requires `current_password`).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Jane Doe\",\n  \"email\": \"jane@acme.co.tz\",\n  \"current_password\": \"SecurePass123\",\n  \"new_password\": \"EvenMoreSecure789\",\n  \"new_password_confirmation\": \"EvenMoreSecure789\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Logout",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/logout",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "logout"
              ]
            },
            "description": "Revokes the Bearer token used on this request. Call this when a client (script, app) is done using its token."
          }
        }
      ]
    },
    {
      "name": "Dashboard & Balance",
      "description": "Account-level summary data and SMS credit balance.",
      "item": [
        {
          "name": "Get SMS Balance",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/balance",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "balance"
              ]
            },
            "description": "Returns the tenant's remaining SMS credit balance. Check this before sending to avoid a `422` insufficient-balance error — 1 credit = 1 SMS segment (see **Send Single SMS** for segment math)."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"sms_balance\": 1520\n}"
            }
          ]
        },
        {
          "name": "Get Dashboard Summary",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/dashboard",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "dashboard"
              ]
            },
            "description": "Aggregate counters for the tenant: users, contacts, groups, messages (total + today), plus upcoming holiday/birthday auto-wish previews."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"users_count\": 3,\n  \"contacts_count\": 240,\n  \"groups_count\": 5,\n  \"messages_count\": 18432,\n  \"messages_today\": 56,\n  \"upcoming_holiday\": null,\n  \"upcoming_birthdays\": [\n    {\n      \"id\": 4,\n      \"name\": \"Amina Suleiman\",\n      \"phone\": \"255712345678\",\n      \"birth_date\": \"1990-07-20\",\n      \"days_until\": 3,\n      \"upcoming_age\": 36\n    }\n  ],\n  \"birthday_wishes_enabled\": true\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Send SMS",
      "description": "The core sending endpoints — single message, bulk batch, or a saved group.",
      "item": [
        {
          "name": "Send Single SMS",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/sms/send",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms",
                "send"
              ]
            },
            "description": "Sends one SMS immediately (or schedules it for later with `scheduled_at`). Requires the `messages.send` permission.\n\n**Body parameters**\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `to` | string | yes | Any reasonable format (`0712345678`, `+255712345678`, `255712345678`) — normalized server-side. |\n| `text` | string | required unless `template_id` given | Max 4000 chars. |\n| `contact_id` | integer | no | Links the message to an existing contact. |\n| `template_id` | integer | no | Renders a saved template (`{name}`, `{phone}`, `{email}` placeholders) instead of `text`. |\n| `scheduled_at` | datetime | no | ISO date-time in the future — queues the send instead of sending now. |\n\n**Credit cost**: GSM-7 messages cost 1 credit per 160 characters; messages containing Unicode (emoji, non-Latin scripts) cost 1 credit per 70 characters. The gateway rejects the request with `422` if your balance can't cover the segment count.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"0712345678\",\n  \"text\": \"Hello from MoSMS!\",\n  \"scheduled_at\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created — sent",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 1042,\n    \"to_number\": \"255712345678\",\n    \"from_sender\": \"MOSMS\",\n    \"body\": \"Hello from MoSMS!\",\n    \"status\": \"PENDING_ACCEPTED\",\n    \"delivery_status\": null,\n    \"delivered_at\": null,\n    \"delivery_error\": null,\n    \"gateway_message_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n    \"gateway_response\": {\n      \"messageId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\n    },\n    \"sent_at\": \"2026-07-17T10:00:00.000000Z\",\n    \"scheduled_at\": null,\n    \"contact\": null,\n    \"group\": null,\n    \"user\": null,\n    \"created_at\": \"2026-07-17T10:00:00.000000Z\"\n  }\n}"
            },
            {
              "name": "422 Insufficient balance",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Insufficient SMS balance. Message requires 2 credit(s) but you have 0 remaining.\",\n  \"balance\": 0,\n  \"required\": 2\n}"
            }
          ]
        },
        {
          "name": "Send Bulk SMS",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/sms/send-bulk",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms",
                "send-bulk"
              ]
            },
            "description": "Queues up to any number of distinct messages for background delivery (does **not** send synchronously — you'll get a `202` immediately, then track each message via **List Messages**). Requires `bulk_sms.send`.\n\n**Body parameters**\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `messages` | array | yes | At least 1 item: `{ \"to\": \"...\", \"text\": \"...\", \"contact_id\": null }`. |\n| `messages[].to` | string | yes | Recipient number. |\n| `messages[].text` | string | required unless `template_id` given | Per-message text (max 4000 chars). |\n| `template_id` | integer | no | Applies one template to **every** message in the array, with per-contact placeholder rendering. |\n| `scheduled_at` | datetime | no | Delays the whole batch. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    {\n      \"to\": \"0712345678\",\n      \"text\": \"Hi Juma, your order has shipped!\"\n    },\n    {\n      \"to\": \"0765432109\",\n      \"text\": \"Hi Amina, your order has shipped!\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "202 Accepted",
              "originalRequest": {},
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Bulk SMS queued for delivery.\",\n  \"count\": 2\n}"
            },
            {
              "name": "422 Insufficient balance",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Insufficient SMS balance. Messages require 2 credit(s) but you have 0 remaining.\",\n  \"balance\": 0,\n  \"required\": 2\n}"
            }
          ]
        },
        {
          "name": "Send SMS to Group",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/sms/send-group/{{group_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms",
                "send-group",
                "{{group_id}}"
              ]
            },
            "description": "Sends (queues) the same message to every contact in a saved **Group**. Requires `group_sms.send`.\n\n**Path parameter**: `group_id` — the group's numeric ID (see **Groups** folder).\n\n**Body**: same `text` / `template_id` / `scheduled_at` fields as Send Single SMS. Returns `422` if the group has no contacts or the balance can't cover `contacts_count × segments_per_message`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"text\": \"Reminder: our office is closed tomorrow for the public holiday.\",\n  \"template_id\": null,\n  \"scheduled_at\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "202 Accepted",
              "originalRequest": {},
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Group SMS to 'VIP Customers' queued for delivery.\",\n  \"contacts\": 84\n}"
            },
            {
              "name": "422 Empty group",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Group has no contacts.\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Messages",
      "description": "Message history, delivery-status polling, cancellation, and deletion.",
      "item": [
        {
          "name": "List Messages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "messages"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "",
                  "description": "Filter by delivery/send status",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Search phone number or message body",
                  "disabled": true
                },
                {
                  "key": "date_from",
                  "value": "",
                  "description": "YYYY-MM-DD",
                  "disabled": true
                },
                {
                  "key": "date_to",
                  "value": "",
                  "description": "YYYY-MM-DD",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "Pagination page number",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated history of every message sent (or queued/scheduled) by the tenant, newest first.\n\n**Query parameters** (all optional): `status` (e.g. `PENDING_ACCEPTED`, `DELIVERED`, `scheduled`, `cancelled`), `search` (matches `to_number` or `body`), `date_from`, `date_to` (both `YYYY-MM-DD`, filtered on `sent_at`), `page`."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": [\n    {\n      \"id\": 1042,\n      \"to_number\": \"255712345678\",\n      \"from_sender\": \"MOSMS\",\n      \"body\": \"Hello from MoSMS!\",\n      \"status\": \"DELIVERED\",\n      \"delivery_status\": \"DELIVERED_TO_HANDSET\",\n      \"delivered_at\": \"2026-07-17T10:00:05.000000Z\",\n      \"delivery_error\": null,\n      \"gateway_message_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n      \"sent_at\": \"2026-07-17T10:00:00.000000Z\",\n      \"scheduled_at\": null,\n      \"created_at\": \"2026-07-17T10:00:00.000000Z\"\n    }\n  ],\n  \"links\": {\n    \"first\": \"{{baseUrl}}/messages?page=1\",\n    \"last\": \"{{baseUrl}}/messages?page=12\",\n    \"prev\": null,\n    \"next\": \"{{baseUrl}}/messages?page=2\"\n  },\n  \"meta\": {\n    \"current_page\": 1,\n    \"last_page\": 12,\n    \"per_page\": 20,\n    \"total\": 231\n  }\n}"
            }
          ]
        },
        {
          "name": "Get Message",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/messages/{{message_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "messages",
                "{{message_id}}"
              ]
            },
            "description": "Fetches a single message by ID, with its linked contact/group/user (when present)."
          }
        },
        {
          "name": "Check Delivery Status",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/messages/{{message_id}}/check-status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "messages",
                "{{message_id}}",
                "check-status"
              ]
            },
            "description": "Actively polls the upstream SMS gateway for the latest delivery report on this message and updates `status` / `delivery_status` / `delivered_at` / `delivery_error` accordingly, then returns the refreshed message. Requires `messages.check_status`.\n\nErrors: `422` if the message has no `gateway_message_id` (e.g. it's still `scheduled`), `404` if the gateway has no report yet, `429` if the gateway rate-limited us, `502` if the gateway is unreachable."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 1042,\n    \"to_number\": \"255712345678\",\n    \"status\": \"DELIVERED\",\n    \"delivery_status\": \"DELIVERED_TO_HANDSET\",\n    \"delivered_at\": \"2026-07-17T10:00:05.000000Z\",\n    \"delivery_error\": null\n  }\n}"
            },
            {
              "name": "422 No gateway ID",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"This message has no gateway ID \\u2014 status cannot be checked.\"\n}"
            }
          ]
        },
        {
          "name": "Cancel Scheduled Message",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/messages/{{message_id}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "messages",
                "{{message_id}}",
                "cancel"
              ]
            },
            "description": "Cancels a message that hasn't been sent yet (`status === 'scheduled'`). Requires `messages.cancel_scheduled`. Returns `422` if the message already sent."
          },
          "response": [
            {
              "name": "422 Not scheduled",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Only scheduled messages can be cancelled.\"\n}"
            }
          ]
        },
        {
          "name": "Bulk Delete Messages",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/messages/bulk",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "messages",
                "bulk"
              ]
            },
            "description": "Permanently deletes multiple message records by ID. Requires `messages.delete`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    1042,\n    1043,\n    1044\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Messages deleted.\",\n  \"count\": 3\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Delivery Reports & Logs",
      "description": "Raw records proxied from the upstream SMS gateway, for reconciliation.",
      "item": [
        {
          "name": "List Delivery Reports",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/delivery-reports",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "delivery-reports"
              ],
              "query": [
                {
                  "key": "messageId",
                  "value": "",
                  "description": "Filter to a single gateway message ID",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "25",
                  "description": "Max results to return",
                  "disabled": true
                }
              ]
            },
            "description": "Proxies straight through to the upstream carrier's native delivery-report endpoint and returns its raw JSON — useful for reconciling against gateway-side records directly. Requires `delivery_reports`.\n\nQuery: `messageId` (optional, filters to one message), `limit` (default 25)."
          },
          "response": [
            {
              "name": "502 Gateway unreachable",
              "originalRequest": {},
              "status": "Bad Gateway",
              "code": 502,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Unable to reach the SMS gateway. Please try again later.\"\n}"
            }
          ]
        },
        {
          "name": "List SMS Logs",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/sms-logs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms-logs"
              ]
            },
            "description": "Tenant-scoped raw send logs proxied from the upstream gateway (distinct from **List Messages**, which is MoSMS's own local record). Requires `sms_logs`."
          }
        }
      ]
    },
    {
      "name": "Contacts",
      "description": "Manage the tenant's contact book, including CSV/vCard bulk import.",
      "item": [
        {
          "name": "List Contacts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/contacts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts"
              ],
              "query": [
                {
                  "key": "search",
                  "value": "",
                  "description": "Search by name or phone",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated contact list. Query: `search` (matches name/phone), `page`."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": [\n    {\n      \"id\": 501,\n      \"name\": \"Juma Hassan\",\n      \"phone\": \"255712345678\",\n      \"email\": null,\n      \"birth_date\": null,\n      \"groups\": [],\n      \"created_at\": \"2026-01-10T08:00:00.000000Z\",\n      \"updated_at\": \"2026-01-10T08:00:00.000000Z\"\n    }\n  ],\n  \"links\": {},\n  \"meta\": {\n    \"current_page\": 1,\n    \"last_page\": 3,\n    \"per_page\": 20,\n    \"total\": 58\n  }\n}"
            }
          ]
        },
        {
          "name": "Create Contact",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/contacts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts"
              ]
            },
            "description": "`name` and `phone` are required; `phone` must be unique per tenant (normalized before the uniqueness check). `birth_date` (if set) powers the birthday auto-SMS feature.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Juma Hassan\",\n  \"phone\": \"0712345678\",\n  \"email\": \"juma@example.com\",\n  \"birth_date\": \"1988-03-14\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 501,\n    \"name\": \"Juma Hassan\",\n    \"phone\": \"255712345678\",\n    \"email\": \"juma@example.com\",\n    \"birth_date\": \"1988-03-14\",\n    \"groups\": []\n  }\n}"
            }
          ]
        },
        {
          "name": "Get Contact",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/contacts/{{contact_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts",
                "{{contact_id}}"
              ]
            },
            "description": "Fetch one contact with its groups loaded."
          }
        },
        {
          "name": "Update Contact",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/contacts/{{contact_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts",
                "{{contact_id}}"
              ]
            },
            "description": "All fields optional (`sometimes` validation) — send only what changes.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Juma Hassan Mnyonge\",\n  \"phone\": \"0712345678\",\n  \"email\": \"juma@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete Contact",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/contacts/{{contact_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts",
                "{{contact_id}}"
              ]
            },
            "description": "Permanently deletes one contact."
          }
        },
        {
          "name": "Bulk Delete Contacts",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/contacts/bulk",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts",
                "bulk"
              ]
            },
            "description": "Deletes multiple contacts by ID.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [\n    501,\n    502,\n    503\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Contacts deleted.\",\n  \"count\": 3\n}"
            }
          ]
        },
        {
          "name": "Import Contacts (CSV)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/contacts/import/csv",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts",
                "import",
                "csv"
              ]
            },
            "description": "Multipart upload. CSV columns: `name, phone, email, birth_date` (header row required). Max 5MB.",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": "",
                  "description": "CSV file — columns: name, phone, email, birth_date"
                }
              ]
            }
          }
        },
        {
          "name": "Import Contacts (vCard)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/contacts/import/vcf",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "contacts",
                "import",
                "vcf"
              ]
            },
            "description": "Multipart upload of a `.vcf` vCard export (e.g. exported from a phone's contacts app). Max 5MB.",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": "",
                  "description": "vCard (.vcf) file"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Groups",
      "description": "Organize contacts into groups for targeted group sends.",
      "item": [
        {
          "name": "List Groups",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/groups",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups"
              ]
            },
            "description": "Paginated list of contact groups, with `contacts_count` on each."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": [\n    {\n      \"id\": 9,\n      \"name\": \"VIP Customers\",\n      \"description\": \"Top-tier clients\",\n      \"contacts_count\": 84,\n      \"created_at\": \"2026-02-01T00:00:00.000000Z\",\n      \"updated_at\": \"2026-02-01T00:00:00.000000Z\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Create Group",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/groups",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups"
              ]
            },
            "description": "Creates an empty group — add contacts afterwards via **Add Contacts to Group**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"VIP Customers\",\n  \"description\": \"Top-tier clients\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 9,\n    \"name\": \"VIP Customers\",\n    \"description\": \"Top-tier clients\",\n    \"contacts_count\": 0\n  }\n}"
            }
          ]
        },
        {
          "name": "Get Group",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/groups/{{group_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups",
                "{{group_id}}"
              ]
            },
            "description": "Fetch one group with its `contacts_count`."
          }
        },
        {
          "name": "Update Group",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/groups/{{group_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups",
                "{{group_id}}"
              ]
            },
            "description": "Rename a group or change its description.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"VIP Customers (Tier 1)\",\n  \"description\": \"Top-tier clients\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete Group",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/groups/{{group_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups",
                "{{group_id}}"
              ]
            },
            "description": "Deletes the group (contacts themselves are not deleted, only the grouping)."
          }
        },
        {
          "name": "List Group Contacts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/groups/{{group_id}}/contacts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups",
                "{{group_id}}",
                "contacts"
              ]
            },
            "description": "Paginated list of contacts that belong to this group."
          }
        },
        {
          "name": "Add Contacts to Group",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/groups/{{group_id}}/contacts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups",
                "{{group_id}}",
                "contacts"
              ]
            },
            "description": "Attaches existing contacts (by ID) to the group — safe to call repeatedly, duplicates are ignored.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"contact_ids\": [\n    501,\n    502,\n    503\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Contacts added to group.\"\n}"
            }
          ]
        },
        {
          "name": "Remove Contacts from Group",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/groups/{{group_id}}/contacts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "groups",
                "{{group_id}}",
                "contacts"
              ]
            },
            "description": "Detaches contacts from the group without deleting the contacts themselves.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"contact_ids\": [\n    501\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Contacts removed from group.\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Templates",
      "description": "Reusable message bodies with `{name}`/`{phone}`/`{email}` placeholders.",
      "item": [
        {
          "name": "List Templates",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "templates"
              ]
            },
            "description": "Paginated list of saved message templates."
          }
        },
        {
          "name": "Create Template",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "templates"
              ]
            },
            "description": "`body` supports `{name}`, `{phone}`, and `{email}` placeholders, filled in per-contact when the template is used in a send.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Order Shipped\",\n  \"body\": \"Hi {name}, your order has shipped! Track it at example.com/track.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 3,\n    \"name\": \"Order Shipped\",\n    \"body\": \"Hi {name}, your order has shipped! Track it at example.com/track.\",\n    \"created_at\": \"2026-01-05T00:00:00.000000Z\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Get Template",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/templates/{{template_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "templates",
                "{{template_id}}"
              ]
            },
            "description": "Fetch one template."
          }
        },
        {
          "name": "Update Template",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/templates/{{template_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "templates",
                "{{template_id}}"
              ]
            },
            "description": "Updates a template's name and/or body.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Order Shipped\",\n  \"body\": \"Hi {name}, your order #{phone} has shipped!\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete Template",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/templates/{{template_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "templates",
                "{{template_id}}"
              ]
            },
            "description": "Deletes a template."
          }
        }
      ]
    },
    {
      "name": "SMS Packages & Purchases",
      "description": "Buying more SMS/WhatsApp credits — manual (admin-approved) or online via Pesapal.",
      "item": [
        {
          "name": "List SMS Packages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/sms-packages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms-packages"
              ]
            },
            "description": "Public price tiers available for purchase (no `purchase_sms` permission required to view)."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"Starter\",\n      \"min_quantity\": 100,\n      \"max_quantity\": 999,\n      \"price_per_sms\": 20\n    },\n    {\n      \"id\": 2,\n      \"name\": \"Growth\",\n      \"min_quantity\": 1000,\n      \"max_quantity\": 9999,\n      \"price_per_sms\": 18\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Purchase SMS Credits (Manual)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/sms-purchases",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms-purchases"
              ]
            },
            "description": "Submits a manual top-up request (e.g. after a bank/mobile-money transfer) for admin approval — credits are **not** added instantly. `sms_quantity` must be ≥100 for SMS or ≥10 for `channel: whatsapp`. Only one pending manual request per channel is allowed at a time.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sms_quantity\": 1000,\n  \"transaction_reference\": \"MPESA-XJ29KD8\",\n  \"payer_phone\": \"0712345678\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Purchase request submitted successfully.\",\n  \"data\": {\n    \"id\": 88,\n    \"sms_quantity\": 1000,\n    \"total_amount\": 20000,\n    \"status\": \"pending\"\n  }\n}"
            },
            {
              "name": "422 Pending request exists",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"You already have a pending SMS purchase of 500 credits (Ref: MPESA-AB12CD) awaiting approval. Please wait for it to be approved before creating a new one.\",\n  \"pending_id\": 87\n}"
            }
          ]
        },
        {
          "name": "Purchase via Pesapal",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/sms-purchases/pesapal",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms-purchases",
                "pesapal"
              ]
            },
            "description": "Initiates an online card/mobile-money checkout through Pesapal. Returns a `redirect_url` — send the user there to complete payment, then poll **Check Pesapal Purchase Status**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sms_quantity\": 1000\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Pesapal checkout initiated.\",\n  \"data\": {\n    \"payment_id\": 89,\n    \"redirect_url\": \"https://pay.pesapal.com/...\",\n    \"order_tracking_id\": \"...\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Check Pesapal Purchase Status",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/sms-purchases/{{purchase_id}}/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms-purchases",
                "{{purchase_id}}",
                "status"
              ]
            },
            "description": "Polls Pesapal for the latest status of a pending online payment and updates the local record."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 89,\n    \"status\": \"approved\",\n    \"payment_status_description\": \"COMPLETED\",\n    \"confirmation_code\": \"ABC123\",\n    \"payment_method\": \"pesapal\",\n    \"sms_quantity\": 1000,\n    \"total_amount\": 18000\n  }\n}"
            }
          ]
        },
        {
          "name": "List Purchase History",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/sms-purchases",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms-purchases"
              ]
            },
            "description": "Paginated history of the tenant's SMS/WhatsApp credit purchases."
          }
        },
        {
          "name": "Download Purchase Receipt",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/sms-purchases/{{purchase_id}}/receipt",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "sms-purchases",
                "{{purchase_id}}",
                "receipt"
              ]
            },
            "description": "Streams a PDF receipt for an **approved** purchase. Only available once `status === 'approved'`."
          }
        }
      ]
    },
    {
      "name": "Events",
      "description": "Weddings, harambees, and fundraisers — invitees, pledge/payment tracking, and multi-stage SMS/WhatsApp campaigns (invitations, reminders, thank-yous).",
      "item": [
        {
          "name": "List Events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events"
              ],
              "query": [
                {
                  "key": "search",
                  "value": "",
                  "description": "Search by event name",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "draft|active|completed|cancelled",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated list of events (weddings, harambees/fundraisers, etc.) with computed SMS-cost stats. Requires `events`. Query: `search` (matches name), `status` (`draft`/`active`/`completed`/`cancelled`), `page`."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": [\n    {\n      \"id\": 12,\n      \"name\": \"Amina & Juma's Wedding\",\n      \"type\": \"wedding\",\n      \"description\": null,\n      \"event_date\": \"2026-09-12\",\n      \"venue\": \"Kigamboni Beach Hall\",\n      \"target_amount\": 5000000,\n      \"status\": \"active\",\n      \"invitees_count\": 84,\n      \"payments_count\": 12,\n      \"sms_stages_count\": 3,\n      \"total_pledged\": 3200000,\n      \"total_collected\": 1450000,\n      \"total_sms\": 252,\n      \"sent_sms\": 84,\n      \"sms_cost\": 5040,\n      \"created_at\": \"2026-06-01T09:00:00.000000Z\"\n    }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Create Event",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events"
              ]
            },
            "description": "**Body parameters**\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `name` | string | yes | |\n| `type` | string | yes | `wedding`, `harambee`, `fundraiser`, or `other`. |\n| `event_date` | date | yes | |\n| `description` | string | no | Max 2000 chars. |\n| `venue` | string | no | |\n| `target_amount` | number | no | Fundraising target, if any. |\n| `status` | string | no | `draft`, `active`, `completed`, or `cancelled` (default `draft`). |\n| `public_message` | string | no | Shown on the public RSVP/pledge page. |\n| `bank_name` / `bank_account_number` / `bank_account_name` | string | no | For displaying payment instructions publicly. |\n| `pledge_deadline` | date | no | |\n\nRequires `events.add`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Amina & Juma's Wedding\",\n  \"type\": \"wedding\",\n  \"event_date\": \"2026-09-12\",\n  \"venue\": \"Kigamboni Beach Hall\",\n  \"target_amount\": 5000000\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 12,\n    \"name\": \"Amina & Juma's Wedding\",\n    \"type\": \"wedding\",\n    \"event_date\": \"2026-09-12\",\n    \"status\": \"draft\",\n    \"invitees_count\": 0\n  }\n}"
            }
          ]
        },
        {
          "name": "Get Event",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}"
              ]
            },
            "description": "Fetch one event, including `total_pledged`/`total_collected` totals. Requires `events.view`."
          }
        },
        {
          "name": "Update Event",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}"
              ]
            },
            "description": "Same fields as Create Event, all optional (`sometimes` validation). Requires `events.edit`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"active\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete Event",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}"
              ]
            },
            "description": "Requires `events.delete`. This also removes the event's invitees, payments, and SMS stages."
          }
        },
        {
          "name": "List Event Invitees",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/invitees",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "invitees"
              ],
              "query": [
                {
                  "key": "search",
                  "value": "",
                  "description": "Search by name or phone",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated. Query: `search` (matches name/phone), `page`. Requires `events.view`."
          }
        },
        {
          "name": "Add Invitee",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/invitees",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "invitees"
              ]
            },
            "description": "`name` and `phone` required; `pledge_amount` and `contact_id` (link to an existing Contact) optional. Requires `events.manage_invitees`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Fatuma Rajabu\",\n  \"phone\": \"0712345678\",\n  \"pledge_amount\": 50000\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 501,\n    \"event_id\": 12,\n    \"name\": \"Fatuma Rajabu\",\n    \"phone\": \"255712345678\",\n    \"pledge_amount\": 50000,\n    \"paid_amount\": 0,\n    \"status\": \"pending\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Import Invitees (from Contacts/Group)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/invitees/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "invitees",
                "import"
              ]
            },
            "description": "Bulk-adds invitees from existing Contacts and/or a Group — pass `contact_ids` (array) and/or `group_id` (at least one required). Duplicates (already-invited contacts) are skipped. Requires `events.manage_invitees`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"group_id\": 9,\n  \"contact_ids\": [\n    501,\n    502\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"42 invitees imported.\",\n  \"imported\": 42\n}"
            }
          ]
        },
        {
          "name": "Import Invitees (CSV/vCard file)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/invitees/import/file",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "invitees",
                "import",
                "file"
              ]
            },
            "description": "Multipart upload — CSV (with `name`/`phone`/`email` columns) or `.vcf` vCard export. Max 5MB. Requires `events.manage_invitees`.",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": "",
                  "description": "CSV or .vcf file"
                }
              ]
            }
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"38 invitees imported, 4 skipped.\",\n  \"imported\": 38,\n  \"skipped\": 4\n}"
            }
          ]
        },
        {
          "name": "Update Invitee",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/invitees/{{invitee_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "invitees",
                "{{invitee_id}}"
              ]
            },
            "description": "All fields optional. `status` is one of `pending`, `invited`, `confirmed`, `declined`. Requires `events.manage_invitees`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"confirmed\",\n  \"pledge_amount\": 60000\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Remove Invitee",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/invitees/{{invitee_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "invitees",
                "{{invitee_id}}"
              ]
            },
            "description": "Requires `events.manage_invitees`."
          }
        },
        {
          "name": "List Event Payments",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/payments",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "payments"
              ]
            },
            "description": "Paginated, newest first, with invitee + recorder loaded. Requires `events.view`."
          }
        },
        {
          "name": "Record a Payment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/payments",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "payments"
              ]
            },
            "description": "Records a pledge payment against an invitee. If the event has `auto_payment_sms` enabled, a thank-you SMS is sent automatically. Requires `events.manage_payments`.\n\n**Body parameters**\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `event_invitee_id` | integer | yes | Must belong to this event. |\n| `amount` | number | yes | > 0. |\n| `payment_date` | date | yes | |\n| `payment_method` | string | no | `cash`, `mobile_money`, `bank`, or `other`. |\n| `reference` | string | no | e.g. mobile money transaction ID. |\n| `notes` | string | no | |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"event_invitee_id\": 501,\n  \"amount\": 50000,\n  \"payment_method\": \"mobile_money\",\n  \"reference\": \"MPESA-XJ29KD8\",\n  \"payment_date\": \"2026-07-18\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 88,\n    \"event_id\": 12,\n    \"event_invitee_id\": 501,\n    \"amount\": 50000,\n    \"payment_method\": \"mobile_money\",\n    \"reference\": \"MPESA-XJ29KD8\",\n    \"payment_date\": \"2026-07-18\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Update Payment",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/payments/{{payment_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "payments",
                "{{payment_id}}"
              ]
            },
            "description": "All fields optional. Requires `events.manage_payments`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 55000\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete Payment",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/payments/{{payment_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "payments",
                "{{payment_id}}"
              ]
            },
            "description": "Requires `events.manage_payments`."
          }
        },
        {
          "name": "List SMS Stages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/sms-stages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "sms-stages"
              ]
            },
            "description": "Each stage includes computed `stage_total_sms`/`stage_remaining`/`stage_cost` based on the current invitee count. Requires `events.view`."
          }
        },
        {
          "name": "Create SMS Stage",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/sms-stages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "sms-stages"
              ]
            },
            "description": "Defines a message (SMS or WhatsApp) to send to all invitees, once or on a recurring schedule. **SMS and WhatsApp are always separate stages** — there's no combined \"both\" channel; create one stage per channel if you want to reach invitees on both. Requires `events.send_sms`.\n\n**Body parameters**\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `stage_type` | string | yes | `invitation`, `reminder`, `pledge_reminder`, `non_pledge_reminder`, `thank_you`, `rsvp`, or `custom`. |\n| `name` | string | yes | Internal label. |\n| `body` | string | yes | Max 4000 chars. Supports placeholders like `{invitee_name}`, `{event_name}`, `{event_date}`, `{venue}`, `{pledge_amount}`, `{event_link}`. |\n| `channel` | string | no | `sms` (default) or `whatsapp`. |\n| `whatsapp_template_id` | integer | required if `channel=whatsapp` | Must be an approved WhatsApp template. |\n| `send_at` | datetime | no | First/only send time. |\n| `frequency` | string | no | `once`, `daily`, `weekly`, or `monthly`. |\n| `end_at` | date | no | Required alongside a recurring `frequency`; must be ≥ `send_at`. |\n| `status` | string | no | `draft` or `scheduled`. |\n\nCreating a stage does **not** send it — call **Send SMS Stage** (or let it fire on `send_at`).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"stage_type\": \"invitation\",\n  \"name\": \"Save the Date\",\n  \"channel\": \"sms\",\n  \"body\": \"You're invited to {invitee_name}'s wedding on {event_date} at {venue}!\",\n  \"send_at\": \"2026-08-01T09:00:00\",\n  \"frequency\": \"once\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {},
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"id\": 7,\n    \"event_id\": 12,\n    \"stage_type\": \"invitation\",\n    \"name\": \"Save the Date\",\n    \"status\": \"draft\",\n    \"sent_count\": 0\n  }\n}"
            }
          ]
        },
        {
          "name": "Update SMS Stage",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/sms-stages/{{stage_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "sms-stages",
                "{{stage_id}}"
              ]
            },
            "description": "Same fields as Create SMS Stage. `status` may additionally be set to `cancelled`. Requires `events.send_sms`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"scheduled\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete SMS Stage",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/sms-stages/{{stage_id}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "sms-stages",
                "{{stage_id}}"
              ]
            },
            "description": "Requires `events.send_sms`."
          }
        },
        {
          "name": "Send SMS Stage",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/events/{{event_id}}/sms-stages/{{stage_id}}/send",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{event_id}}",
                "sms-stages",
                "{{stage_id}}",
                "send"
              ]
            },
            "description": "Queues the stage's message to every eligible invitee now. WhatsApp stages are metered against the tenant's WhatsApp wallet (not the SMS balance) and billed per conversation. SMS stages are checked against the SMS credit balance the same way as **Send SMS to Group**. Requires `events.send_sms`."
          },
          "response": [
            {
              "name": "200 OK — queued",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"Sending 84 message(s). You'll be notified when complete.\"\n}"
            },
            {
              "name": "422 Already sent",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"This stage has already been sent.\"\n}"
            },
            {
              "name": "422 No eligible invitees",
              "originalRequest": {},
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"No invitees are eligible for this stage.\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "Webhooks (reference only)",
      "description": "Inbound callbacks that MoSMS itself receives from the SMS carrier and Meta. Not callable by API clients — included so the origin of delivery-status updates is clear.",
      "item": [
        {
          "name": "Delivery Callback (carrier → MoSMS)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/webhooks/delivery",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks",
                "delivery"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messageId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"status\": \"DELIVERED_TO_HANDSET\",\n  \"error\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            },
            "description": "**Not called by developers.** This is the endpoint the upstream SMS carrier calls to push delivery-status updates for messages MoSMS sent on your behalf. Documented here only so you understand where `delivery_status` on a message comes from — you don't need to (and can't meaningfully) call this yourself.\n\nThere is currently no outbound webhook that notifies *your* systems when a message's status changes — poll **Check Delivery Status** or **List Messages** instead."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {},
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"message\": \"OK\"\n}"
            }
          ]
        },
        {
          "name": "WhatsApp Webhook — Verify (Meta → MoSMS)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=...&hub.challenge=...",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks",
                "whatsapp"
              ],
              "query": [
                {
                  "key": "hub.mode",
                  "value": "subscribe"
                },
                {
                  "key": "hub.verify_token",
                  "value": "..."
                },
                {
                  "key": "hub.challenge",
                  "value": "..."
                }
              ]
            },
            "auth": {
              "type": "noauth"
            },
            "description": "**Not called by developers.** Meta's webhook-verification handshake for the tenant's WhatsApp Cloud API integration."
          }
        },
        {
          "name": "WhatsApp Webhook — Receive (Meta → MoSMS)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/webhooks/whatsapp",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks",
                "whatsapp"
              ]
            },
            "auth": {
              "type": "noauth"
            },
            "description": "**Not called by developers.** Meta pushes WhatsApp delivery statuses (`sent`/`delivered`/`read`/`failed`) and inbound message replies here. Verified via `X-Hub-Signature-256` HMAC when `WHATSAPP_APP_SECRET` is configured."
          }
        }
      ]
    },
    {
      "name": "Coming Soon — WhatsApp Send API",
      "description": "WhatsApp sending currently only exists in MoSMS's internal dashboard (session auth), not this token-based developer API. These items sketch the likely shape of a future public endpoint for planning purposes — **not live, subject to change**.",
      "item": [
        {
          "name": "[Planned] Send Single WhatsApp Message",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/whatsapp/send",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "whatsapp",
                "send"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"0712345678\",\n  \"whatsapp_template_id\": 4,\n  \"text\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "**⚠️ Not yet available on the public API.** WhatsApp sending today only exists in the internal MoSMS dashboard (session-authenticated web routes), metered against the tenant's `whatsapp_balance` wallet — mirroring **Send Single SMS**, but via `App\\Services\\WhatsApp\\WhatsAppCloudService` instead of the SMS gateway.\n\nThis item documents the **shape a future token-authenticated `/api/whatsapp/send` endpoint would likely take**, based on the existing dashboard implementation, so client integrations can be planned ahead of time. Confirm with the MoSMS team before building against it."
          }
        },
        {
          "name": "[Planned] Send Bulk WhatsApp Broadcast",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/whatsapp/send-bulk",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "whatsapp",
                "send-bulk"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"numbers\": [\n    \"0712345678\",\n    \"0765432109\"\n  ],\n  \"whatsapp_template_id\": 4\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "**⚠️ Not yet available on the public API.** Modeled on the internal bulk/group broadcast flow (`channel: whatsapp`), which requires an **approved, no-variable WhatsApp template** and is metered 1 WhatsApp credit per recipient. Confirm with the MoSMS team before building against it."
          }
        }
      ]
    }
  ]
}