openapi: 3.1.0
info:
  title: SMSBOX API
  description: >
    SMS gateway API van JAAN BV (smsbox.be).
    Ondersteunt het versturen en ontvangen van SMS berichten,
    OTP verificatie, HLR lookup, voice berichten, telefoonboek beheer,
    URL tracking en authenticatie.


    **WhatsApp Business API** is beschikbaar op aanvraag.
    Neem contact op via [jaan.be/contact](https://www.jaan.be/contact) om
    WhatsApp te activeren op uw account.


    ## Response formaat

    Alle endpoints geven HTTP `200` terug. Succes of fout wordt aangegeven
    via het `code`-veld in de JSON body. Zie per endpoint welke codes
    mogelijk zijn (zie de Responses-sectie).
  version: 2.0.0
  contact:
    name: JAAN BV
    url: https://www.jaan.be/contact
  termsOfService: https://www.jaan.be/algemene-voorwaarden

servers:
  - url: https://core.smsbox.be/api
    description: Productie server

security:
  - ApiKeyHeader: []

tags:
  - name: Berichten
    description: SMS berichten versturen en ophalen
  - name: WhatsApp
    description: >
      > **📩 WhatsApp Business — op aanvraag**

      >

      > WhatsApp Business berichten versturen en ontvangen via de SMSBOX API.

      > Deze functie is beschikbaar als aanvullende dienst op uw account.

      >

      > Na activatie krijgt u toegang tot endpoints voor het versturen van

      > template berichten, vrije tekst, en media via WhatsApp Business.

      >

      > **Neem contact op** via [jaan.be/contact](https://www.jaan.be/contact) om WhatsApp te activeren.
  - name: Teams
    description: >
      > **💬 Microsoft Teams — op aanvraag**

      >

      > Stuur 1-op-1 Teams-berichten naar collega's in uw eigen Microsoft-tenant.

      > Deze functie is beschikbaar als aanvullende dienst op uw account.

      >

      > U koppelt eerst een Microsoft-serviceaccount via

      > [Integraties](/cp/smsbox/integrations). Berichten worden vanaf dat account

      > verstuurd.

      >

      > **Neem contact op** via [jaan.be/contact](https://www.jaan.be/contact) om Teams te activeren.
  - name: Saldo
    description: Credit saldo opvragen
  - name: OTP
    description: One Time Password (Twee-Factor Authenticatie)
  - name: HLR
    description: Mobiele nummer validatie (Home Location Register)
  - name: Nummer
    description: Nummer format conversie
  - name: Voice
    description: Voice berichten versturen (Text-to-Speech)
  - name: Telefoonboek
    description: Contacten en groepen beheren
  - name: Authenticatie
    description: API key validatie
  - name: Tracking
    description: Short URL aanmaken met tracking

paths:
  # ─────────────────────────────────────────────
  # Berichten
  # ─────────────────────────────────────────────
  /v1/sendsms:
    post:
      operationId: sendSmsV1
      x-internal: true   # verborgen in docs — oude V1 API, voorlopig niet relevant
      tags: [Berichten]
      summary: SMS bericht versturen (V1)
      description: Verstuur een SMS bericht naar één of meerdere ontvangers via de V1 endpoint.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SendSmsV1Request'
          application/json:
            schema:
              $ref: '#/components/schemas/SendSmsV1Request'
      responses:
        '200':
          description: |
            Resultaat van de verzending. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `100` | Succesvol verzonden — `message` bevat array met `number` en `msgid` per ontvanger |
            | `01`  | Sender not ok — afzender NOK |
            | `04`  | Apikey not allowed |
            | `05`  | No prefix — geen prefix opgegeven |
            | `06`  | No message — geen bericht opgegeven |
            | `07`  | Max char reached — longsms=0 max 160 tekens, longsms=1 max 918 tekens |
            | `08`  | No number — geen GSM-nummer opgegeven |
            | `09`  | No valid number |
            | `10`  | Not enough credits |
            | `11`  | Max 500 numbers allowed |
            | `13`  | Sender too long — max 16 numeriek / 11 alphanumeriek |
            | `14`  | Invalid channel |
            | `15`  | Invalid template |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendSmsV1Response'

  /v2/message/send:
    post:
      operationId: sendMessageV2
      tags: [Berichten]
      summary: SMS bericht versturen (V2)
      description: >
        Verstuur een SMS bericht naar één of meerdere ontvangers.
        Ondersteunt een test-modus via de `test` parameter.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/SendMessageV2Request'
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageV2Request'
      responses:
        '200':
          description: |
            Resultaat van de verzending. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `100` | Succesvol verzonden — `message` bevat array met `mobile`, `msgid` en `type` per ontvanger |
            | `01`  | Sender not ok — afzender NOK |
            | `04`  | Apikey not allowed |
            | `05`  | No prefix |
            | `06`  | No message |
            | `07`  | Max char reached |
            | `08`  | No number |
            | `09`  | No valid number |
            | `10`  | Not enough credits |
            | `11`  | Max 500 numbers allowed |
            | `13`  | Sender too long |
            | `14`  | Invalid channel |
            | `15`  | Invalid template |
            | `16`  | Invalid media type |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageV2Response'

  /v2/message/inbox:
    get:
      operationId: getInbox
      tags: [Berichten]
      summary: Inkomende berichten ophalen
      description: Haal inkomende SMS berichten op. Optioneel filterbaar op channel, mobiel nummer of datum.
      parameters:
        - name: channel
          in: query
          description: Filter op channel/keyword
          schema:
            type: string
        - name: mobile
          in: query
          description: Filter op mobiel nummer
          schema:
            type: string
        - name: daterange
          in: query
          description: Filter op datum (formaat Ymd-Ymd)
          schema:
            type: string
            example: '20220801-20220831'
      responses:
        '200':
          description: |
            Lijst van inkomende berichten. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Messages found |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxResponse'

  /v2/message/outbox:
    get:
      operationId: getOutbox
      tags: [Berichten]
      summary: Uitgaande berichten ophalen
      description: Haal uitgaande SMS berichten op. Optioneel filterbaar op channel, mobiel nummer of datum.
      parameters:
        - name: channel
          in: query
          description: Filter op channel/keyword
          schema:
            type: string
        - name: mobile
          in: query
          description: Filter op mobiel nummer
          schema:
            type: string
        - name: daterange
          in: query
          description: Filter op datum (formaat Ymd-Ymd)
          schema:
            type: string
            example: '20220801-20220831'
      responses:
        '200':
          description: |
            Lijst van uitgaande berichten. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Messages found |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutboxResponse'

  # ─────────────────────────────────────────────
  # WhatsApp (op aanvraag)
  # ─────────────────────────────────────────────
  /whatsapp/send:
    servers:
      - url: https://core.smsbox.be/api
        description: WhatsApp is beschikbaar op aanvraag
    post:
      operationId: sendWhatsApp
      tags: [WhatsApp]
      summary: WhatsApp Business bericht versturen (op aanvraag)
      description: >
        > **📩 WhatsApp Business — op aanvraag**

        >

        > Deze functie is beschikbaar als aanvullende dienst.

        > Neem contact op via [jaan.be/contact](https://www.jaan.be/contact)

        > om WhatsApp te activeren op uw account.


        Verstuur een WhatsApp Business bericht. Geef exact **één** van de
        volgende velden mee: `message` (vrije tekst), `media_url` (media) of
        `template` (template bericht met `parameters`).


        > 💡 Een overzicht van de beschikbare templates, hun variabelen en
        > voorbeeldtekst vind je op de
        > [WhatsApp templates-pagina](/cp/smsbox/whatsapp/templates).


        > **Let op:** dit endpoint geeft een echte HTTP-status terug (anders dan
        > de SMS-endpoints). Bij een fout zit een machine-leesbare code in het
        > `code`-veld.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WhatsAppSendRequest'
            examples:
              text:
                summary: Vrije tekst
                value:
                  to: '32496123456'
                  message: 'Hallo, uw bestelling is onderweg!'
              media:
                summary: Media (met optioneel bijschrift)
                value:
                  to: '32496123456'
                  media_url: 'https://example.com/factuur.pdf'
                  message: 'Uw factuur'
              template:
                summary: Template bericht
                value:
                  to: '32496123456'
                  template: 'sample_issue_resolution'
                  parameters: ['jan']
                  language: 'en_US'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/WhatsAppSendRequest'
      responses:
        '200':
          description: Bericht aanvaard en in de wachtrij gezet voor verzending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhatsAppSendResponse'
        '403':
          description: |
            Geen WhatsApp-rechten op dit account.

            | Code | Betekenis |
            |------|-----------|
            | `ACCESS_DENIED` | Geen WhatsApp-permissie voor dit account |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhatsAppErrorResponse'
        '422':
          description: |
            Validatiefout. Het `code`-veld geeft de exacte oorzaak:

            | Code | Betekenis |
            |------|-----------|
            | `TO_REQUIRED` | `to` ontbreekt |
            | `TO_INVALID` | `to` is geen geldig mobiel nummer |
            | `PARAMETERS_INVALID` | `parameters` is geen array |
            | `NO_CONTENT` | Geen `message`, `media_url` of `template` opgegeven |
            | `TEMPLATE_CONFLICT` | `template` gecombineerd met `message` of `media_url` |
            | `MEDIA_URL_INVALID` | `media_url` is geen geldige URL |
            | `TEMPLATE_INVALID` | Template onbekend, of verkeerd aantal parameters |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhatsAppErrorResponse'
        '502':
          description: |
            WhatsApp (Meta) heeft het bericht geweigerd.

            | Code | Betekenis |
            |------|-----------|
            | `WHATSAPP_SEND_FAILED` | Meta wees de aanvraag af (zie `message` voor de reden) |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhatsAppErrorResponse'
        '500':
          description: |
            Onverwachte serverfout.

            | Code | Betekenis |
            |------|-----------|
            | `INTERNAL_ERROR` | Het bericht kon niet verstuurd worden |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhatsAppErrorResponse'

  # ─────────────────────────────────────────────
  # Microsoft Teams (op aanvraag)
  # ─────────────────────────────────────────────
  /teams/send:
    servers:
      - url: https://core.smsbox.be/api
        description: Teams is beschikbaar op aanvraag
    post:
      operationId: sendTeams
      tags: [Teams]
      summary: Microsoft Teams bericht versturen (op aanvraag)
      description: >
        > **💬 Microsoft Teams — op aanvraag**

        >

        > Deze functie is beschikbaar als aanvullende dienst.

        > Neem contact op via [jaan.be/contact](https://www.jaan.be/contact)

        > om Teams te activeren op uw account.


        Verstuur een 1-op-1 Teams-bericht naar een collega in uw Microsoft-tenant.
        Geef het e-mailadres (UPN) van de ontvanger mee in `to`, en de tekst in
        `message`. Het bericht wordt verstuurd vanaf het serviceaccount dat u bij
        de [integratie](/cp/smsbox/integrations) gekoppeld heeft.


        > 💡 De ontvanger moet in dezelfde Microsoft-tenant zitten en een
        > Teams-licentie hebben. Bestaat er al een 1-op-1 chat tussen het
        > serviceaccount en de ontvanger, dan wordt die hergebruikt.


        > **Kost:** 0,5 credits per bericht. Het saldo wordt vóór verzenden
        > gecontroleerd — is het ontoereikend, dan krijgt u `402` en wordt er
        > niets verstuurd. Een mislukt bericht kost nooit credits.


        > **Let op:** dit endpoint verstuurt synchroon en geeft een echte
        > HTTP-status terug. Bij een fout zit een machine-leesbare code in het
        > `code`-veld.


        > ⚠️ Microsoft staat niet toe dat Teams als logbestand gebruikt wordt:
        > stuur enkel berichten die door mensen gelezen worden.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TeamsSendRequest'
            examples:
              text:
                summary: Tekstbericht
                value:
                  to: 'jan@uwbedrijf.be'
                  message: 'Er staat een nieuwe bestelling klaar.'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TeamsSendRequest'
      responses:
        '200':
          description: Bericht verstuurd naar Microsoft Teams.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsSendResponse'
        '403':
          description: |
            Geen Teams-rechten op dit account.

            | Code | Betekenis |
            |------|-----------|
            | `ACCESS_DENIED` | Geen Teams-permissie voor dit account |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'
        '404':
          description: |
            Ontvanger niet gevonden.

            | Code | Betekenis |
            |------|-----------|
            | `RECIPIENT_NOT_FOUND` | `to` bestaat niet in de gekoppelde Microsoft-tenant |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'
        '409':
          description: |
            De Teams-connectie is niet (meer) bruikbaar. Los dit op via
            [Integraties](/cp/smsbox/integrations).

            | Code | Betekenis |
            |------|-----------|
            | `TEAMS_NOT_CONFIGURED` | Er is nog geen actieve Teams-connectie |
            | `TEAMS_NEEDS_AUTH` | De koppeling moet opnieuw geautoriseerd worden (toestemming ingetrokken of verlopen) |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'
        '402':
          description: |
            Onvoldoende saldo. Er is niets verstuurd.

            | Code | Betekenis |
            |------|-----------|
            | `INSUFFICIENT_CREDITS` | Te weinig credits om het bericht te versturen (0,5 per bericht) |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'
        '422':
          description: |
            Validatiefout. Het `code`-veld geeft de exacte oorzaak:

            | Code | Betekenis |
            |------|-----------|
            | `TO_REQUIRED` | `to` ontbreekt |
            | `TO_INVALID` | `to` is geen geldig e-mailadres (UPN) |
            | `RECIPIENT_IS_SENDER` | `to` is het gekoppelde serviceaccount zelf; Teams kent geen 1-op-1 chat met jezelf |
            | `NO_CONTENT` | `message` ontbreekt of is leeg |
            | `MESSAGE_TOO_LONG` | `message` is te lang |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'
        '429':
          description: |
            Microsoft beperkt tijdelijk het aantal aanvragen. Probeer later opnieuw.

            | Code | Betekenis |
            |------|-----------|
            | `RATE_LIMITED` | Microsoft Teams throttlet de aanvragen |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'
        '502':
          description: |
            Microsoft Teams heeft het bericht geweigerd.

            | Code | Betekenis |
            |------|-----------|
            | `TEAMS_SEND_FAILED` | Microsoft wees de aanvraag af (zie `message` voor de reden) |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'
        '500':
          description: |
            Onverwachte serverfout.

            | Code | Betekenis |
            |------|-----------|
            | `INTERNAL_ERROR` | Het bericht kon niet verstuurd worden |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeamsErrorResponse'

  # ─────────────────────────────────────────────
  # Saldo
  # ─────────────────────────────────────────────
  /v1/balance:
    get:
      operationId: getBalance
      tags: [Saldo]
      summary: Saldo opvragen
      description: Vraag het resterend aantal SMS credits op.
      responses:
        '200':
          description: |
            Huidig credit saldo. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `100` | Success — credits worden in het `message` veld getoond |
            | `04`  | Apikey not allowed |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceResponse'

  # ─────────────────────────────────────────────
  # OTP
  # ─────────────────────────────────────────────
  /v1/otp/send:
    post:
      operationId: sendOtpV1
      x-internal: true   # verborgen in docs — oude V1 API, voorlopig niet relevant
      tags: [OTP]
      summary: OTP versturen (V1)
      description: Verstuur een One Time Password via SMS of e-mail.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OtpSendV1Request'
          application/json:
            schema:
              $ref: '#/components/schemas/OtpSendV1Request'
      responses:
        '200':
          description: |
            OTP verzendresultaat. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Code has been sent — `number` bevat het ontvangernummer |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `40` | Number verification failed |
            | `41` | Number not provided |
            | `42` | Only one number allowed |
            | `60` | Credits too low |
            | `90` | Custom error |
            | `98` | Try again later |
            | `99` | Unknown status, contact support |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OtpSendResponse'

  /v1/otp/verify:
    get:
      operationId: verifyOtpV1
      x-internal: true   # verborgen in docs — oude V1 API, voorlopig niet relevant
      tags: [OTP]
      summary: OTP verifiëren (V1)
      description: Verifieer een eerder verzonden OTP code. Deze call is **gratis**.
      parameters:
        - name: otp
          in: query
          required: true
          description: De OTP code om te verifiëren
          schema:
            type: string
            example: '123456'
      responses:
        '200':
          description: |
            OTP verificatieresultaat. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `11` | Number is verified — `number` bevat het ontvangernummer |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `50` | OTP not provided |
            | `51` | OTP not correct |
            | `90` | Custom error |
            | `98` | Try again later |
            | `99` | Unknown status, contact support |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OtpVerifyResponse'

  /v2/otp/send:
    post:
      operationId: sendOtpV2
      tags: [OTP]
      summary: OTP versturen (V2)
      description: |
        Verstuur een One Time Password via SMS, e-mail of WhatsAppBeta.

        **Kostprijs:**
        - **E-mail:** vaste credit-prijs
        - **SMS:** volgens netwerk
        - **WhatsAppBeta:** 1.4 credits (valt terug op SMS bij onbereikbaarheid)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OtpSendV2Request'
          application/json:
            schema:
              $ref: '#/components/schemas/OtpSendV2Request'
      responses:
        '200':
          description: |
            OTP verzendresultaat. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Code has been sent — `number` bevat het ontvangernummer |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `40` | Number verification failed |
            | `41` | Number not provided |
            | `42` | Only one number allowed |
            | `60` | Credits too low |
            | `90` | Custom error |
            | `98` | Try again later |
            | `99` | Unknown status, contact support |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OtpSendResponse'

  /v2/otp/verify:
    get:
      operationId: verifyOtpV2
      tags: [OTP]
      summary: OTP verifiëren (V2)
      description: Verifieer een eerder verzonden OTP code (V2). Deze call is **gratis**.
      parameters:
        - name: otp
          in: query
          required: true
          description: De OTP code om te verifiëren
          schema:
            type: string
            example: '123456'
      responses:
        '200':
          description: |
            OTP verificatieresultaat. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `11` | Number is verified — `number` bevat het ontvangernummer |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `50` | OTP not provided |
            | `51` | OTP not correct |
            | `90` | Custom error |
            | `98` | Try again later |
            | `99` | Unknown status, contact support |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OtpVerifyResponse'

  # ─────────────────────────────────────────────
  # HLR
  # ─────────────────────────────────────────────
  /v1/hlr:
    get:
      operationId: hlrLookup
      tags: [HLR]
      summary: Mobiele nummer validatie (HLR lookup)
      description: |
        Vraag het Home Location Register (HLR) op van een mobiel nummer.

        **Kostprijs:** 0.25 credits per aanvraag.
      parameters:
        - name: number
          in: query
          required: true
          description: Mobiel nummer in internationaal formaat (countrycode+nummer)
          schema:
            type: string
            example: '32498765432'
      responses:
        '200':
          description: |
            HLR lookup resultaat. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — lookup array in `message` |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Number not provided |
            | `40` | Lookup failed |
            | `60` | Credits low |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HlrResponse'

  # ─────────────────────────────────────────────
  # Nummer format
  # ─────────────────────────────────────────────
  /v1/number/format:
    get:
      operationId: formatNumber
      tags: [Nummer]
      summary: Nummer omzetten naar internationaal formaat
      description: Zet een lokaal mobiel nummer om naar internationaal formaat.
      parameters:
        - name: number
          in: query
          required: true
          description: Het ongeformateerde nummer
          schema:
            type: string
            example: '0498765432'
        - name: countryCode
          in: query
          required: true
          description: ISO landcode (bv. BE, NL, FR)
          schema:
            type: string
            example: 'BE'
      responses:
        '200':
          description: |
            Geformateerd nummer. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Number not provided |
            | `31` | Country code not provided |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NumberFormatResponse'

  # ─────────────────────────────────────────────
  # Voice
  # ─────────────────────────────────────────────
  /v1/voice/send:
    post:
      operationId: sendVoice
      tags: [Voice]
      summary: Voice bericht versturen
      description: Verstuur een text-to-speech voice bericht naar een mobiel nummer.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/VoiceSendRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/VoiceSendRequest'
      responses:
        '200':
          description: |
            Voice bericht resultaat. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — voicecall is sent |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Number not provided |
            | `31` | Number invalid |
            | `40` | Message not provided |
            | `50` | Send voice failed |
            | `60` | Credits low |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VoiceSendResponse'

  # ─────────────────────────────────────────────
  # Telefoonboek - Contacten
  # ─────────────────────────────────────────────
  /v2/phonebook/contact:
    get:
      operationId: getAllContacts
      tags: [Telefoonboek]
      summary: Alle contacten ophalen
      description: Haal alle contacten op uit je telefoonboek.
      responses:
        '200':
          description: |
            Lijst van contacten. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'

  /v1/phonebook/contact:
    post:
      operationId: addContact
      tags: [Telefoonboek]
      summary: Contact toevoegen
      description: Voeg een nieuw contact toe aan het telefoonboek.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ContactRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/ContactRequest'
      responses:
        '200':
          description: |
            Contact aangemaakt. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — contact aangemaakt |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Mobile not provided |
            | `40` | Name not provided |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'

  /v2/phonebook/contact/{contact_id}:
    get:
      operationId: getContact
      tags: [Telefoonboek]
      summary: Contact ophalen op ID
      description: Haal de gegevens van één contact op via zijn ID.
      parameters:
        - name: contact_id
          in: path
          required: true
          schema:
            type: integer
            example: 12345
      responses:
        '200':
          description: |
            Contactgegevens. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Contact not found |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'

  /v1/phonebook/contact/{contact_id}:
    put:
      operationId: editContact
      tags: [Telefoonboek]
      summary: Contact bewerken
      description: Werk een bestaand contact bij. Alle parameters zijn optioneel — enkel meegegeven velden worden aangepast.
      parameters:
        - name: contact_id
          in: path
          required: true
          schema:
            type: integer
            example: 12345
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/ContactEditRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/ContactEditRequest'
      responses:
        '200':
          description: |
            Contact bijgewerkt. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — contact bijgewerkt |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Contact not found |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
    delete:
      operationId: deleteContact
      tags: [Telefoonboek]
      summary: Contact verwijderen
      description: Verwijder een contact uit het telefoonboek.
      parameters:
        - name: contact_id
          in: path
          required: true
          schema:
            type: integer
            example: 12345
      responses:
        '200':
          description: |
            Contact verwijderd. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — contact verwijderd |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Contact not found |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'

  # ─────────────────────────────────────────────
  # Telefoonboek - Groepen
  # ─────────────────────────────────────────────
  /v2/phonebook/groups:
    get:
      operationId: getAllGroups
      tags: [Telefoonboek]
      summary: Alle groepen ophalen
      description: Haal alle groepen op uit het telefoonboek.
      responses:
        '200':
          description: |
            Lijst van groepen. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
    post:
      operationId: addGroup
      tags: [Telefoonboek]
      summary: Groep toevoegen
      description: Maak een nieuwe groep aan in het telefoonboek.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/GroupRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/GroupRequest'
      responses:
        '200':
          description: |
            Groep aangemaakt. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — groep aangemaakt |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Name not provided |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'

  /v2/phonebook/groups/{group_id}:
    get:
      operationId: getGroup
      tags: [Telefoonboek]
      summary: Groep ophalen op ID
      description: Haal de gegevens van één groep op via zijn ID.
      parameters:
        - name: group_id
          in: path
          required: true
          schema:
            type: integer
            example: 12345
      responses:
        '200':
          description: |
            Groepgegevens. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Group not found |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
    put:
      operationId: editGroup
      tags: [Telefoonboek]
      summary: Groep bewerken
      description: Werk een bestaande groep bij.
      parameters:
        - name: group_id
          in: path
          required: true
          schema:
            type: integer
            example: 12345
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/GroupRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/GroupRequest'
      responses:
        '200':
          description: |
            Groep bijgewerkt. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — groep bijgewerkt |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Group not found |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
    delete:
      operationId: deleteGroup
      tags: [Telefoonboek]
      summary: Groep verwijderen
      description: Verwijder een groep uit het telefoonboek.
      parameters:
        - name: group_id
          in: path
          required: true
          schema:
            type: integer
            example: 12345
      responses:
        '200':
          description: |
            Groep verwijderd. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — groep verwijderd |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
            | `30` | Group not found |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'

  # ─────────────────────────────────────────────
  # Authenticatie
  # ─────────────────────────────────────────────
  /v1/auth:
    get:
      operationId: authenticate
      tags: [Authenticatie]
      summary: API key valideren
      description: Test of je API key geldig is.
      responses:
        '200':
          description: |
            Authenticatie resultaat. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Success — apikey is valid |
            | `20` | Apikey not provided |
            | `21` | Apikey not allowed |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'

  # ─────────────────────────────────────────────
  # Tracking / Short URL
  # ─────────────────────────────────────────────
  /v2/message/tracking:
    post:
      operationId: createTrackingUrl
      tags: [Tracking]
      summary: Short URL aanmaken met tracking
      description: |
        Maak een verkorte URL aan (bv. `v3.be/esaI3W`) voor gebruik in berichten.
        Elke URL is **30 dagen** geldig. Gratis bij voldoende saldo.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TrackingRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/TrackingRequest'
      responses:
        '200':
          description: |
            Tracking URL aangemaakt. De werkelijke status zit in het `code`-veld:

            | Code | Betekenis |
            |------|-----------|
            | `10` | Created — tracking url is aangemaakt |
            | `90` | Url not passed — geen url meegegeven |
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrackingResponse'

# ═══════════════════════════════════════════════
# Components
# ═══════════════════════════════════════════════
components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-Api-Key
      description: >
        SMSBOX API key. Aan te maken via het SMSBOX account.
        Kan ook als `apikey` query/body parameter meegegeven worden.

  schemas:
    # ── Generieke response wrapper ───────────────
    StandardResponse:
      type: object
      description: >
        Generieke response. Alle endpoints geven HTTP 200 terug —
        de werkelijke status zit in het `code`-veld.
      properties:
        code:
          type: integer
          description: Statuscode (zie endpoint-beschrijving)
        message:
          description: Resultaat of foutmelding
          oneOf:
            - type: string
            - type: array
              items:
                type: object
            - type: object

    # ── WhatsApp ─────────────────────────────────
    WhatsAppSendRequest:
      type: object
      required: [to]
      description: >
        Verstuur exact **één** van: `message` (tekst), `media_url` (media) of
        `template` (template bericht).
      properties:
        to:
          type: string
          description: Mobiel nummer van de ontvanger (countrycode + nummer)
          example: '32496123456'
        message:
          type: string
          description: >
            Vrije tekst bericht. Bij media optioneel als bijschrift.
            Niet combineren met `template`.
          example: 'Hallo, uw bestelling is onderweg!'
        media_url:
          type: string
          format: uri
          description: Publieke URL naar het mediabestand. Niet combineren met `template`.
          example: 'https://example.com/factuur.pdf'
        template:
          type: string
          description: >
            Naam van een goedgekeurde WhatsApp template. Een overzicht van de
            beschikbare templates staat op de
            [WhatsApp templates-pagina](/cp/smsbox/whatsapp/templates).
          example: 'order_confirmation'
        parameters:
          type: array
          description: Waarden voor de variabelen in de template, in volgorde.
          items:
            type: string
          example: ['Jan', '12345']
        language:
          type: string
          description: >
            Taalcode van de template. Valt automatisch terug op een andere
            variant van dezelfde taal (bv. `nl` ↔ `nl_BE`).
          default: 'nl_BE'
          example: 'nl_BE'

    WhatsAppSendResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
          example: success
        to:
          type: string
          description: Het opgeschoonde ontvangernummer
          example: '32496123456'
        type:
          type: string
          enum: [text, media, template]
          description: Het verstuurde berichttype
          example: template

    # ── Teams ────────────────────────────────────
    TeamsSendRequest:
      type: object
      required: [to, message]
      description: >
        Stuurt een tekstbericht in een 1-op-1 Teams-chat met `to`.
      properties:
        to:
          type: string
          format: email
          description: >
            E-mailadres (user principal name) van de ontvanger, in dezelfde
            Microsoft-tenant als het gekoppelde serviceaccount.
          example: 'jan@uwbedrijf.be'
        message:
          type: string
          description: De tekst van het bericht.
          maxLength: 4000
          example: 'Er staat een nieuwe bestelling klaar.'

    TeamsSendResponse:
      type: object
      properties:
        status:
          type: string
          enum: [success]
          example: success
        to:
          type: string
          description: De ontvanger
          example: 'jan@uwbedrijf.be'
        message_id:
          type: string
          description: >
            Het bericht-id bij Microsoft. Handig om bij een supportvraag naar te
            verwijzen.
          example: '1616991851193'

    TeamsErrorResponse:
      type: object
      properties:
        status:
          type: string
          enum: [error]
          example: error
        message:
          type: string
          description: Leesbare foutmelding
          example: 'Validation failed'
        code:
          type: string
          description: Machine-leesbare foutcode
          example: 'RECIPIENT_NOT_FOUND'
        errors:
          type: object
          description: >
            Veld-specifieke foutmeldingen (alleen bij validatiefouten, HTTP 422).
          additionalProperties:
            type: string

    WhatsAppErrorResponse:
      type: object
      properties:
        status:
          type: string
          enum: [error]
          example: error
        message:
          type: string
          description: Leesbare foutmelding
          example: 'Validation failed'
        code:
          type: string
          description: Machine-leesbare foutcode
          example: 'TO_REQUIRED'
        errors:
          type: object
          description: >
            Veld-specifieke foutmeldingen (alleen bij validatiefouten, HTTP 422).
            Sleutel = veldnaam, waarde = melding.
          additionalProperties:
            type: string
          example:
            to: 'Required'

    # ── Send SMS V1 ──────────────────────────────
    SendSmsV1Request:
      type: object
      required: [numbers, message]
      properties:
        numbers:
          type: string
          description: Mobiele nummers gescheiden door een komma (countrycode+nummer)
          example: '32496123456,32479123456'
        message:
          type: string
          description: Het te versturen bericht (max 160 tekens, of 918 met longsms)
          maxLength: 918
        from:
          type: string
          description: >
            Afzender. Alphanumeriek max 11 tekens, numeriek max 16 tekens.
            Enkel zinvol voor NIET-BE bestemmingen.
          maxLength: 16
        longsms:
          type: integer
          enum: [0, 1]
          default: 0
          description: Long SMS activeren (max 918 tekens)
        noti:
          type: string
          format: uri
          description: Webhook URL voor afleverrapporten (HTTP POST met msgid en status)
        unicode:
          type: integer
          enum: [0, 1]
          default: 0
          description: Unicode SMS (max 70 tekens per bericht i.p.v. 160)

    SendSmsV1Response:
      type: object
      properties:
        code:
          type: integer
          description: Statuscode (100 = succesvol verzonden)
          example: 100
        message:
          oneOf:
            - type: array
              items:
                type: object
                properties:
                  number:
                    type: string
                    example: '32496123456'
                  msgid:
                    type: integer
                    example: 123
            - type: string
              description: Foutmelding bij niet-100 codes

    # ── Send Message V2 ──────────────────────────
    SendMessageV2Request:
      type: object
      required: [mobile, message]
      properties:
        mobile:
          type: string
          description: Mobiele nummers gescheiden door een komma (countrycode+nummer)
          example: '32496123456,32479123456'
        message:
          type: string
          description: Het te versturen bericht
          maxLength: 918
        from:
          type: string
          description: >
            Afzender. Alphanumeriek max 11 tekens, numeriek max 16 tekens.
            Enkel zinvol voor NIET-BE bestemmingen.
          maxLength: 16
        date:
          type: string
          description: Gepland verzendmoment (formaat jjjj-MM-dd UU:mm)
          example: '2026-04-13 14:30'
        longsms:
          type: boolean
          default: false
          description: Long SMS activeren (> 160 tekens)
        channel:
          type: string
          description: Kanaal (zie controle paneel)
        noti:
          type: string
          format: uri
          description: Webhook URL voor afleverrapporten (HTTP POST met msgid en status)
        test:
          type: boolean
          default: false
          description: Test modus — geen daadwerkelijke verzending

    SendMessageV2Response:
      type: object
      properties:
        code:
          type: integer
          description: Statuscode (100 = succesvol verzonden)
          example: 100
        timestamp:
          type: string
          description: Tijdstip van verzending
          example: '2022-06-10 12:35:10'
        message:
          oneOf:
            - type: array
              items:
                type: object
                properties:
                  mobile:
                    type: string
                    example: '32496123456'
                  msgid:
                    type: integer
                    example: 123
                  type:
                    type: string
                    enum: [SMS]
            - type: string
              description: Foutmelding bij niet-100 codes

    # ── Inbox ────────────────────────────────────
    InboxResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = berichten gevonden
          example: 10
        data:
          type: array
          items:
            type: object
            properties:
              sender:
                type: string
                description: Mobiel nummer van de afzender
              msgid:
                type: integer
              channel:
                type: string
              message:
                type: string
              readen:
                type: boolean
              datetime:
                type: string
                example: '2022-08-02 15:20:21'
              name:
                type: string
                description: Naam van het contact (indien bekend)

    # ── Outbox ───────────────────────────────────
    OutboxResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = berichten gevonden
          example: 10
        data:
          type: array
          items:
            type: object
            properties:
              recipient:
                type: string
                description: Mobiel nummer van de ontvanger
              msgid:
                type: integer
              channel:
                type: string
              message:
                type: string
              datetime:
                type: string
                example: '2022-08-02 15:20:21'
              name:
                type: string
                description: Naam van het contact (indien bekend)

    # ── Balance ──────────────────────────────────
    BalanceResponse:
      type: object
      properties:
        code:
          type: integer
          description: 100 = succes
          example: 100
        message:
          type: string
          description: Resterend aantal credits
          example: '1630.38'

    # ── OTP ──────────────────────────────────────
    OtpSendV1Request:
      type: object
      description: Vereist `number` en/of `email`.
      properties:
        number:
          type: string
          description: Mobiel nummer (countrycode+nummer)
          example: '32496123456'
        email:
          type: string
          format: email
          description: E-mailadres
          example: 'name@domain.com'
        text:
          type: string
          description: >
            Eigen tekst i.p.v. de standaardtekst.
            Gebruik `{OTP}` als placeholder voor de OTP-code.
          example: 'Beste klant, uw code is {OTP}. Vul deze in op de website.'

    OtpSendV2Request:
      type: object
      description: Vereist `number` en/of `email`.
      properties:
        number:
          type: string
          description: Mobiel nummer (countrycode+nummer)
          example: '32496123456'
        email:
          type: string
          format: email
          description: E-mailadres
          example: 'name@domain.com'
        text:
          type: string
          description: >
            Eigen tekst i.p.v. de standaardtekst.
            Gebruik `{OTP}` als placeholder voor de OTP-code.
          example: 'Beste klant, uw code is {OTP}. Vul deze in op de website.'
        message_type:
          type: string
          enum: [SMS, WA]
          default: SMS
          description: >
            Verzendkanaal. `SMS` voor sms, `WA` voor WhatsAppBeta.
            WhatsAppBeta valt terug op SMS bij onbereikbaarheid.
        lang_id:
          type: string
          enum: [NL, EN, FR]
          default: NL
          description: Taal van de standaardtekst

    OtpSendResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = code verzonden
          example: 10
        message:
          type: string
          example: 'Code has been sent'
        number:
          type: string
          description: Het ontvangernummer

    OtpVerifyResponse:
      type: object
      properties:
        code:
          type: integer
          description: 11 = geverifieerd, 51 = OTP niet correct
          example: 11
        message:
          type: string
          example: 'Number is verified'
        number:
          type: string
          description: Het ontvangernummer (enkel bij succesvolle verificatie)

    # ── HLR ──────────────────────────────────────
    HlrResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = succes
          example: 10
        message:
          type: object
          properties:
            issueing_info:
              $ref: '#/components/schemas/HlrNetworkInfo'
            home_info:
              $ref: '#/components/schemas/HlrNetworkInfo'
            type:
              type: string
              example: 'Mobile'
            msisdn:
              type: string
              example: '32498765432'
            error_code:
              type: integer
            error_text:
              type: string
              example: 'Live'
            status:
              type: string
              example: 'Delivered'
            mccmnc:
              type: string
              example: '20601'
            is_ported:
              type: boolean
            is_roaming:
              type: boolean
        number:
          type: string

    HlrNetworkInfo:
      type: object
      properties:
        verified:
          type: boolean
        timezone:
          type: string
          example: 'Europe/Brussels'
        location:
          type: string
          example: 'Belgium'
        network_name:
          type: string
          example: 'Proximus'
        country_code:
          type: integer
          example: 32
        area:
          type: string
          example: 'Belgium'

    # ── Number Format ────────────────────────────
    NumberFormatResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = succes
          example: 10
        message:
          type: string
          description: Het geformateerde nummer in internationaal formaat
          example: '32498765432'

    # ── Voice ────────────────────────────────────
    VoiceSendRequest:
      type: object
      required: [number, message]
      properties:
        number:
          type: string
          description: Mobiel nummer (countrycode+nummer)
          example: '32496123456'
        message:
          type: string
          description: Het bericht dat voorgelezen wordt
        lang:
          type: string
          enum: [NL, FR, EN]
          default: NL
          description: Taal van het bericht
        gender:
          type: string
          enum: [M, F]
          default: M
          description: Geslacht van de stem
        send_at:
          type: string
          format: date-time
          description: Gepland verzendmoment (ISO 8601). Bij ongeldige datum/tijd wordt huidige datum/tijd genomen.
          example: '2026-04-13T14:30:00Z'

    VoiceSendResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = succes
          example: 10
        message:
          type: string
          example: 'Voice is sent'
        number:
          type: string

    # ── Phonebook ────────────────────────────────
    ContactRequest:
      type: object
      required: [mobile, name]
      properties:
        mobile:
          type: string
          description: Mobiel nummer (countrycode+nummer)
          example: '32496123456'
        name:
          type: string
          description: Naam van het contact (urlencoded)
          example: 'Jan Janssens'

    ContactEditRequest:
      type: object
      description: Alle velden optioneel — enkel meegegeven velden worden aangepast.
      properties:
        mobile:
          type: string
          description: Mobiel nummer (countrycode+nummer)
          example: '32496123456'
        name:
          type: string
          description: Naam van het contact (urlencoded)
          example: 'Jan Janssens'

    GroupRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Naam van de groep (urlencoded)
          example: 'Klanten Gent'
        desc:
          type: string
          description: Omschrijving van de groep (urlencoded)
          example: 'Alle actieve klanten in regio Gent'

    # ── Auth ─────────────────────────────────────
    AuthResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = succes
          example: 10
        message:
          type: object
          properties:
            user:
              type: string
              description: Gebruikersnaam
            credits:
              type: number
              format: double
              description: Resterend aantal credits
            email:
              type: string
              format: email
            service:
              type: string
              example: 'SMS'

    # ── Tracking ─────────────────────────────────
    TrackingRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: De URL om te verkorten
          example: 'https://www.jaan.be/nieuws/sms-actie'
        name:
          type: string
          description: Friendly name voor de tracking URL
          example: 'SMS actie Q2'

    TrackingResponse:
      type: object
      properties:
        code:
          type: integer
          description: 10 = aangemaakt
          example: 10
        message:
          type: string
          example: 'Created'

    # ── Delivery Report (Webhook) ────────────────
    DeliveryReport:
      type: object
      description: >
        Wordt via HTTP POST naar de `noti` URL gestuurd.
        Statuscodes: 10=Sent, 11=Delivered, 12=Buffered, 13=Undelivered,
        14=Expired, 15=Rejected, 16=Unknown, 20=Blacklist, 21=LowBalance, 01=Error.
      properties:
        msgid:
          type: integer
        status:
          type: integer
          enum: [1, 10, 11, 12, 13, 14, 15, 16, 20, 21]