openapi: 3.0.3
info:
  title: SMS Maroc API
  description: |
    # SMS Maroc — L'API SMS #1 au Maroc
    
    Envoyez des SMS, WhatsApp OTP et Telegram OTP à travers une API REST simple et rapide.
    
    ## Authentication
    All API requests require an `Authorization: Bearer YOUR_API_KEY` header.
    
    ## Base URL
    ```
    https://api.smsmaroc.ma/v1
    ```
    
    ## Rate Limiting
    Rate limits are applied per API key. Default: 100 requests/minute.
    Response headers include `X-RateLimit-Limit` and `Retry-After` on 429 responses.
    
    ## Error Format
    ```json
    {
      "error": {
        "code": "ERROR_CODE",
        "message": "Human-readable message",
        "docs": "https://smsmaroc.ma/en/docs#errors"
      }
    }
    ```
  version: "1.0.0"
  contact:
    name: SMS Maroc Support
    email: support@smsmaroc.ma
    url: https://smsmaroc.ma/support
  license:
    name: Commercial

servers:
  - url: https://api.smsmaroc.ma/v1
    description: Production
  - url: https://api-staging.smsmaroc.ma/v1
    description: Staging / Testing

security:
  - BearerAuth: []

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: Your SMS Maroc API key

  schemas:
    Message:
      type: object
      properties:
        id:
          type: string
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        to:
          type: string
          example: "+212612345678"
        channel:
          type: string
          enum: [sms, whatsapp, telegram]
        body:
          type: string
          example: "Votre code est 1234"
        sender_id:
          type: string
          example: "MonApp"
        status:
          type: string
          enum: [queued, sent, delivered, failed, undeliverable]
        cost:
          type: object
          properties:
            eur: { type: number, example: 0.19 }
            mad: { type: number, example: 2.09 }
        segments:
          type: integer
          example: 1
        created_at:
          type: string
          format: date-time
        delivered_at:
          type: string
          format: date-time

    SendMessageRequest:
      type: object
      required: [to, message]
      properties:
        to:
          type: string
          description: "Destination phone in E.164 format"
          example: "+212612345678"
        message:
          type: string
          description: "Message content (max 1600 chars)"
          example: "Bonjour! Votre commande #1234 est confirmée."
        from:
          type: string
          description: "Sender ID (alphanumeric, max 11 chars)"
          example: "MonApp"
        channel:
          type: string
          enum: [sms, whatsapp, telegram]
          default: sms
        cascade:
          type: boolean
          description: "Try cheapest channel first, fallback if not delivered"
          default: false
        metadata:
          type: object
          description: "Custom key-value pairs, returned in DLR webhooks"
          example: { "order_id": "ORD-12345", "customer_ref": "C-789" }

    BulkSendRequest:
      type: object
      required: [messages]
      properties:
        messages:
          type: array
          maxItems: 10000
          items:
            type: object
            required: [to, message]
            properties:
              to: { type: string, example: "+212612345678" }
              message: { type: string, example: "Bonjour {{name}}!" }
        from: { type: string }
        channel:
          type: string
          enum: [sms, whatsapp, telegram]
          default: sms

    VerifySendRequest:
      type: object
      required: [to]
      properties:
        to:
          type: string
          example: "+212612345678"
        channel:
          type: string
          enum: [sms, whatsapp, telegram]
          default: sms
        code_length:
          type: integer
          minimum: 4
          maximum: 8
          default: 6
        expiry:
          type: integer
          description: "Code validity in seconds"
          default: 600
        brand:
          type: string
          description: "Brand name shown in OTP message"
          example: "MaSociété"
        template:
          type: string
          description: "Custom OTP message. Use {{code}} and {{brand}} placeholders."
          example: "Code {{brand}}: {{code}}. Ne partagez jamais ce code."

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
            docs: { type: string }

paths:
  /health:
    get:
      summary: Health check
      description: Public service health endpoint. This route is served from the API root, not the /v1 prefix.
      operationId: healthCheck
      security: []
      servers:
        - url: https://api.smsmaroc.ma
          description: Production root
        - url: https://api-staging.smsmaroc.ma
          description: Staging root
      responses:
        "200":
          description: Service health
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  service: { type: string, example: smsmaroc-api }
                  version: { type: string, example: 1.0.0 }
                  time: { type: string, format: date-time }

  /messages:
    post:
      summary: Send a message
      description: Send a single SMS, WhatsApp OTP, or Telegram OTP
      operationId: sendMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
            examples:
              sms:
                summary: Simple SMS
                value:
                  to: "+212612345678"
                  message: "Votre commande #1234 est expédiée!"
                  from: "MaMarque"
              whatsapp_otp:
                summary: WhatsApp OTP (cheapest)
                value:
                  to: "+212612345678"
                  message: "Votre code: 123456"
                  channel: "whatsapp"
              cascade:
                summary: Cascade (try WhatsApp → Telegram → SMS)
                value:
                  to: "+212612345678"
                  message: "Votre code: 123456"
                  cascade: true
      responses:
        "202":
          description: Message queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "402":
          description: Insufficient balance
        "429":
          description: Rate limit exceeded
    get:
      summary: List messages
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [queued, sent, delivered, failed, undeliverable]
        - name: channel
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: from_date
          in: query
          description: Start date (ISO 8601)
          schema:
            type: string
        - name: to_date
          in: query
          description: End date (ISO 8601)
          schema:
            type: string
      responses:
        "200":
          description: List of messages

  /messages/bulk:
    post:
      summary: Send bulk messages
      description: Send up to 10,000 messages in a single request
      operationId: sendBulk
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkSendRequest'
      responses:
        "202":
          description: Batch queued

  /messages/{id}:
    get:
      summary: Get message status
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Message details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        "404":
          description: Message not found

  /verify/send:
    post:
      summary: Send OTP verification code
      operationId: sendVerify
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifySendRequest'
      responses:
        "202":
          description: OTP sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id: { type: string }
                  to: { type: string }
                  channel: { type: string }
                  expires_at: { type: string, format: date-time }
                  status: { type: string }

  /verify/check:
    post:
      summary: Check OTP code
      operationId: checkVerify
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session_id, code]
              properties:
                session_id: { type: string }
                code: { type: string, example: "123456" }
      responses:
        "200":
          description: Code verified
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id: { type: string }
                  verified: { type: boolean }
                  verified_at: { type: string, format: date-time }
        "400":
          description: Invalid code

  /verify/{session_id}:
    get:
      summary: Get OTP verification session
      operationId: getVerifySession
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OTP session
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  to: { type: string }
                  channel: { type: string, enum: [sms, whatsapp, telegram] }
                  expires_at: { type: string, format: date-time }
                  verified_at: { type: string, format: date-time, nullable: true }
                  created_at: { type: string, format: date-time }
        "404":
          description: Session not found

  /balance:
    get:
      summary: Get account balance (alias)
      operationId: getBalance
      responses:
        "200":
          description: Current balance
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance_eur: { type: number, example: 45.23 }
                  balance_mad: { type: number, example: 497.53 }
                  currency: { type: string, example: "EUR" }
                  plan: { type: string, example: "business" }

  /billing/balance:
    get:
      summary: Get billing balance
      operationId: getBillingBalance
      responses:
        "200":
          description: Current balance
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance_eur: { type: number, example: 45.23 }
                  balance_mad: { type: number, example: 497.53 }
                  currency: { type: string, example: "EUR" }
                  plan: { type: string, example: "business" }

  /billing/transactions:
    get:
      summary: List billing transactions
      operationId: listBillingTransactions
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Billing transactions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        type: { type: string, enum: [topup, usage, refund, adjustment] }
                        amount_eur: { type: number }
                        amount_mad: { type: number }
                        balance_after_eur: { type: number }
                        description: { type: string }
                        reference: { type: string, nullable: true }
                        payment_method: { type: string, nullable: true }
                        status: { type: string }
                        created_at: { type: string, format: date-time }
                  limit: { type: integer }
                  offset: { type: integer }
                  total: { type: integer }

  /billing/plans:
    get:
      summary: List plan pricing
      operationId: listBillingPlans
      responses:
        "200":
          description: Pricing rows by plan and channel
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        plan: { type: string, example: starter }
                        channel: { type: string, enum: [sms, whatsapp, telegram] }
                        price_eur: { type: number, example: 0.22 }
                        price_mad: { type: number, example: 2.42 }

  /billing/topups:
    post:
      summary: Create balance top-up
      operationId: createBillingTopup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount_eur]
              properties:
                amount_eur:
                  type: number
                  minimum: 10
                  example: 50
                payment_method:
                  type: string
                  example: stripe
      responses:
        "201":
          description: Pending top-up
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, example: topup_abcd }
                  status: { type: string, example: pending }
                  amount_eur: { type: number, example: 50 }
                  amount_mad: { type: number, example: 550 }
                  checkout_url: { type: string, format: uri }
        "400":
          description: Invalid amount

  /analytics:
    get:
      summary: Get usage analytics
      operationId: getAnalytics
      parameters:
        - name: days
          in: query
          schema:
            type: integer
            default: 30
            maximum: 90
      responses:
        "200":
          description: Analytics data
          content:
            application/json:
              schema:
                type: object
                properties:
                  window_days: { type: integer }
                  summary:
                    type: object
                    properties:
                      total: { type: integer }
                      delivered: { type: integer }
                      failed: { type: integer }
                      delivery_rate: { type: number }
                      cost_eur: { type: number }
                      cost_mad: { type: number }
                  channels:
                    type: array
                    items: { type: object }
                  daily:
                    type: array
                    items: { type: object }

  /contacts:
    get:
      summary: List contacts
      operationId: listContacts
      parameters:
        - name: q
          in: query
          schema: { type: string }
        - name: list_id
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            maximum: 500
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Contact list
    post:
      summary: Create or update contact
      operationId: upsertContact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone]
              properties:
                phone: { type: string, example: "+212612345678" }
                name: { type: string, example: "Sara" }
                email: { type: string, format: email }
                list_id: { type: string }
                custom_fields:
                  type: object
                  properties:
                    custom1: { type: string }
                    custom2: { type: string }
                    custom3: { type: string }
      responses:
        "201":
          description: Contact created

  /contacts/import:
    post:
      summary: Import contacts
      operationId: importContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contacts]
              properties:
                list_id: { type: string }
                contacts:
                  type: array
                  maxItems: 5000
                  items:
                    type: object
                    required: [phone]
                    properties:
                      phone: { type: string, example: "+212612345678" }
                      name: { type: string }
                      email: { type: string, format: email }
      responses:
        "202":
          description: Import accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  imported: { type: integer }
                  skipped: { type: integer }

  /contacts/lists:
    get:
      summary: List contact lists
      operationId: listContactLists
      responses:
        "200":
          description: Contact lists
    post:
      summary: Create contact list
      operationId: createContactList
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: Customers }
                description: { type: string }
      responses:
        "201":
          description: Contact list created

  /contacts/{id}:
    delete:
      summary: Delete contact
      operationId: deleteContact
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Contact deleted

  /optouts:
    get:
      summary: List opt-outs
      operationId: listOptouts
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            maximum: 500
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Opt-out registry
    post:
      summary: Create opt-out
      operationId: createOptout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone]
              properties:
                phone: { type: string, example: "+212612345678" }
      responses:
        "201":
          description: Recipient opted out

  /optouts/{phone}:
    delete:
      summary: Remove opt-out
      operationId: deleteOptout
      parameters:
        - name: phone
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Recipient opted back in

  /campaigns:
    get:
      summary: List campaigns
      operationId: listCampaigns
      responses:
        "200":
          description: Campaign list
    post:
      summary: Create campaign
      operationId: createCampaign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                channel:
                  type: string
                  enum: [sms, whatsapp, telegram]
                  default: sms
                list_id: { type: string }
                template_id: { type: string }
                body: { type: string }
                sender_id: { type: string, example: SMSMAROC }
                scheduled_at: { type: string, format: date-time }
      responses:
        "201":
          description: Campaign created

  /campaigns/{id}:
    get:
      summary: Get campaign
      operationId: getCampaign
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Campaign details
        "404":
          description: Campaign not found

  /campaigns/{id}/send:
    post:
      summary: Send campaign
      operationId: sendCampaign
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "202":
          description: Campaign queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  queued: { type: integer }
                  total: { type: integer }

  /webhooks:
    get:
      summary: List webhooks
      operationId: listWebhooks
      responses:
        "200":
          description: Webhook list
    post:
      summary: Create webhook
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  example: "https://yourapp.com/smsma-webhook"
                events:
                  type: array
                  items:
                    type: string
                    enum: [message.sent, message.delivered, message.failed, message.undeliverable, message.test, message.*]
                  default: ["message.delivered", "message.failed"]
                secret:
                  type: string
                  description: Optional HMAC secret. Generated and returned when omitted.
      responses:
        "201":
          description: Webhook created

  /webhooks/{id}:
    patch:
      summary: Update webhook
      operationId: updateWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items:
                    type: string
                active: { type: boolean }
      responses:
        "200":
          description: Webhook updated
    delete:
      summary: Disable webhook
      operationId: deleteWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Webhook disabled

  /webhooks/{id}/test:
    post:
      summary: Test webhook
      operationId: testWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "202":
          description: Test webhook queued

  /api-keys:
    get:
      summary: List API keys
      operationId: listApiKeys
      responses:
        "200":
          description: API keys without full secret values
    post:
      summary: Create API key
      operationId: createApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, example: Production key }
                permissions:
                  type: array
                  items:
                    type: string
                    enum: [send, verify, status, balance, contacts, campaigns, webhooks, billing, analytics, api_keys, optouts]
                ip_whitelist:
                  type: array
                  items: { type: string }
                rate_limit:
                  type: integer
                  minimum: 10
                  maximum: 1000
      responses:
        "201":
          description: API key created. Full key is returned once.

  /api-keys/{id}:
    patch:
      summary: Update API key
      operationId: updateApiKey
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                permissions:
                  type: array
                  items: { type: string }
                ip_whitelist:
                  type: array
                  items: { type: string }
                rate_limit:
                  type: integer
                active:
                  type: boolean
      responses:
        "200":
          description: API key updated
    delete:
      summary: Revoke API key
      operationId: revokeApiKey
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: API key revoked
