{
  "openapi": "3.1.0",
  "info": {
    "title": "SendAfrica API",
    "version": "1.0.0",
    "description": "Tanzania-first SMS platform API.\n\n- Base URL: https://api.sendafrica.online\n- Server-to-server auth: `X-API-Key: SA-...` header. User-context endpoints accept JWT Bearer tokens.\n- All responses use the envelope `{ success, data, error, request_id, timestamp }`.\n- Tanzanian phone numbers (`0712345678`, `+255712345678`) are normalized to E.164 automatically; non-TZ numbers are rejected.\n- Sends are synchronous (`status: \"sent\"` = submitted to gateway); delivery updates arrive via HMAC-SHA256-signed webhooks (`X-SendAfrica-Signature`).\n- Billing is per SMS part in credits (GSM-7: 160 chars/part; UCS-2: 70 chars/part).\n- Pass an `Idempotency-Key` header to make retries safe; successful responses are cached for 24h.\n- Default sender ID is **SendAfrika**. Custom sender IDs must be registered; unregistered IDs are silently replaced by the fallback sender.",
    "contact": { "name": "SendAfrica", "url": "https://docs.sendafrica.online" }
  },
  "servers": [{ "url": "https://api.sendafrica.online", "description": "Production" }],
  "security": [{ "ApiKeyAuth": [] }, { "BearerAuth": [] }],
  "tags": [
    { "name": "SMS" },
    { "name": "Credits" },
    { "name": "Contacts" },
    { "name": "Campaigns" },
    { "name": "Payments" },
    { "name": "Vouchers" },
    { "name": "Rates" },
    { "name": "Notifications" },
    { "name": "Auth" }
  ],
  "paths": {
    "/v1/sms/": {
      "post": {
        "tags": ["SMS"],
        "summary": "Send a single SMS",
        "operationId": "sendSms",
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "$ref": "#/components/schemas/SmsSendRequest" }
          }
        },
        "responses": {
          "200": {
            "description": "Message submitted to the gateway",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Envelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": { "$ref": "#/components/schemas/SmsSendResult" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": { "$ref": "#/components/responses/InvalidPhone" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/sms/bulk": {
      "post": {
        "tags": ["SMS"],
        "summary": "Send the same message to up to 100 recipients",
        "operationId": "sendBulkSms",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/BulkSmsRequest" } }
          }
        },
        "responses": {
          "200": {
            "description": "Per-recipient results",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Envelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": { "$ref": "#/components/schemas/BulkSmsResult" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/sms/logs": {
      "get": {
        "tags": ["SMS"],
        "summary": "List message logs",
        "operationId": "listMessages",
        "parameters": [
          { "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
          { "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 25, "maximum": 100 } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["sent", "delivered", "failed", "pending"] } }
        ],
        "responses": {
          "200": {
            "description": "Paginated message logs",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Envelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "messages": { "type": "array", "items": { "$ref": "#/components/schemas/MessageLog" } },
                            "page": { "type": "integer" },
                            "total": { "type": "integer" }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits/balance": {
      "get": {
        "tags": ["Credits"],
        "summary": "Current credit balance",
        "operationId": "getBalance",
        "responses": {
          "200": {
            "description": "Balance",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Envelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "account_id": { "type": "string" },
                            "balance": { "type": "integer" }
                          },
                          "required": ["account_id", "balance"]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits/history": {
      "get": {
        "tags": ["Credits"],
        "summary": "Transaction ledger (append-only)",
        "operationId": "getCreditHistory",
        "parameters": [
          { "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
          { "name": "per_page", "in": "query", "schema": { "type": "integer", "default": 50 } }
        ],
        "responses": {
          "200": {
            "description": "Transactions: purchases, deductions, refunds, grants",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Envelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "transactions": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": { "type": "string" },
                                  "type": { "type": "string", "enum": ["purchase", "deduct", "refund", "grant"] },
                                  "status": { "type": "string" },
                                  "amount": { "type": "integer" },
                                  "balance_after": { "type": "integer" },
                                  "description": { "type": "string" },
                                  "created_at": { "type": "string", "format": "date-time" }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/contact-lists": {
      "get": {
        "tags": ["Contacts"],
        "summary": "List contact lists",
        "operationId": "listContactLists",
        "responses": { "200": { "description": "Contact lists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PagedContactLists" } } } } }
      },
      "post": {
        "tags": ["Contacts"],
        "summary": "Create a contact list",
        "operationId": "createContactList",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "description": { "type": "string" }
                },
                "required": ["name"]
              }
            }
          }
        },
        "responses": { "201": { "description": "Created list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactListEnvelope" } } } } }
      }
    },
    "/v1/contact-lists/{listId}": {
      "parameters": [{ "name": "listId", "in": "path", "required": true, "schema": { "type": "string" } }],
      "get": {
        "tags": ["Contacts"],
        "summary": "Get a contact list with contacts",
        "operationId": "getContactList",
        "responses": { "200": { "description": "The list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactListEnvelope" } } } } }
      },
      "put": {
        "tags": ["Contacts"],
        "summary": "Update a contact list",
        "operationId": "updateContactList",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "name": { "type": "string" }, "description": { "type": "string" } }
              }
            }
          }
        },
        "responses": { "200": { "description": "Updated list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactListEnvelope" } } } } }
      },
      "delete": {
        "tags": ["Contacts"],
        "summary": "Delete a contact list",
        "operationId": "deleteContactList",
        "responses": { "200": { "description": "Deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/v1/campaigns": {
      "get": {
        "tags": ["Campaigns"],
        "summary": "List campaigns",
        "operationId": "listCampaigns",
        "responses": { "200": { "description": "Campaigns", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PagedCampaigns" } } } } }
      },
      "post": {
        "tags": ["Campaigns"],
        "summary": "Create a campaign (scheduled or immediate)",
        "operationId": "createCampaign",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "message": { "type": "string" },
                  "contact_list_id": { "type": "string", "format": "uuid" },
                  "sender_id": { "type": "string", "description": "Registered sender ID; omit for platform default SendAfrika" },
                  "scheduled_at": { "type": "string", "format": "date-time", "description": "Omit or set in the past to send immediately" }
                },
                "required": ["name", "message", "contact_list_id"]
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Campaign created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CampaignEnvelope" } } } },
          "402": { "$ref": "#/components/responses/InsufficientCredits" }
        }
      }
    },
    "/v1/payments": {
      "post": {
        "tags": ["Payments"],
        "summary": "Buy a fixed credit package via mobile money",
        "operationId": "createPackagePayment",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "package_id": { "type": "integer" },
                  "provider": { "type": "string", "enum": ["snippe"] },
                  "phone": { "type": "string", "description": "Verified payer phone (USSD prompt is pushed here)" }
                },
                "required": ["package_id", "provider", "phone"]
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Payment order created (status pending until USSD confirmation)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentOrderEnvelope" } } } },
          "409": { "description": "`phone_not_verified` — verify the phone via send-phone-otp + verify-phone first" }
        }
      }
    },
    "/v1/vouchers/": {
      "post": {
        "tags": ["Vouchers"],
        "summary": "Create a pay-as-you-go voucher order for any amount",
        "operationId": "createVoucher",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": { "type": "integer", "description": "Amount in TZS (minimum from rate card)" },
                  "provider": { "type": "string", "enum": ["snippe"] },
                  "phone": { "type": "string" }
                },
                "required": ["amount", "provider", "phone"]
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Voucher order created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentOrderEnvelope" } } } }
        }
      }
    },
    "/v1/vouchers/rate": {
      "get": {
        "tags": ["Vouchers"],
        "summary": "Tiered TZS-per-credit rate card",
        "operationId": "getVoucherRates",
        "responses": {
          "200": {
            "description": "Tiers: larger top-ups buy cheaper credits",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Envelope" },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "min_amount_tzs": { "type": "integer" },
                            "tiers": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "max_amount_tzs": { "type": ["integer", "null"] },
                                  "rate_tzs_per_credit": { "type": "number" }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/packages": {
      "get": {
        "tags": ["Payments"],
        "summary": "Fixed credit packages (public)",
        "operationId": "listPackages",
        "security": [],
        "responses": { "200": { "description": "Packages", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/v1/templates": {
      "get": {
        "tags": ["Payments"],
        "summary": "Compose-UI templates (public)",
        "operationId": "listTemplates",
        "security": [],
        "responses": { "200": { "description": "Templates", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/v1/rates": {
      "get": {
        "tags": ["Rates"],
        "summary": "International rate card (public)",
        "operationId": "getRates",
        "security": [],
        "responses": { "200": { "description": "Per-country rates", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/v1/rates/{country}": {
      "get": {
        "tags": ["Rates"],
        "summary": "Rate for one country (public)",
        "operationId": "getCountryRate",
        "security": [],
        "parameters": [{ "name": "country", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Country rate", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/v1/notifications": {
      "get": {
        "tags": ["Notifications"],
        "summary": "List notifications (low balance, payments, campaigns, announcements)",
        "operationId": "listNotifications",
        "responses": { "200": { "description": "Notifications", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/v1/auth/register": {
      "post": {
        "tags": ["Auth"],
        "summary": "Register an account",
        "operationId": "register",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "minLength": 8 },
                  "business_name": { "type": "string" }
                },
                "required": ["email", "password"]
              }
            }
          }
        },
        "responses": { "201": { "description": "Account created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/v1/auth/login": {
      "post": {
        "tags": ["Auth"],
        "summary": "Log in (returns JWT pair)",
        "operationId": "login",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string" }
                },
                "required": ["email", "password"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JWT access (900s) + refresh (7d, rotating)",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JwtPairEnvelope" } } }
          }
        }
      }
    },
    "/v1/auth/refresh": {
      "post": {
        "tags": ["Auth"],
        "summary": "Rotate refresh token, get new access token",
        "operationId": "refreshToken",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "refresh_token": { "type": "string" } },
                "required": ["refresh_token"]
              }
            }
          }
        },
        "responses": { "200": { "description": "New token pair", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JwtPairEnvelope" } } } } }
      }
    },
    "/v1/auth/me": {
      "get": {
        "tags": ["Auth"],
        "summary": "Current account profile",
        "operationId": "getMe",
        "responses": { "200": { "description": "Profile incl. email_verified/phone_verified flags", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    },
    "/health": {
      "get": {
        "tags": ["Auth"],
        "summary": "Service health",
        "operationId": "health",
        "security": [],
        "responses": { "200": { "description": "{ status: \"ok\" }", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "X-API-Key" },
      "BearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": { "type": "string" },
        "description": "Unique key per logical operation; replays within 24h return the cached response. A still-in-flight replay returns 409 request_in_progress."
      }
    },
    "schemas": {
      "Envelope": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "error": { "type": ["object", "null"], "properties": { "code": { "type": "string" }, "message": { "type": "string" } } },
          "request_id": { "type": "string", "format": "uuid" },
          "timestamp": { "type": "string", "format": "date-time" }
        },
        "required": ["success", "request_id", "timestamp"]
      },
      "SmsSendRequest": {
        "type": "object",
        "properties": {
          "to": { "type": "string", "description": "TZ mobile number: 07xxxxxxxx or +2557xxxxxxxxx" },
          "message": { "type": "string", "maxLength": 1600 },
          "from": { "type": "string", "maxLength": 11, "description": "Registered sender ID; omit for platform default SendAfrika" }
        },
        "required": ["to", "message"]
      },
      "SmsSendResult": {
        "type": "object",
        "properties": {
          "message_id": { "type": "string" },
          "status": { "type": "string", "enum": ["sent", "failed"], "description": "sent = accepted by gateway; delivery arrives later via webhook" },
          "cost": { "type": "string", "examples": ["TZS 35.00"] },
          "credits_used": { "type": "integer" }
        },
        "required": ["message_id", "status", "credits_used"]
      },
      "BulkSmsRequest": {
        "type": "object",
        "properties": {
          "to": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 100 },
          "message": { "type": "string" },
          "from": { "type": "string", "maxLength": 11 }
        },
        "required": ["to", "message"]
      },
      "BulkSmsResult": {
        "type": "object",
        "properties": {
          "sent": { "type": "integer" },
          "failed": { "type": "integer" },
          "results": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "properties": {
                    "to": { "type": "string" },
                    "status": { "type": "string", "const": "sent" },
                    "message_id": { "type": "string" },
                    "credits_used": { "type": "integer" }
                  }
                },
                {
                  "type": "object",
                  "properties": {
                    "to": { "type": "string" },
                    "status": { "type": "string", "const": "failed" },
                    "error": { "type": "string" }
                  }
                }
              ]
            }
          }
        }
      },
      "MessageLog": {
        "type": "object",
        "properties": {
          "message_id": { "type": "string" },
          "to": { "type": "string" },
          "from": { "type": "string" },
          "status": { "type": "string", "enum": ["sent", "delivered", "pending", "failed"] },
          "cost": { "type": "string" },
          "credits_used": { "type": "integer" },
          "sms_parts": { "type": "integer" },
          "encoding": { "type": "string", "enum": ["GSM-7", "UCS-2"] },
          "created_at": { "type": "string", "format": "date-time" },
          "delivered_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "ContactList": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "contact_count": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "PagedContactLists": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "data": { "type": "object", "properties": { "lists": { "type": "array", "items": { "$ref": "#/components/schemas/ContactList" } } } }
            }
          }
        ]
      },
      "ContactListEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/ContactList" } } }
        ]
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "message": { "type": "string" },
          "status": { "type": "string", "enum": ["draft", "scheduled", "running", "completed", "failed"] },
          "contact_list_id": { "type": "string", "format": "uuid" },
          "total_contacts": { "type": "integer" },
          "sent_count": { "type": "integer" },
          "delivered_count": { "type": "integer" },
          "failed_count": { "type": "integer" },
          "scheduled_at": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "PagedCampaigns": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "data": { "type": "object", "properties": { "campaigns": { "type": "array", "items": { "$ref": "#/components/schemas/Campaign" } } } }
            }
          }
        ]
      },
      "CampaignEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Campaign" } } }
        ]
      },
      "PaymentOrder": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "confirmed", "failed"] },
          "source": { "type": "string" },
          "provider": { "type": "string" },
          "amount": { "type": "integer", "description": "TZS" },
          "currency": { "type": "string", "const": "TZS" },
          "credit_amount": { "type": "integer" },
          "checkout_message": { "type": "string" }
        }
      },
      "PaymentOrderEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/PaymentOrder" } } }
        ]
      },
      "JwtPairEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/Envelope" },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "properties": {
                  "access_token": { "type": "string" },
                  "refresh_token": { "type": "string" },
                  "expires_in": { "type": "integer", "const": 900 }
                }
              }
            }
          }
        ]
      }
    },
    "responses": {
      "InsufficientCredits": {
        "description": "402 insufficient_credits — top up before retrying; not transient",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } }
      },
      "InvalidPhone": {
        "description": "403 invalid_phone_number — reject the input; don't retry",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } }
      },
      "RateLimited": {
        "description": "429 rate_limit_exceeded — honor the Retry-After header, then retry once",
        "headers": { "Retry-After": { "schema": { "type": "integer" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } }
      }
    }
  }
}
