openapi: 3.0.3
info:
  title: NotifyBridge API
  description: |
    Omnichannel notification API for Ghana and Africa. Send SMS, WhatsApp, email, push, and voice messages.

    **Authentication options:**
    - `X-API-Key` header (recommended for server integrations)
    - `Authorization: Bearer <JWT>` + `X-Organization-Id` header (dashboard sessions)

    **Base URL:** `http://localhost:4000` (development) or your production API host.
  version: 1.0.0
  contact:
    name: NotifyBridge Developer Support
    url: https://notifybridge.africa/developers

servers:
  - url: http://localhost:4000
    description: Local development
  - url: https://api.notify.rorodigitalsolutions.com
    description: Production

tags:
  - name: Auth
  - name: System
  - name: Messages
  - name: Contacts
  - name: Campaigns
  - name: Segments
  - name: Events
  - name: Templates
  - name: API Keys
  - name: Delivery Logs
  - name: Analytics
  - name: Webhooks
  - name: Billing
  - name: Compliance
  - name: Organizations
  - name: Admin
  - name: Providers

paths:
  /auth/register:
    post:
      tags: [Auth]
      summary: Register a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            example:
              email: developer@example.com
              password: SecurePass123
              firstName: Kwame
              lastName: Mensah
              organizationName: Demo Clinic
      responses:
        '201':
          description: User registered; returns JWT
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          description: Email already registered (EMAIL_EXISTS)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/login:
    post:
      tags: [Auth]
      summary: Login with email and password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            example:
              email: admin@notifybridge.africa
              password: Admin123!
      responses:
        '200':
          description: Login successful; returns JWT and organizations
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          description: Invalid credentials (INVALID_CREDENTIALS)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/me:
    get:
      tags: [Auth]
      summary: Get current user profile
      security:
        - BearerAuth: []
      responses:
        '200':
          description: User profile with organization memberships
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/forgot-password:
    post:
      tags: [Auth]
      summary: Request password reset email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForgotPasswordRequest'
      responses:
        '200':
          description: Always returns success message (does not reveal if email exists)
        '400':
          description: Missing email (EMAIL_REQUIRED)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/reset-password:
    post:
      tags: [Auth]
      summary: Reset password with token from email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordRequest'
      responses:
        '200':
          description: Password updated
        '400':
          description: Invalid input, weak password, or expired token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /organizations/current:
    get:
      tags: [Organizations]
      summary: Get current organization profile
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Organization with wallet and members
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags: [Organizations]
      summary: Update organization settings
      description: Used during onboarding and settings. Supports industry, phone, timezone, and compliance fields.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrganizationUpdate'
      responses:
        '200':
          description: Organization updated

  /organizations/members:
    get:
      tags: [Organizations]
      summary: List organization members
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Member list with user profiles

  /organizations/invite:
    post:
      tags: [Organizations]
      summary: Invite an existing user to the organization
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrganizationInvite'
      responses:
        '201':
          description: Member added
        '404':
          description: User must register first (USER_NOT_FOUND)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /health:
    get:
      tags: [System]
      summary: Health check
      description: Returns API status plus Redis, database, and worker queue connectivity.
      responses:
        '200':
          description: All dependencies healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthCheckResult'
              example:
                status: ok
                service: notifybridge-api
                timestamp: '2026-07-03T12:00:00.000Z'
                dependencies:
                  redis: { status: ok, latencyMs: 2 }
                  database: { status: ok }
                  workers:
                    status: ok
                    notificationSend:
                      waiting: 0
                      active: 0
                      completed: 42
                      failed: 0
                      workers: 1
        '503':
          description: One or more dependencies degraded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthCheckResult'

  /messages/bulk:
    post:
      tags: [Messages]
      summary: Send multiple messages
      description: |
        Send up to 100 messages in one request. Set `async: true` to enqueue all messages
        via BullMQ — returns **202** with each delivery log in `QUEUED` status.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkSendRequest'
            example:
              async: true
              countryCode: GH
              messages:
                - channel: SMS
                  to: "+233241234567"
                  body: "Reminder 1"
                - channel: SMS
                  to: "+233241234568"
                  body: "Reminder 2"
      responses:
        '201':
          description: Messages sent synchronously (async=false)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkSendResponse'
        '202':
          description: Messages queued (async=true)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkSendResponse'
              example:
                success: true
                data:
                  - id: clxyz123
                    internalMessageId: nb_msg_abc123
                    status: QUEUED
                    channel: SMS
                    recipient: "+233241234567"
                  - id: clxyz124
                    internalMessageId: nb_msg_abc124
                    status: QUEUED
                    channel: SMS
                    recipient: "+233241234568"
        '400':
          $ref: '#/components/responses/ValidationError'

  /messages/send:
    post:
      tags: [Messages]
      summary: Send a message
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
            example:
              channel: SMS
              to: "+233241234567"
              body: "Your OTP is 482910. Valid for 5 minutes."
              countryCode: GH
      responses:
        '201':
          description: Message sent synchronously
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessDeliveryLog'
        '202':
          description: Message queued for async delivery (async=true)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessDeliveryLog'
        '400':
          $ref: '#/components/responses/ValidationError'

  /messages/{id}:
    get:
      tags: [Messages]
      summary: Get message status by ID
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Delivery log

  /contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: search
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Contact list
    post:
      tags: [Contacts]
      summary: Create a contact
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactCreate'
      responses:
        '201':
          description: Contact created
        '400':
          $ref: '#/components/responses/ValidationError'

  /contacts/import:
    post:
      tags: [Contacts]
      summary: Import contacts from CSV
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: Import job created

  /campaigns:
    get:
      tags: [Campaigns]
      summary: List campaigns
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Campaign list
    post:
      tags: [Campaigns]
      summary: Create a campaign
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignCreate'
      responses:
        '201':
          description: Campaign created
        '400':
          $ref: '#/components/responses/ValidationError'

  /campaigns/{id}/send:
    post:
      tags: [Campaigns]
      summary: Send a campaign
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Campaign sending started

  /events:
    get:
      tags: [Events]
      summary: List notification events
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Event list with linked templates
    post:
      tags: [Events]
      summary: Create a notification event
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EventCreate'
            example:
              name: Appointment Reminder
              slug: appointment.reminder
              channelPriority: [SMS, WHATSAPP]
              description: Remind patients before clinic visits
      responses:
        '201':
          description: Event created
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          description: Slug already exists (SLUG_EXISTS)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /templates:
    get:
      tags: [Templates]
      summary: List message templates
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Template list
    post:
      tags: [Templates]
      summary: Create a message template
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateCreate'
            example:
              name: OTP Verification
              channel: SMS
              category: OTP
              body: "Your {{brandName}} code is {{code}}. Valid for 5 minutes."
              variables: [brandName, code]
      responses:
        '201':
          description: Template created (may require approval for high-risk categories)
        '400':
          $ref: '#/components/responses/ValidationError'

  /api-keys:
    get:
      tags: [API Keys]
      summary: List API keys (prefixes only; full keys are never returned)
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: API key list
    post:
      tags: [API Keys]
      summary: Create an API key
      description: Returns the full key once in the response — store it securely.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreate'
            example:
              name: Production integration
              isSandbox: false
              scopes: [messages:send, events:trigger]
      responses:
        '201':
          description: API key created with one-time secret
        '400':
          description: Missing key name (NAME_REQUIRED)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /delivery-logs:
    get:
      tags: [Delivery Logs]
      summary: List delivery logs
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [QUEUED, SENT, DELIVERED, FAILED, PENDING]
        - name: channel
          in: query
          schema:
            type: string
            enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
      responses:
        '200':
          description: Paginated delivery logs

  /delivery-logs/export:
    get:
      tags: [Delivery Logs]
      summary: Export delivery logs as CSV
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: status
          in: query
          schema:
            type: string
        - name: channel
          in: query
          schema:
            type: string
      responses:
        '200':
          description: CSV export (up to 10,000 rows)
          content:
            text/csv:
              schema:
                type: string

  /events/trigger:
    post:
      tags: [Events]
      summary: Trigger a notification event
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EventTrigger'
            example:
              event: appointment.reminder
              to: "+233241234567"
              variables:
                firstName: Kwame
                clinicName: Demo Clinic
                date: "2026-07-04"
                time: "10:00"
      responses:
        '201':
          description: Event triggered
        '400':
          $ref: '#/components/responses/ValidationError'

  /compliance/suppression:
    get:
      tags: [Compliance]
      summary: List suppression entries
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Paginated suppression list
    post:
      tags: [Compliance]
      summary: Add phone or email to suppression list
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuppressionCreate'
            example:
              phone: "+233241234567"
              channel: SMS
              reason: Customer opt-out
      responses:
        '201':
          description: Suppression entry created
        '400':
          $ref: '#/components/responses/ValidationError'

  /compliance/summary:
    get:
      tags: [Compliance]
      summary: Compliance dashboard summary
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Suppression count and pending approvals

  /webhook-endpoints:
    get:
      tags: [Webhooks]
      summary: List tenant webhook endpoints
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Webhook endpoints
    post:
      tags: [Webhooks]
      summary: Create webhook endpoint
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                  example: [message.status.updated, contact.opted_out]
      responses:
        '201':
          description: Webhook created (signing secret returned once)
        '400':
          description: Missing webhook URL (URL_REQUIRED)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /webhook-endpoints/{id}:
    delete:
      tags: [Webhooks]
      summary: Deactivate a webhook endpoint
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Webhook deactivated
        '404':
          description: Webhook not found (NOT_FOUND)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags: [Webhooks]
      summary: Update webhook URL, events, or active status
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                isActive:
                  type: boolean
      responses:
        '200':
          description: Webhook updated
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /webhook-endpoints/{id}/test:
    post:
      tags: [Webhooks]
      summary: Send a test payload to a webhook endpoint
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  example: message.status.updated
      responses:
        '200':
          description: Test delivery attempted
        '404':
          description: Active webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

                $ref: '#/components/schemas/ErrorResponse'

  /segments:
    get:
      tags: [Segments]
      summary: List audience segments
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Segment list with contact counts
    post:
      tags: [Segments]
      summary: Create a segment
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SegmentCreate'
            example:
              name: Accra SMS subscribers
              description: Contacts in Greater Accra with SMS preference
              rules:
                region: Greater Accra
                preferredChannel: SMS
              isDynamic: true
      responses:
        '201':
          description: Segment created
        '400':
          $ref: '#/components/responses/ValidationError'

  /segments/preview:
    post:
      tags: [Segments]
      summary: Preview segment rules against contacts
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                rules:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Matching contact count and sample rows

  /analytics/overview:
    get:
      tags: [Analytics]
      summary: Dashboard analytics overview
      description: 30-day send volume, delivery rate, spend, and month-over-month trends.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Overview metrics

  /analytics/recommendations:
    get:
      tags: [Analytics]
      summary: AI-style messaging recommendations
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Up to four actionable recommendations

  /analytics/costs:
    get:
      tags: [Analytics]
      summary: Channel cost breakdown
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: months
          in: query
          schema:
            type: integer
            default: 3
      responses:
        '200':
          description: Usage and cost summary

  /analytics/failures:
    get:
      tags: [Analytics]
      summary: Recent failed deliveries
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Last 50 failed delivery logs

  /billing/wallet:
    get:
      tags: [Billing]
      summary: Get wallet balance
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Wallet balance

  /billing/top-up:
    post:
      tags: [Billing]
      summary: Top up wallet balance
      description: |
        For `mobile_money` or `card`, returns a Paystack checkout URL. Instant credit (no payment) is limited to platform admins.
        For `invoice` or manual flows, credits the wallet immediately when configured.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TopUpRequest'
            example:
              amount: 100
              paymentMethod: mobile_money
              gateway: paystack
      responses:
        '200':
          description: Redirect URL or instant top-up result

  /billing/verify-payment:
    post:
      tags: [Billing]
      summary: Verify payment after gateway redirect
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyPaymentRequest'
      responses:
        '200':
          description: Wallet credited or already processed
        '400':
          description: Missing reference or payment failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /billing/usage:
    get:
      tags: [Billing]
      summary: Usage and spend summary
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
        - name: months
          in: query
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: Channel usage breakdown

  /billing/invoices:
    get:
      tags: [Billing]
      summary: List invoices
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Invoice list

  /billing/pricing:
    get:
      tags: [Billing]
      summary: Public pricing rules and subscription plans
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      responses:
        '200':
          description: Active pricing rules and plans

  /billing/webhooks/paystack:
    post:
      tags: [Billing]
      summary: Paystack payment webhook
      description: |
        Paystack calls this endpoint on successful charges. NotifyBridge verifies the
        `x-paystack-signature` HMAC and credits the wallet when `charge.success` is received.
      parameters:
        - name: x-paystack-signature
          in: header
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Paystack event payload
      responses:
        '200':
          description: Webhook acknowledged
        '401':
          description: Invalid signature (INVALID_SIGNATURE)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /providers:
    get:
      tags: [Providers]
      summary: List messaging providers and routes
      description: Returns configured SMS, WhatsApp, email, push, and voice providers with country routing rules.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Provider list with latest health status

  /providers/{id}:
    get:
      tags: [Providers]
      summary: Get provider details
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Provider with routes and health history

  /providers/{id}/health-check:
    post:
      tags: [Providers]
      summary: Run a live provider health check
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Health check result recorded
        '404':
          description: Provider not found

  /compliance/check-message:
    post:
      tags: [Compliance]
      summary: Check a message for compliance risk
      description: Rule-based checks for spam phrases, phishing patterns, sensitive health data and opt-out wording. Risk score is 0-100; 30 or more needs approval.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrganizationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AiComplianceReviewRequest'
      responses:
        '200':
          description: Risk score, issues, and approval requirement

  /admin/revenue:
    get:
      tags: [Admin]
      summary: Platform revenue summary
      description: Requires `PLATFORM_SUPER_ADMIN` role.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Total revenue, message count, and wallet balances
        '403':
          description: Not a platform admin

  /admin/tenants:
    get:
      tags: [Admin]
      summary: List all tenant organizations
      description: Requires `PLATFORM_SUPER_ADMIN` role.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Tenant list with wallet and member counts

  /admin/tenants/{id}/impersonate:
    post:
      tags: [Admin]
      summary: Issue impersonation token for a tenant
      description: Returns a short-lived JWT scoped to the target organization.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Impersonation token and organization
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /admin/exit-impersonation:
    post:
      tags: [Admin]
      summary: Exit tenant impersonation session
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Restored platform admin token
        '400':
          description: Not impersonating (NOT_IMPERSONATING)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /admin/approvals:
    get:
      tags: [Admin]
      summary: List pending campaign and template approvals
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Pending approval requests

  /admin/approvals/{id}/approve:
    post:
      tags: [Admin]
      summary: Approve a campaign or template
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Approval granted
        '404':
          description: Approval not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /admin/approvals/{id}/reject:
    post:
      tags: [Admin]
      summary: Reject a campaign or template approval
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                comment:
                  type: string
      responses:
        '200':
          description: Approval rejected

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    OrganizationId:
      name: X-Organization-Id
      in: header
      required: true
      schema:
        type: string
      description: Required when using JWT authentication

  responses:
    ValidationError:
      description: Invalid request body (Zod validation)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
    ErrorResponse:
      description: Application error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string

    RegisterRequest:
      type: object
      required: [email, password, firstName, lastName]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        firstName:
          type: string
        lastName:
          type: string
        organizationName:
          type: string
          description: When provided, creates an organization and wallet

    ForgotPasswordRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email

    ResetPasswordRequest:
      type: object
      required: [token, password]
      properties:
        token:
          type: string
        password:
          type: string
          minLength: 8

    SuppressionCreate:
      type: object
      description: At least one of phone or email is required
      properties:
        phone:
          type: string
        email:
          type: string
          format: email
        channel:
          type: string
          enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
        reason:
          type: string

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: INVALID_CREDENTIALS
            message:
              type: string
            details:
              type: object

    SendMessageRequest:
      type: object
      required: [channel, to]
      properties:
        channel:
          type: string
          enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
        to:
          type: string
          description: Phone, email, or FCM device token
        body:
          type: string
        subject:
          type: string
        countryCode:
          type: string
          default: GH
        templateId:
          type: string
        variables:
          type: object
          additionalProperties:
            type: string
        idempotencyKey:
          type: string
        async:
          type: boolean
          default: false
          description: When true, enqueue delivery via BullMQ and return 202 with QUEUED status

    BulkSendRequest:
      type: object
      required: [messages]
      properties:
        async:
          type: boolean
          default: false
          description: When true, enqueue all messages and return 202
        countryCode:
          type: string
          default: GH
        messages:
          type: array
          maxItems: 100
          items:
            type: object
            required: [channel, to, body]
            properties:
              channel:
                type: string
                enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
              to:
                type: string
              body:
                type: string
              variables:
                type: object
                additionalProperties:
                  type: string
              senderId:
                type: string
              templateId:
                type: string

    ContactCreate:
      type: object
      properties:
        firstName:
          type: string
        lastName:
          type: string
        phone:
          type: string
        email:
          type: string
        countryCode:
          type: string
          default: GH
        tags:
          type: array
          items:
            type: string
        fcmToken:
          type: string

    CampaignCreate:
      type: object
      required: [name, channel]
      properties:
        name:
          type: string
        channel:
          type: string
          enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
        templateId:
          type: string
        segmentId:
          type: string
        scheduledAt:
          type: string
          format: date-time

    EventTrigger:
      type: object
      required: [event]
      description: Trigger a dashboard-configured event. Provide `to` or `contactId` (at least one required).
      properties:
        event:
          type: string
          pattern: '^[a-z0-9._-]+$'
          description: Event slug e.g. appointment.reminder
        to:
          type: string
          description: Phone, email, or device token (required if contactId omitted)
        contactId:
          type: string
          description: Contact ID to resolve recipient (required if to omitted)
        variables:
          type: object
          additionalProperties:
            type: string
        idempotencyKey:
          type: string

    EventCreate:
      type: object
      required: [name, slug]
      properties:
        name:
          type: string
        slug:
          type: string
          pattern: '^[a-z0-9._-]+$'
        description:
          type: string
        channelPriority:
          type: array
          items:
            type: string
            enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
          default: [SMS]
        templateId:
          type: string
        retryRules:
          type: object
        fallbackRules:
          type: object
          properties:
            channels:
              type: array
              items:
                type: string
                enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
        isActive:
          type: boolean
          default: true

    TemplateCreate:
      type: object
      required: [name, channel, body]
      properties:
        name:
          type: string
        channel:
          type: string
          enum: [SMS, WHATSAPP, EMAIL, PUSH, IN_APP, VOICE]
        countryCode:
          type: string
          default: GH
        language:
          type: string
          default: en
        category:
          type: string
          enum: [TRANSACTIONAL, MARKETING, OTP, REMINDER, ALERT, HEALTHCARE, INSURANCE, LOGISTICS, FINTECH, ECOMMERCE]
          default: TRANSACTIONAL
        body:
          type: string
        subject:
          type: string
        variables:
          type: array
          items:
            type: string

    ApiKeyCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
        isSandbox:
          type: boolean
          default: true
        scopes:
          type: array
          items:
            type: string
          example: [messages:send, contacts:read]
        expiresAt:
          type: string
          format: date-time

    SegmentCreate:
      type: object
      required: [name, rules]
      properties:
        name:
          type: string
        description:
          type: string
        rules:
          type: object
          additionalProperties: true
          description: Filter rules evaluated against contacts
        isDynamic:
          type: boolean
          default: true

    TopUpRequest:
      type: object
      required: [amount]
      properties:
        amount:
          type: number
          minimum: 1
          description: Amount in GHS
        paymentMethod:
          type: string
          enum: [mobile_money, card, invoice]
          default: mobile_money
        reference:
          type: string
          description: Reference for manual invoice credits (platform admins only)

    VerifyPaymentRequest:
      type: object
      required: [reference]
      properties:
        reference:
          type: string

    OrganizationUpdate:
      type: object
      properties:
        name:
          type: string
        industry:
          type: string
        businessPhone:
          type: string
        businessEmail:
          type: string
          format: email
        timezone:
          type: string
        defaultLanguage:
          type: string
        website:
          type: string
        onboardingComplete:
          type: boolean
        complianceAcknowledged:
          type: boolean
        monthlyVolume:
          type: integer
        preferredChannels:
          type: array
          items:
            type: string

    OrganizationInvite:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        role:
          type: string
          enum: [TENANT_OWNER, TENANT_ADMIN, CAMPAIGN_MANAGER, COMPLIANCE_REVIEWER, DEVELOPER, VIEWER]
          default: VIEWER

    AiGenerateRequest:
      type: object
      required: [goal, channel]
      properties:
        goal:
          type: string
        channel:
          type: string
          enum: [SMS, WHATSAPP, EMAIL, PUSH, VOICE]
        category:
          type: string
        language:
          type: string
        tone:
          type: string
        variables:
          type: array
          items:
            type: string

    AiRewriteRequest:
      type: object
      required: [message, style]
      properties:
        message:
          type: string
        style:
          type: string
          enum: [professional, friendly, urgent, formal]

    AiTranslateRequest:
      type: object
      required: [message, targetLanguage]
      properties:
        message:
          type: string
        targetLanguage:
          type: string
        sourceLanguage:
          type: string

    AiComplianceReviewRequest:
      type: object
      required: [message]
      properties:
        message:
          type: string
        category:
          type: string
          enum: [TRANSACTIONAL, MARKETING, OTP, REMINDER, ALERT, HEALTHCARE, INSURANCE, LOGISTICS, FINTECH, ECOMMERCE]
          default: MARKETING

    SuccessDeliveryLog:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            id:
              type: string
            internalMessageId:
              type: string
            status:
              type: string
              enum: [QUEUED, SENT, DELIVERED, FAILED, PENDING]
            channel:
              type: string
            recipient:
              type: string
            cost:
              type: number

    BulkSendResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              internalMessageId:
                type: string
              status:
                type: string
              channel:
                type: string
              recipient:
                type: string
              cost:
                type: number

    HealthCheckResult:
      type: object
      properties:
        status:
          type: string
          enum: [ok, degraded]
        service:
          type: string
        timestamp:
          type: string
          format: date-time
        dependencies:
          type: object
          properties:
            redis:
              type: object
              properties:
                status:
                  type: string
                latencyMs:
                  type: integer
                message:
                  type: string
            database:
              type: object
              properties:
                status:
                  type: string
                message:
                  type: string
            workers:
              type: object
              properties:
                status:
                  type: string
                message:
                  type: string
                notificationSend:
                  type: object
                  properties:
                    waiting:
                      type: integer
                    active:
                      type: integer
                    completed:
                      type: integer
                    failed:
                      type: integer
                    workers:
                      type: integer

    ValidationErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
              example: VALIDATION_ERROR
            message:
              type: string
              example: Either to or contactId is required
            details:
              type: object
              description: Zod flatten() output with fieldErrors and formErrors
