{
  "openapi": "3.0.3",
  "info": {
    "title": "EnvoiSMS API",
    "x-service-info": {
      "title": "EnvoiSMS API",
      "description": "Telecom messaging and phone verification APIs for Morocco",
      "categories": [
        "telecom",
        "messaging",
        "sms",
        "otp"
      ]
    },
    "description": "Send SMS and WhatsApp messages and verify phone numbers, as JSON over HTTPS.\nGuides, copy-paste samples (cURL, Node.js, Python, PHP) and the full error\ncatalogue: https://envoisms.ma/en/docs/\n\n## Quickstart\n```bash\nexport ENVOISMS_API_KEY=env_test_...   # a sandbox key: nothing is sent or charged\ncurl https://api.envoisms.ma/v1/messages \\\n  -H \"Authorization: Bearer $ENVOISMS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"to\": \"+212612345678\", \"message\": \"Your order #1042 has shipped.\"}'\n```\n\n## Authentication\nSend `Authorization: Bearer <key>` on every request. Create keys in the\ndashboard (Dashboard → API keys); a key is shown once, when it is created.\n\n- `smr_...` — a **live** key: messages are sent and charged.\n- `env_test_...` — a **sandbox** key: every request is validated exactly as\n  a live one, but nothing is sent and nothing is charged. `POST /messages`\n  returns `sandbox: true` and `simulated_cost` (the price a live key would\n  pay); `POST /verify/send` returns `sandbox_code`. Each key sees only its\n  own kind of traffic.\n\n**Sandbox test numbers** (`POST /messages`, sandbox keys only):\n`+212600000002` fails (`message.failed`, `SANDBOX_TEST_FAILED`),\n`+212600000003` is undeliverable (`message.undeliverable`),\n`+212600000004` is opted out (`403 OPTED_OUT` at request time),\n`+212600000005` never gets a receipt; every other number is delivered\n(`message.delivered`) about 2 seconds after the send.\n\nEach key carries a list of permissions; a call outside them returns\n`403 FORBIDDEN`. Each operation below states the permission it needs. An\noptional IP allow-list per key turns a call from any other IP into\n`401 UNAUTHORIZED`.\n\nThe WhatsApp setup routes (`/whatsapp/...`: connecting numbers, profile,\ntools, relay settings) change state only from a dashboard session; an API\nkey may read `GET /whatsapp/waba-connection` and `GET /whatsapp/profile`.\n\n## Base URL\n```\nhttps://api.envoisms.ma/v1\n```\n`GET /health` is served from the API root (`https://api.envoisms.ma/health`).\n\n## Rate limiting\nLimits apply per API key over a one-minute window. A new key gets its\nplan's default — Starter 60, Business 180, Pro 500, Enterprise 1,200\nrequests/minute — adjustable per key.\n\n| Header | Meaning |\n|---|---|\n| `X-RateLimit-Limit` | The key's requests/minute allowance |\n| `X-RateLimit-Remaining` | Requests left in the current window |\n| `X-RateLimit-Reset` | Unix time (seconds) when the window resets |\n\nOver the limit the API returns `429 RATE_LIMITED` with `Retry-After`\n(seconds). Honor it — retrying earlier only consumes the next window.\n\n## Idempotency\n`POST /messages`, `POST /messages/bulk` and their `/waba/messages`\nequivalents accept an `Idempotency-Key` header (up to 255 characters, e.g.\na UUID). Retry a timed-out or failed call with the **same key**: the send\nhappens at most once, and the retry returns the original JSON body with\n`Idempotency-Replayed: true`.\n\n- Keys are scoped to your account and kept for 24 hours.\n- Every response below 500 is stored and replayed — **including 4xx**\n  (e.g. `402`, `429`). After a 4xx, fix the cause and use a **new** key.\n  A 5xx is not stored: retry it with the same key.\n- The same key with a **different** body returns `422 IDEMPOTENCY_KEY_REUSED`.\n- While the original request still runs, a retry returns\n  `409 IDEMPOTENCY_IN_FLIGHT` with `Retry-After`.\n\n## Pagination\nLists take `limit` and `offset` and return\n`{\"data\": [...], \"limit\", \"offset\", \"total\"}`. Each list states its\ndefault and maximum `limit`.\n\n## Versioning and deprecation\nThe version is in the path (`/v1`). Within `/v1` we only make additive\nchanges: new endpoints, new optional request fields, new response fields,\nnew error codes and new webhook event types. Write clients that ignore\nfields they do not know.\n\nA breaking change ships as a new major path (`/v2`) beside `/v1`, never in\nplace. Retiring a `/v1` endpoint, field or behaviour follows this notice:\n\n- At least **12 months** between the announcement and the date it stops\n  working (the sunset), announced by e-mail to the account owner, in the\n  changelog and on the endpoint itself.\n- Every response to a deprecated endpoint carries\n  `Deprecation: @<unix-seconds>` (RFC 9745, when it was deprecated),\n  `Sunset: <HTTP-date>` (RFC 8594, when it stops working) and\n  `Link: <url>; rel=\"deprecation\"` pointing at the migration guide.\n  Browsers can read them (`Access-Control-Expose-Headers`).\n- Security fixes are the one exception: an endpoint that cannot be made\n  safe may be changed or withdrawn sooner, with notice as early as we can\n  give it.\n\nNo `/v1` endpoint is deprecated today.\n\n## Webhooks\nRegister an HTTPS endpoint with `POST /webhooks`; the response carries its\nsigning secret, shown once (`POST /webhooks/{id}/rotate-secret` issues a\nnew one). Each delivery is a JSON `POST`:\n\n```json\n{\"event\": \"message.delivered\", \"data\": {\"id\": \"msg_...\", \"to\": \"+212612345678\", \"channel\": \"sms\", \"status\": \"delivered\", \"metadata\": {}}, \"timestamp\": \"2026-10-22T10:30:04.000Z\"}\n```\n\nEvents: `message.sent`, `message.delivered`, `message.read`,\n`message.failed`, `message.undeliverable`, `message.fallback`,\n`message.inbound`, `message.reaction`, `contact.optout`, `contact.optin`,\n`lead.qualified`. Subscribe to an exact name, a namespace (`message.*`) or\n`*`. A message status carries `{id, to, channel, status, error_code?,\nerror_message?, metadata?}`.\n\n**Signature.** Verify `X-EnvoiSMS-Signature-256: t=<unix-seconds>,v1=<hex>`:\ncompute `HMAC-SHA256(secret, \"{t}.\" + raw_request_body)` (hex), compare it\nto `v1` in constant time, and reject when `|now - t|` exceeds 5 minutes.\nThe older `X-EnvoiSMS-Signature: sha256=<hex>` (HMAC of the body alone, no\ntimestamp) is still sent for existing integrations. The event name is also\nin `X-EnvoiSMS-Event`.\n\n**Retries.** A `5xx`, a `429` or a network error is retried after 1 minute,\n10 minutes and 1 hour (4 attempts). Any other `4xx` is not retried.\n`GET /webhooks/logs` lists recent attempts and\n`POST /webhooks/logs/{log_id}/replay` sends one again. Answer `2xx` within\n10 seconds and de-duplicate on `data.id` + `event`.\n\n**WhatsApp relay.** A connected WhatsApp number set to *webhook* mode in the\ndashboard relays its messages and statuses to your URL in WhatsApp Cloud's\nnative shape, signed with `X-Hub-Signature-256: sha256=<hex>` —\n`HMAC-SHA256(webhook_secret, raw_request_body)` with that number's secret,\nshown once in the dashboard when it is created or rotated. Without a secret\nnothing is relayed.\n\n## Errors\n```json\n{\n  \"error\": {\n    \"code\": \"INVALID_PHONE\",\n    \"message\": \"to must be E.164 format, for example +212612345678\",\n    \"docs\": \"https://envoisms.ma/en/docs#errors\"\n  }\n}\n```\n\n`code` is the stable, machine-readable key — branch on it, never on the\nmessage text. `message` is an English sentence saying what went wrong and\nwhat to do; its wording may improve. Every code an API key can receive,\ngrouped and with what to do, is at https://envoisms.ma/en/docs/#errors.\nThe most common:\n\n| Code | HTTP | What to do |\n|---|---|---|\n| `UNAUTHORIZED` | 401 | Send a valid key as `Authorization: Bearer <key>` (also: IP not on the key's allow-list) |\n| `FORBIDDEN` | 403 | Add the permission the operation needs to the key |\n| `INVALID_JSON` / `MISSING_FIELD` / `INVALID_FIELD` | 400 | Fix the body; the message names the field |\n| `INVALID_PHONE` | 400 | Send E.164, e.g. `+212612345678` |\n| `INSUFFICIENT_BALANCE` | 402 | Top up; nothing was charged |\n| `OPTED_OUT` | 403 | The recipient unsubscribed (STOP); do not resend |\n| `WHATSAPP_NOT_CONNECTED` | 403 | Connect a WhatsApp Business number, or omit `channel` to send SMS |\n| `OUT_OF_24H_WINDOW` | 400 | Send an approved `template`, or an SMS; nothing was charged |\n| `SENDER_ID_GENERIC` / `SENDER_ID_PROTECTED_BRAND` | 400 / 403 | Use your own brand name as `sender_id`, or omit it |\n| `CONTENT_REFUSED` / `CONTENT_BLOCKED` / `SPAM_OR_PHISHING_DETECTED` | 400 | Content refused; appeal with `POST /moderation/appeals` and `error.appeal` |\n| `MISSING_TEMPLATE_PARAMS` | 400 | Supply the listed values in `template_variables` |\n| `CONSENT_ATTESTATION_REQUIRED` | 400 | WhatsApp marketing campaigns need `consent_attested: true` |\n| `TEMPLATE_PENDING_REVIEW` / `TEMPLATE_REJECTED_BY_REVIEW` | 409 | Wait for the EnvoiSMS review, or edit the template to resubmit |\n| `INVALID_CODE` / `MAX_ATTEMPTS` / `SESSION_NOT_FOUND` | 400 / 429 / 404 | OTP: wrong code, too many attempts, or session expired — start a new one |\n| `OTP_COOLDOWN` / `OTP_RATE_LIMIT` / `OTP_COUNTRY_PAUSED` | 429 | Wait `Retry-After` |\n| `RATE_LIMITED` / `QUOTA_EXCEEDED` | 429 | Wait `Retry-After` / your daily quota resets at 00:00 UTC |\n| `IDEMPOTENCY_IN_FLIGHT` / `IDEMPOTENCY_KEY_REUSED` | 409 / 422 | Retry with the same key / use a new key |\n| `INTERNAL_ERROR` | 500 | Retry with backoff (same `Idempotency-Key`); contact support if it persists |\n\nTreat an unknown code as a failure of that request, not of your\nintegration.\n\n### WhatsApp failure codes\nA WhatsApp message that WhatsApp refuses ends `failed` with one of these\n`error_code` values (on `GET /messages/{id}` and in the `failed` webhook),\neach with an English `error_message`:\n\n| Code | WhatsApp code | Meaning | Automatic retry |\n|---|---|---|---|\n| `META_WINDOW_EXPIRED` | 131047 | 24-hour customer service window closed — send a template | no |\n| `META_UNDELIVERABLE` | 131026 | Number not reachable on WhatsApp | never |\n| `META_MARKETING_LIMIT` | 131049 | Per-user marketing message limit (ecosystem protection) — do not resend immediately | never |\n| `META_PAIR_RATE_LIMITED` | 131056 | Too many messages to this contact in a short time | yes |\n| `META_RATE_LIMITED` | 130429 | Throughput limit of the sending number | yes |\n| `META_POLICY_BLOCKED` | 368 | Sending number temporarily blocked for policy violations | never |\n| `META_ACCOUNT_LOCKED` | 131031 | WhatsApp Business account locked | no |\n| `META_PAYMENT_ISSUE` | 131042 | Payment-method problem on the WhatsApp Business account | no |\n| `META_UNSUPPORTED_TYPE` | 131051 | Message type not supported | no |\n| `META_TEMPLATE_PARAM_MISMATCH` | 132000 | Template variable count does not match | no |\n| `META_TEMPLATE_NOT_FOUND` | 132001 | Template missing in that language, or not approved | no |\n| `META_TEMPLATE_PARAM_FORMAT` | 132012 | Template variable in the wrong format | no |\n| `META_PHONE_NOT_REGISTERED` | 133010 | Sending number not registered | no |\n| `META_GENERIC_ERROR` / `META_SERVICE_UNAVAILABLE` | 131000 / 131016 | Temporary WhatsApp-side failure | yes |\n| `META_<code>` | other | Any other WhatsApp code, with WhatsApp's own description | no |\n\n\"yes\" = re-sent automatically with a growing delay, three attempts in\ntotal, before the message fails. \"never\" = retrying would only hurt the\nsending number's quality, so it is not retried even on a server error. A\nmessage refused before WhatsApp accepted it is refunded automatically.\n",
    "version": "1.0.0",
    "contact": {
      "name": "EnvoiSMS support",
      "email": "contact@envoisms.ma",
      "url": "https://envoisms.ma/support"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://envoisms.ma/en/legal/terms/"
    }
  },
  "servers": [
    {
      "url": "https://api.envoisms.ma/v1",
      "description": "Production (live and sandbox keys)"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your EnvoiSMS API key"
      }
    },
    "schemas": {
      "ConsentEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "cns_9f2a..."
          },
          "phone": {
            "type": "string",
            "example": "+212612345678"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "sms",
              "any"
            ]
          },
          "purpose": {
            "type": "string",
            "enum": [
              "marketing",
              "transactional",
              "otp",
              "service"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "granted",
              "withdrawn"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "api",
              "form",
              "import",
              "dashboard",
              "whatsapp_keyword",
              "sms_keyword",
              "whatsapp_inbound",
              "ctwa"
            ]
          },
          "evidence": {
            "type": "object",
            "nullable": true
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Message": {
        "type": "object",
        "description": "Response to POST /messages — the message as accepted into the send queue.",
        "properties": {
          "id": {
            "type": "string",
            "example": "msg_8f2da1b2c3d4e5f6a7b8c9d0"
          },
          "to": {
            "type": "string",
            "example": "+212612345678"
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "whatsapp",
              "voice",
              "rcs"
            ],
            "description": "sms is generally available with multi-operator routing. whatsapp sends from your own connected WhatsApp Business number (templates, text, media, interactive messages, reactions, contacts, locations) via the official WhatsApp Cloud API. voice and rcs are available on request."
          },
          "cascade": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "example": "queued"
          },
          "cost": {
            "type": "object",
            "description": "What was actually charged. Always zero on a sandbox key.",
            "properties": {
              "eur": {
                "type": "number",
                "example": 0.072
              },
              "mad": {
                "type": "number",
                "example": 0.79
              }
            }
          },
          "segments": {
            "type": "integer",
            "example": 1
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "sandbox": {
            "type": "boolean",
            "description": "Present and true only when the request used an `env_test_` key. Nothing was delivered and nothing was charged."
          },
          "simulated_cost": {
            "type": "object",
            "description": "Sandbox only. What this exact send would have cost on a live key, at the account's own rates — so an integration can be costed before it goes live. Reported separately from `cost` so a rehearsal can never be mistaken for revenue.\n",
            "properties": {
              "eur": {
                "type": "number",
                "example": 0.072
              },
              "mad": {
                "type": "number",
                "example": 0.79
              }
            }
          },
          "warnings": {
            "type": "array",
            "description": "Sandbox only. What a live key would have been refused for — today `OUT_OF_24H_WINDOW` for a free-form WhatsApp message outside the customer service window. The sandbox still simulates the send.\n",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "example": "OUT_OF_24H_WINDOW"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "MessageRecord": {
        "type": "object",
        "description": "A stored message as returned by GET /messages/{id}. The metadata object provided at send time is not echoed here; it travels in DLR webhooks.",
        "properties": {
          "id": {
            "type": "string",
            "example": "msg_8f2da1b2c3d4e5f6a7b8c9d0"
          },
          "campaign_id": {
            "type": "string",
            "nullable": true
          },
          "to": {
            "type": "string",
            "example": "+212612345678"
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "whatsapp",
              "telegram",
              "voice",
              "rcs"
            ]
          },
          "body": {
            "type": "string",
            "example": "Votre code est 1234"
          },
          "sender_id": {
            "type": "string",
            "example": "MonApp"
          },
          "unicode": {
            "type": "integer",
            "description": "1 when the body required the UCS-2 alphabet (Arabic etc.), 0 for GSM-7"
          },
          "segments": {
            "type": "integer",
            "example": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "failed",
              "undeliverable",
              "unconfirmed"
            ],
            "description": "unconfirmed = the network accepted the message but never reported a final outcome within the reporting window; a delivery receipt arriving later still replaces it."
          },
          "error_code": {
            "type": "string",
            "nullable": true,
            "description": "Set on failed/undeliverable messages. A short diagnostic key: DLR_<network status> when the failure came from a delivery report (e.g. DLR_REJECTD, DLR_EXPIRED, DLR_UNDELIV), GATEWAY_* for a failure on the EnvoiSMS routing side before the network was reached, ENVOISMS_* for other routing diagnostics, META_* for a WhatsApp refusal (see \"WhatsApp failure codes\" in the API description). Treat unknown values as a generic terminal failure — new keys may appear."
          },
          "error_message": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable failure reason in English, set together with error_code. When the network gave no reason, this reads \"The destination network did not deliver this message and gave no reason.\" Display text — do not parse it programmatically; branch on status and error_code instead."
          },
          "cost_eur": {
            "type": "number",
            "example": 0.072
          },
          "cost_mad": {
            "type": "number",
            "example": 0.79
          },
          "scheduled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "delivered_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "read_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Timestamp when the recipient opened and read the message (WhatsApp blue checkmark)"
          },
          "failed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "operator": {
            "type": "string",
            "nullable": true,
            "example": "Maroc Telecom",
            "description": "Resolved destination network for Moroccan numbers (Maroc Telecom, Orange, Inwi, Autre); best-effort elsewhere."
          },
          "sandbox": {
            "type": "integer",
            "description": "1 when the message was created by an env_test_ sandbox key"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SendMessageRequest": {
        "type": "object",
        "required": [
          "to"
        ],
        "description": "`message` is required for SMS and for WhatsApp text and interactive messages. A WhatsApp message whose content is a template, a media object, a reaction, contacts, a location or a sticker needs no `message` (for media it becomes the caption when given). Exactly one of reaction, contacts, location or sticker per message, and none of them together with another content type.\n",
        "properties": {
          "to": {
            "type": "string",
            "description": "Destination phone in E.164 format",
            "example": "+212612345678"
          },
          "message": {
            "type": "string",
            "description": "Message content (max 1600 chars). See the object description for when it may be omitted on WhatsApp.",
            "example": "Bonjour! Votre commande #1234 est confirmée."
          },
          "from": {
            "type": "string",
            "description": "SMS sender shown to the recipient (3–11 letters, digits or spaces; a generic header such as INFO or a bank name is refused). Omitted, the SMS goes out under the shared sender EnvoiSMS at 0.792 MAD. Your own name works with no registration at 1.70 MAD until it is validated with your RC, then at the standard rate. Aliases: sender_id, sender.",
            "example": "MonApp"
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "whatsapp",
              "voice",
              "rcs"
            ],
            "default": "sms",
            "description": "Channel for message delivery: sms (default) for Moroccan operator routes; whatsapp to send from your own connected WhatsApp Business number (403 WHATSAPP_NOT_CONNECTED otherwise). To send an ordinary text to a customer who happens to use WhatsApp, sms is enough: no connection, no template, no 24-hour window. On whatsapp, a message that is not a template may only be sent to a contact who wrote to your number in the last 24 hours — otherwise it is refused with 400 OUT_OF_24H_WINDOW before any charge (with a cascade, WhatsApp is skipped and the next channel is used instead)."
          },
          "cascade": {
            "type": "boolean",
            "description": "When true, tries WhatsApp, then falls back to SMS: at once if WhatsApp refuses the message or reports it failed (not on WhatsApp, 24-hour window closed, ...), or when no delivery report arrives within cascade_timeout (120 s by default). Only a send that actually went out is billed: a refused or failed WhatsApp message is not, and you pay for the SMS alone. A WhatsApp message still waiting for its delivery report did go out (it can still be delivered when the phone reconnects), so it is billed along with its SMS fallback, and refunded if WhatsApp later reports it failed. Tip: give your WhatsApp template a time-to-live (TTL) no longer than cascade_timeout, so a phone that comes back online does not receive both.",
            "default": false
          },
          "cascade_timeout": {
            "type": "integer",
            "minimum": 30,
            "maximum": 43200,
            "default": 120,
            "description": "With cascade: seconds to wait for a delivery report before the next channel. Shorter falls back faster but bills more double sends; longer means fewer duplicates. Out of range: 400 INVALID_FIELD."
          },
          "metadata": {
            "type": "object",
            "description": "Custom key-value pairs, stored with the message and returned in DLR webhooks. One key carries platform semantics: set purpose: \"otp\" when the message carries a one-time code you generated yourself — if the network reports a hard delivery failure on a Moroccan number while the code is still fresh (within 10 minutes), the platform automatically re-sends it once over an alternate SMS route, under the same message id and at no extra charge. Prefer /verify/send when you want the whole code lifecycle (generation, delivery cascade, validation, expiry) managed for you. Set no_failover: true to opt out of this automatic re-send entirely — the first delivery attempt's outcome is then final.",
            "example": {
              "order_id": "ORD-12345",
              "customer_ref": "C-789"
            }
          },
          "buttons": {
            "type": "array",
            "description": "Optional WhatsApp interactive buttons (max 3). Only supported on the whatsapp channel.",
            "items": {
              "$ref": "#/components/schemas/MessageButton"
            }
          },
          "interactive": {
            "type": "object",
            "description": "WhatsApp Cloud API interactive message payload. cta_url: action {name: \"cta_url\", parameters: {display_text (max 20 chars), url}}. location_request_message: body text plus an optional action {name: \"send_location\"}; the contact answers with a location.",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "list",
                  "button",
                  "product",
                  "product_list",
                  "flow",
                  "carousel",
                  "order_details",
                  "cta_url",
                  "location_request_message"
                ]
              },
              "header": {
                "type": "object"
              },
              "body": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string"
                  }
                }
              },
              "footer": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string"
                  }
                }
              },
              "action": {
                "type": "object"
              }
            }
          },
          "template": {
            "type": "object",
            "description": "WhatsApp only. An approved template of your account; exempt from the 24-hour window. Priced by the template's category as registered on your account.",
            "properties": {
              "name": {
                "type": "string",
                "example": "order_update"
              },
              "language": {
                "type": "string",
                "example": "fr"
              },
              "components": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              }
            }
          },
          "phone_number_id": {
            "type": "string",
            "description": "WhatsApp only. Which of your connected numbers sends; defaults to your default number."
          },
          "image": {
            "$ref": "#/components/schemas/WhatsAppMedia"
          },
          "video": {
            "$ref": "#/components/schemas/WhatsAppMedia"
          },
          "audio": {
            "$ref": "#/components/schemas/WhatsAppMedia"
          },
          "document": {
            "$ref": "#/components/schemas/WhatsAppMedia"
          },
          "sticker": {
            "$ref": "#/components/schemas/WhatsAppMedia"
          },
          "reaction": {
            "type": "object",
            "description": "WhatsApp only. React to a message of the conversation. Not billed. An empty emoji removes your reaction.",
            "required": [
              "message_id",
              "emoji"
            ],
            "properties": {
              "message_id": {
                "type": "string",
                "description": "WhatsApp message id (wamid) of the message you react to",
                "example": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg"
              },
              "emoji": {
                "type": "string",
                "example": "👍"
              }
            }
          },
          "contacts": {
            "type": "array",
            "maxItems": 20,
            "description": "WhatsApp only. Contact cards (WhatsApp Cloud API contacts object). name.formatted_name is required; allowed keys: name, phones, emails, urls, addresses, org, birthday (YYYY-MM-DD).",
            "items": {
              "type": "object",
              "required": [
                "name"
              ],
              "properties": {
                "name": {
                  "type": "object",
                  "required": [
                    "formatted_name"
                  ],
                  "properties": {
                    "formatted_name": {
                      "type": "string",
                      "example": "Service client"
                    },
                    "first_name": {
                      "type": "string"
                    },
                    "last_name": {
                      "type": "string"
                    }
                  }
                },
                "phones": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "phone": {
                        "type": "string",
                        "example": "+212522000000"
                      },
                      "type": {
                        "type": "string",
                        "example": "WORK"
                      },
                      "wa_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "location": {
            "type": "object",
            "description": "WhatsApp only. Send a location pin.",
            "required": [
              "latitude",
              "longitude"
            ],
            "properties": {
              "latitude": {
                "type": "number",
                "minimum": -90,
                "maximum": 90,
                "example": 33.5731
              },
              "longitude": {
                "type": "number",
                "minimum": -180,
                "maximum": 180,
                "example": -7.5898
              },
              "name": {
                "type": "string",
                "example": "Agence Casablanca"
              },
              "address": {
                "type": "string",
                "example": "Bd Zerktouni, Casablanca"
              }
            }
          },
          "context": {
            "type": "object",
            "description": "WhatsApp only. Reply to (quote) an earlier message of the conversation. Works with every type except reaction.",
            "required": [
              "message_id"
            ],
            "properties": {
              "message_id": {
                "type": "string",
                "example": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg"
              }
            }
          }
        }
      },
      "WhatsAppMedia": {
        "type": "object",
        "description": "WhatsApp media by exactly one of id (a media id uploaded to WhatsApp) or link (an https URL). caption on image, video and document only; filename on document only.",
        "properties": {
          "id": {
            "type": "string"
          },
          "link": {
            "type": "string",
            "example": "https://example.com/brochure.pdf"
          },
          "caption": {
            "type": "string",
            "maxLength": 1024
          },
          "filename": {
            "type": "string"
          }
        }
      },
      "BulkSendRequest": {
        "type": "object",
        "required": [
          "messages"
        ],
        "properties": {
          "messages": {
            "type": "array",
            "maxItems": 10000,
            "items": {
              "type": "object",
              "required": [
                "to",
                "message"
              ],
              "properties": {
                "to": {
                  "type": "string",
                  "example": "+212612345678"
                },
                "message": {
                  "type": "string",
                  "example": "Bonjour {{name}}!"
                }
              }
            }
          },
          "from": {
            "type": "string"
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "whatsapp",
              "telegram",
              "voice",
              "rcs"
            ],
            "default": "sms",
            "description": "Use sms by default. whatsapp sends free-form text from your connected WhatsApp Business number; each recipient must have written to you in the last 24 hours, and recipients outside that window are listed under rejected (OUT_OF_24H_WINDOW) and not charged. Bulk carries no templates — use campaigns for template sends."
          }
        }
      },
      "VerifySendRequest": {
        "type": "object",
        "required": [
          "to"
        ],
        "properties": {
          "to": {
            "type": "string",
            "example": "+212612345678"
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "whatsapp",
              "telegram",
              "voice",
              "rcs"
            ],
            "default": "sms",
            "description": "whatsapp = EnvoiSMS generates a 6-digit code and delivers it with its own WhatsApp authentication template, nothing to store on your side. The code is valid for expiry seconds, capped at 600 (10 minutes, also the default). If WhatsApp refuses the submission, the code is sent by SMS instead; an SMS follow-up after a delay happens only with a Verify application (app_id) whose channel list includes sms with auto-cascade on. code_length and template are ignored in this mode. sms = classic mode: you control code length, expiry, template and sender. Both modes are confirmed with the same /verify/check call."
          },
          "otp_type": {
            "type": "string",
            "enum": [
              "copy_code",
              "one_tap",
              "zero_tap",
              "url"
            ],
            "default": "copy_code",
            "description": "WhatsApp Authentication template button type (copy_code, one_tap autofill, zero_tap, or url)."
          },
          "package_name": {
            "type": "string",
            "description": "Android application package name for One-Tap / Zero-Tap Autofill",
            "example": "ma.monapp.ecommerce"
          },
          "signature_hash": {
            "type": "string",
            "description": "Android app signature hash (SMS Retriever / Autofill API)",
            "example": "AbCdEf12345"
          },
          "code_length": {
            "type": "integer",
            "minimum": 4,
            "maximum": 8,
            "default": 6,
            "description": "sms channel only — whatsapp verifications always use 6 digits"
          },
          "expiry": {
            "type": "integer",
            "description": "Code validity in seconds, 60–1800 (default 600). On the whatsapp channel it is capped at 600, the authentication template's own limit.",
            "default": 600
          },
          "app_id": {
            "type": "string",
            "description": "Verify Application ID configured in dashboard. If set, channels parameters and cascade logic are automatically managed.",
            "example": "vra_82fb5a71"
          },
          "brand": {
            "type": "string",
            "description": "Brand name shown in OTP message",
            "example": "MaSociété"
          },
          "template": {
            "type": "string",
            "description": "Custom OTP message. Use {{code}} and {{brand}} placeholders.",
            "example": "Code {{brand}}: {{code}}. Ne partagez jamais ce code."
          },
          "otp_button_text": {
            "type": "string",
            "maxLength": 25,
            "description": "Custom label for the WhatsApp click-to-copy OTP button",
            "example": "Copier le code"
          },
          "web_otp_domain": {
            "type": "string",
            "description": "Target domain for W3C WebOTP browser autofill (appends @domain #code to the SMS)",
            "example": "https://monsite.ma"
          },
          "app_hash": {
            "type": "string",
            "description": "11-character Android SMS Retriever API application hash for automatic Android OTP detection",
            "example": "vG1k6XyZ1aB"
          },
          "auto_cascade": {
            "type": "boolean",
            "description": "When the code was sent on WhatsApp and WhatsApp cannot deliver it, fall back to SMS automatically",
            "example": true
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable key, always UPPER_SNAKE_CASE. Branch on this, never on the message text. The common codes are listed in the Error Format section of the API description above.",
                "example": "INSUFFICIENT_BALANCE"
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation in English of what went wrong and what to do about it. Wording may improve over time — display it, do not parse it.",
                "example": "Insufficient balance. Required: EUR 0.072, available: EUR 0.01"
              },
              "docs": {
                "type": "string",
                "description": "Link to the error documentation page.",
                "example": "https://envoisms.ma/en/docs#errors"
              }
            }
          }
        }
      },
      "MessageButton": {
        "type": "object",
        "required": [
          "type",
          "text"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "copy",
              "url",
              "call"
            ],
            "description": "Button action type",
            "example": "copy"
          },
          "text": {
            "type": "string",
            "maxLength": 25,
            "description": "Button text label",
            "example": "Copier le code"
          },
          "copy_code": {
            "type": "string",
            "description": "Value to copy (only for copy type)",
            "example": "123456"
          },
          "url": {
            "type": "string",
            "description": "Target URL (only for url type). Must start with http:// or https://",
            "example": "https://example.com"
          },
          "phone_number": {
            "type": "string",
            "description": "Target phone number (only for call type) in E.164 format",
            "example": "+212612345678"
          }
        }
      },
      "WhatsAppProfile": {
        "type": "object",
        "properties": {
          "about": {
            "type": "string",
            "example": "Service client officiel EnvoiSMS.ma"
          },
          "address": {
            "type": "string",
            "example": "Casablanca, Maroc"
          },
          "description": {
            "type": "string",
            "example": "Infrastructure de messagerie programmable pour les entreprises."
          },
          "email": {
            "type": "string",
            "example": "contact@votremarque.ma"
          },
          "websites": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "https://votremarque.ma"
            ]
          },
          "profile_picture_url": {
            "type": "string",
            "example": "https://pps.whatsapp.net/v/..."
          }
        }
      },
      "ConversationThread": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string",
            "example": "+212612345678"
          },
          "contact_name": {
            "type": "string",
            "example": "Yassine Alami"
          },
          "last_message": {
            "type": "string",
            "example": "Bonjour, je souhaite des informations sur votre offre."
          },
          "last_message_at": {
            "type": "string",
            "format": "date-time"
          },
          "unread_count": {
            "type": "integer",
            "example": 1
          },
          "bot_muted": {
            "type": "boolean",
            "example": false
          },
          "can_reply_free": {
            "type": "boolean",
            "example": true
          },
          "window_expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "QualifiedLead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "lead_…"
          },
          "phone_number_id": {
            "type": "string",
            "example": "1433348623189601"
          },
          "contact_phone": {
            "type": "string",
            "example": "+212612345678"
          },
          "contact_name": {
            "type": "string",
            "example": "Karim Bennani"
          },
          "preset": {
            "type": "string",
            "example": "real_estate"
          },
          "category": {
            "type": "string",
            "example": "Villa"
          },
          "bant_score": {
            "type": "integer",
            "example": 85,
            "description": "Qualification score, 0–100"
          },
          "urgency": {
            "type": "string",
            "example": "immediate"
          },
          "budget": {
            "type": "string",
            "example": "3.5M - 4M MAD"
          },
          "authority": {
            "type": "string",
            "example": "decision_maker"
          },
          "notes": {
            "type": "string"
          },
          "source_type": {
            "type": "string",
            "example": "whatsapp_bot"
          },
          "status": {
            "type": "string",
            "example": "new",
            "enum": [
              "new",
              "contacted",
              "qualified",
              "Fournisseur",
              "won",
              "lost"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InboundWebhookCatch": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "inwh_…"
          },
          "name": {
            "type": "string",
            "example": "Website form"
          },
          "channel": {
            "type": "string",
            "example": "whatsapp"
          },
          "template_id": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "integer",
            "example": 1
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "description": "POST captured leads to https://api.envoisms.ma/v1/inbound-webhooks/catch/{id} — no API key, 64 KB max, 120 requests/minute per webhook."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is malformed; `error.message` names the field",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "MISSING_FIELD",
                "message": "to is required",
                "docs": "https://envoisms.ma/en/docs#errors"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, unknown or revoked API key (or the caller IP is not on the key's allow-list)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "UNAUTHORIZED",
                "message": "Invalid API key",
                "docs": "https://envoisms.ma/en/docs#errors"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key lacks the permission this operation needs",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "FORBIDDEN",
                "message": "API key does not include the send permission",
                "docs": "https://envoisms.ma/en/docs#errors"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource on this account (or for this key mode: sandbox keys do not see live objects)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "NOT_FOUND",
                "message": "Not found",
                "docs": "https://envoisms.ma/en/docs#errors"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-key rate limit exceeded; wait `Retry-After` seconds",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait"
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer"
            },
            "description": "Unix time (seconds) when the window resets"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "RATE_LIMITED",
                "message": "Rate limit exceeded. Retry after 60 seconds.",
                "docs": "https://envoisms.ma/en/docs#errors"
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/messages": {
      "post": {
        "summary": "Send a message",
        "x-payment-info": {
          "intent": "session",
          "method": "stripe",
          "amount": "5.00",
          "currency": "usd",
          "description": "EnvoiSMS API prepaid session deposit",
          "offers": [
            {
              "intent": "session",
              "method": "stripe",
              "amount": "5.00",
              "currency": "usd"
            },
            {
              "intent": "session",
              "method": "tempo",
              "amount": "5000000",
              "currency": "usd"
            }
          ]
        },
        "description": "Send one SMS, or one WhatsApp message from your own connected WhatsApp Business number (channel whatsapp: templates, text, media by id or link, interactive list/button/flow/cta_url/location_request_message, reactions, contact cards, locations, stickers, and a reply context on any of them). WhatsApp rule: a message that is not a template may only be sent to a contact who wrote to your number in the last 24 hours; otherwise it is refused with 400 OUT_OF_24H_WINDOW before anything is charged. Reactions are not billed. To show a typing indicator while you prepare a reply, see POST /messages/typing. Without a connected number, WhatsApp OTPs are available via /verify/send with channel whatsapp. Sending one-time codes? /verify/send manages the full OTP lifecycle (generation, delivery, validation, expiry) and is the recommended path; if you generate codes yourself and send them here, tag the request with metadata purpose: \"otp\" to enable the automatic one-shot re-send over an alternate SMS route on delivery failure (see the sms_otp example).\n\n**Permission:** `send`.",
        "operationId": "sendMessage",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique string (max 255 chars, e.g. a UUID) making this send safe to retry: the same key replays the original response instead of sending and billing again. See the Idempotency section above.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMessageRequest"
              },
              "examples": {
                "sms": {
                  "summary": "Simple SMS",
                  "value": {
                    "to": "+212612345678",
                    "message": "Votre commande #1234 est expédiée!",
                    "from": "MaMarque"
                  }
                },
                "sms_otp": {
                  "summary": "SMS OTP (self-managed code)",
                  "description": "The purpose: \"otp\" tag is what makes this an OTP for the platform: on a hard delivery failure reported for a Moroccan number, the code is re-sent once over an alternate SMS route while still fresh (within 10 minutes), same message id, no extra charge. Without the tag it is treated as an ordinary SMS. Prefer /verify/send when you want code generation, expiry, attempts and validation handled for you.",
                  "value": {
                    "to": "+212612345678",
                    "message": "Votre code: 123456",
                    "channel": "sms",
                    "metadata": {
                      "purpose": "otp"
                    }
                  }
                },
                "cascade": {
                  "summary": "Conservative cascade using configured channels",
                  "value": {
                    "to": "+212612345678",
                    "message": "Votre code: 123456",
                    "cascade": true
                  }
                },
                "whatsapp_template": {
                  "summary": "WhatsApp template (opens a conversation at any time)",
                  "value": {
                    "to": "+212612345678",
                    "channel": "whatsapp",
                    "template": {
                      "name": "order_update",
                      "language": "fr"
                    },
                    "metadata": {
                      "variables": [
                        "Amal",
                        "#1234"
                      ]
                    }
                  }
                },
                "whatsapp_reply": {
                  "summary": "WhatsApp text reply quoting the customer's message (inside the 24-hour window)",
                  "value": {
                    "to": "+212612345678",
                    "channel": "whatsapp",
                    "message": "Votre colis part aujourd'hui.",
                    "context": {
                      "message_id": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg"
                    }
                  }
                },
                "whatsapp_reaction": {
                  "summary": "WhatsApp reaction (not billed)",
                  "value": {
                    "to": "+212612345678",
                    "channel": "whatsapp",
                    "reaction": {
                      "message_id": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
                      "emoji": "👍"
                    }
                  }
                },
                "whatsapp_location": {
                  "summary": "WhatsApp location pin",
                  "value": {
                    "to": "+212612345678",
                    "channel": "whatsapp",
                    "location": {
                      "latitude": 33.5731,
                      "longitude": -7.5898,
                      "name": "Agence Casablanca"
                    }
                  }
                },
                "whatsapp_contacts": {
                  "summary": "WhatsApp contact card",
                  "value": {
                    "to": "+212612345678",
                    "channel": "whatsapp",
                    "contacts": [
                      {
                        "name": {
                          "formatted_name": "Service client"
                        },
                        "phones": [
                          {
                            "phone": "+212522000000",
                            "type": "WORK"
                          }
                        ]
                      }
                    ]
                  }
                },
                "whatsapp_cta_url": {
                  "summary": "WhatsApp call-to-action URL button",
                  "value": {
                    "to": "+212612345678",
                    "channel": "whatsapp",
                    "interactive": {
                      "type": "cta_url",
                      "body": {
                        "text": "Suivez votre commande en direct."
                      },
                      "action": {
                        "name": "cta_url",
                        "parameters": {
                          "display_text": "Suivre",
                          "url": "https://example.com/track/1234"
                        }
                      }
                    }
                  }
                },
                "whatsapp_video_by_id": {
                  "summary": "WhatsApp video by uploaded media id",
                  "value": {
                    "to": "+212612345678",
                    "channel": "whatsapp",
                    "video": {
                      "id": "1234567890",
                      "caption": "Démonstration"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Message queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "description": "Bad request — including OUT_OF_24H_WINDOW (WhatsApp free-form message outside the customer service window; the error also carries window_expired: true and window_expires_at) and the WhatsApp payload codes listed in Error Format. Nothing is charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient balance"
          },
          "403": {
            "description": "WHATSAPP_NOT_CONNECTED (no connected WhatsApp Business number), FORBIDDEN_ASSET, OPTED_OUT, or a Sender ID that is not usable yet"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        },
        "tags": [
          "SMS & WhatsApp Messaging"
        ],
        "x-permission": "send"
      },
      "get": {
        "summary": "List messages",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "sent",
                "delivered",
                "failed",
                "undeliverable"
              ]
            }
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "from_date",
            "in": "query",
            "description": "Start date (ISO 8601)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to_date",
            "in": "query",
            "description": "End date (ISO 8601)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Set to `csv` to download the full filtered set (not a page) as a UTF-8 CSV file, capped at 10000 rows. When the cap applies, the file's last row says so.",
            "schema": {
              "type": "string",
              "enum": [
                "csv"
              ]
            }
          },
          {
            "name": "campaign_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only messages of this campaign"
          }
        ],
        "responses": {
          "200": {
            "description": "List of messages"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "SMS & WhatsApp Messaging"
        ],
        "operationId": "listMessages",
        "description": "List messages\n\n**Permission:** `status`.",
        "x-permission": "status"
      }
    },
    "/messages/bulk": {
      "post": {
        "summary": "Send bulk messages",
        "description": "Send up to 10,000 messages in one request. channel whatsapp sends free-form text from your connected WhatsApp Business number (403 WHATSAPP_NOT_CONNECTED otherwise); each recipient must have written to your number in the last 24 hours. Entries the platform refuses — outside that window (OUT_OF_24H_WINDOW), opted out, … — are listed under rejected with their error code and are never charged.\n\n**Permission:** `send`.",
        "operationId": "sendBulk",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique string (max 255 chars) making this batch safe to retry without double-sending. See the Idempotency section above.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkSendRequest"
              },
              "example": {
                "messages": [
                  {
                    "to": "+212611111111",
                    "message": "Your appointment is tomorrow at 10:00."
                  },
                  {
                    "to": "+212622222222",
                    "message": "Your appointment is tomorrow at 11:30."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Batch queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "batch_id": {
                      "type": "string"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Messages queued"
                    },
                    "channel": {
                      "type": "string"
                    },
                    "estimated_cost": {
                      "type": "object",
                      "properties": {
                        "eur": {
                          "type": "number"
                        },
                        "mad": {
                          "type": "number"
                        }
                      }
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "to": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "example": "queued"
                          }
                        }
                      }
                    },
                    "rejected": {
                      "type": "array",
                      "description": "Present only when some entries were refused. Nothing was charged for them.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "to": {
                            "type": "string"
                          },
                          "code": {
                            "type": "string",
                            "example": "OUT_OF_24H_WINDOW"
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "SMS & WhatsApp Messaging"
        ],
        "x-permission": "send"
      }
    },
    "/messages/{id}": {
      "get": {
        "summary": "Get message status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageRecord"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Message not found"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "SMS & WhatsApp Messaging"
        ],
        "operationId": "getMessage",
        "description": "Get message status\n\n**Permission:** `status`.",
        "x-permission": "status"
      }
    },
    "/messages/typing": {
      "post": {
        "summary": "WhatsApp typing indicator",
        "description": "Marks a WhatsApp message you received as read and shows your contact a typing indicator while you prepare the reply. The indicator disappears when you reply, or after about 25 seconds. Not a message: nothing is stored or charged. Answered synchronously; a refusal by WhatsApp returns its META_* code. Requires the send scope and a connected WhatsApp Business number; sandbox keys simulate.\n\n**Permission:** `send`.",
        "operationId": "sendTypingIndicator",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message_id"
                ],
                "properties": {
                  "message_id": {
                    "type": "string",
                    "description": "WhatsApp message id (wamid) of the message you received",
                    "example": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg"
                  },
                  "typing_indicator": {
                    "type": "boolean",
                    "default": true,
                    "description": "false sends the read receipt alone"
                  },
                  "phone_number_id": {
                    "type": "string",
                    "description": "Which of your connected numbers received the message; defaults to your default number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted by WhatsApp (or simulated on a sandbox key)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "simulated"
                      ]
                    },
                    "message_id": {
                      "type": "string"
                    },
                    "typing_indicator": {
                      "type": "boolean"
                    },
                    "sandbox": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MISSING_FIELD / INVALID_FIELD, or a WhatsApp refusal (META_*)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "WHATSAPP_NOT_CONNECTED or FORBIDDEN_ASSET"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "META_SERVICE_UNAVAILABLE — WhatsApp could not be reached; retry"
          }
        },
        "tags": [
          "SMS & WhatsApp Messaging"
        ],
        "x-permission": "send"
      }
    },
    "/verify/send": {
      "post": {
        "summary": "Send OTP verification code",
        "x-payment-info": {
          "intent": "session",
          "method": "stripe",
          "amount": "5.00",
          "currency": "usd",
          "description": "EnvoiSMS OTP verification prepaid session deposit",
          "offers": [
            {
              "intent": "session",
              "method": "stripe",
              "amount": "5.00",
              "currency": "usd"
            },
            {
              "intent": "session",
              "method": "tempo",
              "amount": "5000000",
              "currency": "usd"
            }
          ]
        },
        "operationId": "sendVerify",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifySendRequest"
              },
              "example": {
                "to": "+212612345678",
                "brand": "MyApp"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "OTP sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "message_id": {
                      "type": "string",
                      "description": "Underlying message id (sms channel only)"
                    },
                    "to": {
                      "type": "string"
                    },
                    "channel": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "status": {
                      "type": "string",
                      "example": "sent"
                    },
                    "cost": {
                      "type": "object",
                      "description": "Managed whatsapp verifications only — the flat per-verification price",
                      "properties": {
                        "eur": {
                          "type": "number"
                        },
                        "mad": {
                          "type": "number"
                        }
                      }
                    },
                    "sandbox": {
                      "type": "boolean",
                      "description": "Present and true on env_test_ keys; nothing was delivered"
                    },
                    "sandbox_code": {
                      "type": "string",
                      "description": "Sandbox keys only — the generated code, returned so the check endpoint can be exercised in tests. Never present on a live key."
                    },
                    "resend_after_seconds": {
                      "type": "integer",
                      "example": 30,
                      "description": "Seconds before POST /verify/resend is accepted"
                    },
                    "fallback_channels": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [],
                      "description": "[\"sms\"] when the code went out on WhatsApp"
                    },
                    "simulated": {
                      "type": "boolean",
                      "description": "Present and true on env_test_ keys"
                    }
                  }
                },
                "example": {
                  "session_id": "vrf_2c7e9a14b0d3",
                  "message_id": "msg_e1a07c93b5f2",
                  "to": "+212612345678",
                  "channel": "sms",
                  "expires_at": "2026-10-22T10:42:00.000Z",
                  "resend_after_seconds": 30,
                  "fallback_channels": [],
                  "status": "sent",
                  "sandbox": true,
                  "simulated": true,
                  "sandbox_code": "483921"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Phone Verification (OTP)"
        ],
        "description": "Send OTP verification code\n\n**Permission:** `verify`.",
        "x-permission": "verify"
      }
    },
    "/verify/resend": {
      "post": {
        "summary": "Resend the code of an existing verification session",
        "description": "Re-delivers the code of a still-pending session, optionally over a\ndifferent channel. Available on every plan, including Starter — it needs\nthe same `verify` scope as /verify/send and nothing more.\n\nThe code itself is unchanged, so an earlier message that arrives late is\nstill valid. Whichever channel you choose, the session's expiry and\nremaining attempts are untouched.\n\n**Cooldown.** A resend is refused with `OTP_COOLDOWN` (429) until the\nwindow opened by the *previous* delivery has passed: 60 s after a\nWhatsApp delivery, 30 s after an SMS one. `/verify/send` reports the\napplicable figure as `resend_after_seconds`, and\n`GET /verify/{session_id}` reports the live countdown as\n`resend_available_in_seconds`. The window is per account and\ndestination — other customers' traffic to the same number does not\naffect yours. `Retry-After` on the 429 carries the exact remaining\nseconds.\n\n**Sandbox keys.** An `env_test_` key simulates: nothing is delivered and\nnothing is charged. The response carries `sandbox: true` alongside the\n`sandbox_code`, so a simulated resend is never mistaken for a delivered\none. A sandbox key can only resend sessions it created, and a live key\nonly live sessions.\n\n**Permission:** `verify`.",
        "operationId": "resendVerify",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id"
                ],
                "properties": {
                  "session_id": {
                    "type": "string",
                    "description": "The session returned by /verify/send. Must still be pending and unexpired."
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "sms",
                      "whatsapp"
                    ],
                    "default": "sms",
                    "description": "Channel for this delivery. Defaults to sms, which is also the useful fallback when a WhatsApp code did not arrive. whatsapp requires the session's code to be 4–8 numeric digits, the only shape the authentication template accepts."
                  }
                }
              },
              "example": {
                "session_id": "vrf_2c7e9a14b0d3",
                "channel": "sms"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code re-sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "message_id": {
                      "type": "string"
                    },
                    "to": {
                      "type": "string"
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "sms",
                        "whatsapp"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "example": "sent"
                    },
                    "resend_after_seconds": {
                      "type": "integer",
                      "description": "Seconds before this session accepts another resend"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "sandbox": {
                      "type": "boolean",
                      "description": "Present and true on env_test_ keys; nothing was delivered and nothing was charged"
                    },
                    "simulated": {
                      "type": "boolean",
                      "description": "Present and true on env_test_ keys"
                    },
                    "sandbox_code": {
                      "type": "string",
                      "description": "Sandbox keys only — never present on a live key"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Session already completed or invalid (INVALID_STATE), expired (SESSION_EXPIRED), or a non-numeric code requested over whatsapp (INVALID_CODE_TYPE)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient balance (INSUFFICIENT_BALANCE)"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Session not found or expired (SESSION_NOT_FOUND). Also returned when a sandbox key targets a live session or vice versa — the message names the cause."
          },
          "429": {
            "description": "Cooldown still open (OTP_COOLDOWN); Retry-After carries the remaining seconds"
          },
          "502": {
            "description": "The channel accepted nothing (DELIVERY_FAILED); nothing was charged. Retry over sms."
          }
        },
        "tags": [
          "Phone Verification (OTP)"
        ],
        "x-permission": "verify"
      }
    },
    "/verify/check": {
      "post": {
        "summary": "Check OTP code",
        "operationId": "checkVerify",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id",
                  "code"
                ],
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "example": "123456"
                  }
                }
              },
              "example": {
                "session_id": "vrf_2c7e9a14b0d3",
                "code": "483921"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code verified",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "verified": {
                      "type": "boolean"
                    },
                    "verified_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Wrong code (error code INVALID_CODE; the message states how many attempts remain)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown or expired session (SESSION_NOT_FOUND): start a new verification with POST /verify/send"
          },
          "429": {
            "description": "Maximum verification attempts exceeded (MAX_ATTEMPTS); the session is closed"
          }
        },
        "tags": [
          "Phone Verification (OTP)"
        ],
        "description": "Checks the code the user typed. On success the session is closed; a wrong code returns 400 INVALID_CODE with the attempts left.\n\n**Permission:** `status`.",
        "x-permission": "status"
      }
    },
    "/verify/lookup": {
      "post": {
        "summary": "Validate phone number (EnvoiSMS Lookup Engine)",
        "description": "Validates phone number format, carrier, and location using the operator database.\n\n**Permission:** `status`.",
        "operationId": "verifyLookupNumber",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "number"
                ],
                "properties": {
                  "number": {
                    "type": "string",
                    "description": "Phone number to validate (E.164 or local format)",
                    "example": "+212612345678"
                  },
                  "country_code": {
                    "type": "string",
                    "description": "Optional ISO 2-letter country code",
                    "example": "MA"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Phone validation information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "number": {
                      "type": "string"
                    },
                    "local_format": {
                      "type": "string"
                    },
                    "international_format": {
                      "type": "string"
                    },
                    "country_prefix": {
                      "type": "string"
                    },
                    "country_code": {
                      "type": "string"
                    },
                    "country_name": {
                      "type": "string"
                    },
                    "location": {
                      "type": "string"
                    },
                    "carrier": {
                      "type": "string"
                    },
                    "line_type": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid parameters"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Phone Verification (OTP)"
        ],
        "x-permission": "status"
      }
    },
    "/verify/{session_id}": {
      "get": {
        "summary": "Get OTP verification session",
        "operationId": "getVerifySession",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OTP session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "to": {
                      "type": "string"
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "sms",
                        "whatsapp",
                        "telegram",
                        "voice",
                        "rcs"
                      ]
                    },
                    "app_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "verified",
                        "failed",
                        "expired"
                      ]
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "verified_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "attempts_used": {
                      "type": "integer"
                    },
                    "attempts_remaining": {
                      "type": "integer"
                    },
                    "resend_available_in_seconds": {
                      "type": "integer",
                      "description": "0 when a resend is accepted right now"
                    },
                    "can_resend_now": {
                      "type": "boolean"
                    },
                    "deliveries": {
                      "type": "array",
                      "description": "One entry per delivery attempt, oldest first, with the outcome the network reported. This is the authoritative answer to \"did my user receive the code?\" — a delivered entry means the handset acknowledged it.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "channel": {
                            "type": "string",
                            "enum": [
                              "sms",
                              "whatsapp"
                            ]
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "send",
                              "resend"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "submitted",
                              "sent",
                              "delivered",
                              "read",
                              "failed",
                              "simulated"
                            ],
                            "description": "simulated means a sandbox key: nothing left the platform"
                          },
                          "simulated": {
                            "type": "boolean"
                          },
                          "submitted_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "delivered_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "read_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "failed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "error_code": {
                            "type": "string",
                            "nullable": true
                          },
                          "error_message": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Session not found"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Phone Verification (OTP)"
        ],
        "description": "Get OTP verification session\n\n**Permission:** `status`.",
        "x-permission": "status"
      }
    },
    "/lookup/{number}": {
      "get": {
        "summary": "Number lookup",
        "description": "Validate a phone number and resolve its destination and, for Moroccan mobiles, the serving operator — including ported numbers the platform has learned about, which prefix tables alone cannot see. Billed per successful lookup (`lookup` channel of your rate card, 0.05 MAD list price); an invalid number is a 400 and is never billed. Requires the `send` permission scope.\n\n**Permission:** `send`.",
        "operationId": "lookupNumber",
        "parameters": [
          {
            "name": "number",
            "in": "path",
            "required": true,
            "description": "The number to look up, E.164 (URL-encode the +, e.g. %2B212612345678). National Moroccan formats (06…, 07…) are accepted and normalized.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lookup result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "lkp_1a2b3c4d5e6f"
                    },
                    "number": {
                      "type": "string",
                      "example": "+212612345678"
                    },
                    "valid": {
                      "type": "boolean"
                    },
                    "country_iso": {
                      "type": "string",
                      "description": "ISO 3166-1 alpha-2 of the destination, or XX when unknown.",
                      "example": "MA"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "mobile",
                        "unknown"
                      ]
                    },
                    "operator": {
                      "type": "string",
                      "nullable": true,
                      "description": "Serving operator when known (Moroccan mobiles and major foreign prefixes), else null.",
                      "example": "Inwi"
                    },
                    "ported": {
                      "type": "boolean",
                      "description": "true when the platform has evidence the number moved to a different operator than its allocated block."
                    },
                    "cost": {
                      "type": "object",
                      "properties": {
                        "eur": {
                          "type": "number"
                        },
                        "mad": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid number (not billed)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient balance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Number Intelligence & HLR"
        ],
        "x-permission": "send"
      }
    },
    "/hlr/{number}": {
      "get": {
        "summary": "HLR lookup",
        "description": "Ask the mobile network itself about a number: whether the subscriber is reachable right now (`status`), which network serves it today — ported numbers included — whether it is roaming and whether it is a mobile or a fixed line. Where `/lookup` answers from allocation tables and what the platform has learned, this one queries the operator's Home Location Register and costs a network round trip (typically 1–5 s).\n\nBilled per answer on the `hlr` channel of your rate card (0.20 MAD list price), whatever the answer — an `inactive` or `absent` subscriber is an answer. An invalid number is a 400 and is never billed, and neither is a query that did not complete (502/503). `source` says where the answer came from: `network` is a live query of the operator's register; `portability` is the operators' number-portability database, which gives the serving operator, the ported flag and whether the line is in service (`active`), but cannot say whether the handset is switched on right now. Moroccan numbers are answered from the portability database. Requires the `send` permission scope. With a sandbox key the response is simulated from what the platform already knows, nothing is queried or debited, and it carries `sandbox: true`.\n\n**Permission:** `send`.",
        "operationId": "hlrLookup",
        "parameters": [
          {
            "name": "number",
            "in": "path",
            "required": true,
            "description": "The number to query, E.164 (URL-encode the +, e.g. %2B212612345678). National Moroccan formats (06…, 07…) are accepted and normalized.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HLR result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "hlr_1a2b3c4d5e6f"
                    },
                    "number": {
                      "type": "string",
                      "example": "+212612345678"
                    },
                    "valid": {
                      "type": "boolean",
                      "description": "false only when the network reported the number as invalid."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active",
                        "inactive",
                        "absent",
                        "invalid",
                        "unknown"
                      ],
                      "description": "Subscriber state as reported by the network. `active` = reachable; `absent` = known but switched off or out of coverage; `inactive` = not in service; `unknown` = the network gave no usable state."
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "mobile",
                        "fixed_line",
                        "unknown"
                      ]
                    },
                    "country_iso": {
                      "type": "string",
                      "description": "ISO 3166-1 alpha-2 of the serving network's country, or XX when unknown.",
                      "example": "MA"
                    },
                    "operator": {
                      "type": "string",
                      "nullable": true,
                      "description": "Name of the serving (home) network when known.",
                      "example": "Maroc Telecom"
                    },
                    "mcc": {
                      "type": "string",
                      "nullable": true,
                      "description": "Mobile country code of the serving network. Every nullable field is null when the network did not report it — the subscriber's IMSI is never returned.",
                      "example": "604"
                    },
                    "mnc": {
                      "type": "string",
                      "nullable": true,
                      "description": "Mobile network code of the serving network.",
                      "example": "01"
                    },
                    "ported": {
                      "type": "boolean",
                      "description": "true when the number has moved to a network other than the one it was allocated to."
                    },
                    "ported_operator": {
                      "type": "string",
                      "nullable": true,
                      "description": "The network the number moved to, when ported."
                    },
                    "ported_mnc": {
                      "type": "string",
                      "nullable": true,
                      "description": "Mobile network code of the network the number moved to, when ported."
                    },
                    "roaming": {
                      "type": "boolean"
                    },
                    "roaming_country_iso": {
                      "type": "string",
                      "nullable": true,
                      "description": "Country the subscriber is roaming in, when roaming."
                    },
                    "roaming_operator": {
                      "type": "string",
                      "nullable": true,
                      "description": "The visited network's name, when roaming and reported."
                    },
                    "roaming_mnc": {
                      "type": "string",
                      "nullable": true,
                      "description": "The visited network's mobile network code, when roaming and reported."
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "network",
                        "portability"
                      ],
                      "description": "`network`: the operator's register answered live, so `status` reflects reachability. `portability`: the number-portability database answered (Moroccan numbers), so `status: active` means in service and `roaming` is always false."
                    },
                    "cost": {
                      "type": "object",
                      "properties": {
                        "eur": {
                          "type": "number"
                        },
                        "mad": {
                          "type": "number"
                        }
                      }
                    },
                    "sandbox": {
                      "type": "boolean",
                      "description": "Present and true only for a sandbox key — the answer was simulated and nothing was debited."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid number (not billed)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Insufficient balance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The network did not answer (BAD_GATEWAY, not billed)"
          },
          "503": {
            "description": "The query did not complete, or HLR is momentarily unavailable or rate-limited (HLR_UNAVAILABLE, not billed)"
          }
        },
        "tags": [
          "Number Intelligence & HLR"
        ],
        "x-permission": "send"
      }
    },
    "/conversations": {
      "get": {
        "summary": "List WhatsApp conversations",
        "description": "Retrieves all active 2-way conversation threads with 24-hour service window expiry time and bot status.\n\n**Permission:** `send`.",
        "operationId": "listConversations",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100,
              "minimum": 1
            },
            "description": "Page size (default 50, max 100)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "description": "Rows to skip"
          },
          {
            "name": "phone_number_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only threads of this connected WhatsApp number"
          },
          {
            "name": "assigned_to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only threads assigned to this agent"
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Match on phone, name or last message"
          }
        ],
        "responses": {
          "200": {
            "description": "List of conversation threads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConversationThread"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Conversations & Live Chat"
        ],
        "x-permission": "send"
      }
    },
    "/conversations/{phone}": {
      "get": {
        "summary": "Get conversation history",
        "description": "Retrieves full message history exchanged with a specific phone number.\n\n**Permission:** `send`.",
        "operationId": "getConversationHistory",
        "parameters": [
          {
            "name": "phone",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "+212612345678"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Full conversation message history"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Conversations & Live Chat"
        ],
        "x-permission": "send"
      }
    },
    "/conversations/{phone}/messages": {
      "post": {
        "summary": "Send live chat reply",
        "description": "Sends an agent message to a contact and mutes the bot on that conversation (un-mute with POST /conversations/{phone}/toggle-bot). A free-form WhatsApp message (text or media) requires the contact to have written in the last 24 hours — otherwise 400 OUT_OF_24H_WINDOW; a template (template_id or template_name) may be sent at any time. Billed like the same send on POST /messages.\n\n**Permission:** `send`.",
        "operationId": "sendConversationReply",
        "parameters": [
          {
            "name": "phone",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "+212612345678"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "Text of the reply (or the caption of media_url)"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "whatsapp",
                      "sms"
                    ],
                    "default": "whatsapp"
                  },
                  "phone_number_id": {
                    "type": "string",
                    "description": "Which connected WhatsApp number replies"
                  },
                  "template_name": {
                    "type": "string",
                    "description": "Approved WhatsApp template to send instead of free text"
                  },
                  "template_id": {
                    "type": "string"
                  },
                  "template_language": {
                    "type": "string",
                    "example": "fr"
                  },
                  "variables": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Template placeholder values in order"
                  },
                  "media_url": {
                    "type": "string",
                    "description": "https URL (or data: URI) of an image or PDF to attach"
                  },
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "document"
                    ]
                  },
                  "filename": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reply sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "phone": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string",
                          "example": "agent"
                        },
                        "message": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "example": "sent"
                        },
                        "channel": {
                          "type": "string"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "OUT_OF_24H_WINDOW, TEMPLATE_NOT_APPROVED or MISSING_MESSAGE"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "TEMPLATE_NOT_FOUND"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Conversations & Live Chat"
        ],
        "x-permission": "send"
      }
    },
    "/whatsapp/profile": {
      "get": {
        "summary": "Get WhatsApp Business profile",
        "description": "Retrieves the public verified profile of your WhatsApp Business Account.\n\n**Permission:** any valid API key.",
        "operationId": "getWhatsAppProfile",
        "responses": {
          "200": {
            "description": "WhatsApp profile information",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsAppProfile"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "WhatsApp Business Profile"
        ],
        "x-permission": "any"
      }
    },
    "/qualified-leads": {
      "get": {
        "summary": "List qualified leads",
        "description": "Leads qualified by your WhatsApp qualification bot, newest first, with summary metrics.\n\n**Permission:** any valid API key.",
        "operationId": "listQualifiedLeads",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Match on name, phone or notes"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Pipeline status"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ]
            },
            "description": "`csv` returns a file"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            },
            "description": "Max rows (default 100, max 500); no offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of qualified leads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/QualifiedLead"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "AI Qualified Leads"
        ],
        "x-permission": "any"
      }
    },
    "/inbound-webhooks": {
      "get": {
        "summary": "List Inbound Webhooks",
        "description": "Retrieves all configured inbound lead capture endpoints.\n\n**Permission:** any valid API key.",
        "operationId": "listInboundWebhooks",
        "responses": {
          "200": {
            "description": "List of inbound webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InboundWebhookCatch"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Inbound Webhooks"
        ],
        "x-permission": "any"
      },
      "post": {
        "summary": "Create an Inbound Webhook",
        "description": "Creates a unique Catch URL for receiving advertising leads (TikTok Lead Ads, Meta Lead Ads, Zapier).\n\n**Permission:** any valid API key.",
        "operationId": "createInboundWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "channel",
                  "mapping_config"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "sms",
                      "whatsapp"
                    ]
                  },
                  "mapping_config": {
                    "type": "object",
                    "description": "`to_path`: where the recipient phone is in the incoming JSON (dot path); `variables`: template variable → dot path",
                    "example": {
                      "to_path": "lead.phone",
                      "variables": {
                        "1": "lead.first_name"
                      }
                    }
                  },
                  "template_id": {
                    "type": "string",
                    "description": "Template to send to each caught lead"
                  }
                }
              },
              "example": {
                "name": "Website form",
                "channel": "whatsapp",
                "mapping_config": {
                  "to_path": "phone"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created; POST leads to https://api.envoisms.ma/v1/inbound-webhooks/catch/{id} (no key needed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "inwh_…"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Inbound Webhooks"
        ],
        "x-permission": "any"
      }
    },
    "/contacts": {
      "get": {
        "summary": "List contacts",
        "operationId": "listContacts",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "list_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Contacts & Audience"
        ],
        "description": "List contacts\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      },
      "post": {
        "summary": "Create or update contact",
        "operationId": "upsertContact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "example": "+212612345678"
                  },
                  "name": {
                    "type": "string",
                    "example": "Sara"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "list_id": {
                    "type": "string"
                  },
                  "custom_fields": {
                    "type": "object",
                    "properties": {
                      "custom1": {
                        "type": "string"
                      },
                      "custom2": {
                        "type": "string"
                      },
                      "custom3": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Contacts & Audience"
        ],
        "description": "Create or update contact\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      }
    },
    "/contacts/import": {
      "post": {
        "summary": "Import contacts",
        "operationId": "importContacts",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contacts"
                ],
                "properties": {
                  "list_id": {
                    "type": "string"
                  },
                  "contacts": {
                    "type": "array",
                    "maxItems": 5000,
                    "items": {
                      "type": "object",
                      "required": [
                        "phone"
                      ],
                      "properties": {
                        "phone": {
                          "type": "string",
                          "example": "+212612345678"
                        },
                        "name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Import accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "imported": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Contacts & Audience"
        ],
        "description": "Import contacts\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      }
    },
    "/contacts/lists": {
      "get": {
        "summary": "List contact lists",
        "operationId": "listContactLists",
        "responses": {
          "200": {
            "description": "Contact lists"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Contacts & Audience"
        ],
        "description": "List contact lists\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      },
      "post": {
        "summary": "Create contact list",
        "operationId": "createContactList",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Customers"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact list created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Contacts & Audience"
        ],
        "description": "Create contact list\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      }
    },
    "/contacts/{id}": {
      "delete": {
        "summary": "Delete contact",
        "operationId": "deleteContact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Contacts & Audience"
        ],
        "description": "Delete contact\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      }
    },
    "/campaigns": {
      "get": {
        "summary": "List campaigns",
        "operationId": "listCampaigns",
        "responses": {
          "200": {
            "description": "Campaign list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            },
            "description": "Page size (default 50, max 200)"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "description": "Rows to skip"
          }
        ],
        "description": "List campaigns\n\n**Permission:** `campaigns`.",
        "x-permission": "campaigns"
      },
      "post": {
        "summary": "Create campaign",
        "operationId": "createCampaign",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "sms",
                      "whatsapp",
                      "telegram",
                      "voice",
                      "rcs"
                    ],
                    "default": "sms"
                  },
                  "list_id": {
                    "type": "string"
                  },
                  "template_id": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "sender_id": {
                    "type": "string",
                    "description": "Sender for every message of the campaign. Same rules and rates as POST /v1/messages: omitted, the shared sender EnvoiSMS at 0.792 MAD; your own name with no registration at 1.70 MAD until validated with your RC.",
                    "example": "ENVOISMS"
                  },
                  "scheduled_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "buttons": {
                    "type": "array",
                    "description": "Optional WhatsApp interactive buttons (max 3). Only supported on the whatsapp channel.",
                    "items": {
                      "$ref": "#/components/schemas/MessageButton"
                    }
                  },
                  "template_variables": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "WhatsApp template campaigns: one entry per template parameter. A body/header key (\"1\", \"2\" or a named variable) maps to a contact field (name, phone, email, custom1–custom3) or to a literal written \"static:<text>\"; \"_header_media_url\" is the https link of a media header; \"_button_<index>\" is the dynamic suffix of URL button <index>. A missing one returns 400 MISSING_TEMPLATE_PARAMS listing what to send.",
                    "example": {
                      "1": "name",
                      "2": "static:-20%",
                      "_header_media_url": "https://example.ma/promo.jpg",
                      "_button_0": "summer-sale"
                    }
                  },
                  "consent_attested": {
                    "type": "boolean",
                    "description": "Required true for a WhatsApp MARKETING template: you confirm every contact on the list agreed to receive your WhatsApp messages. Otherwise 400 CONSENT_ATTESTATION_REQUIRED."
                  }
                },
                "description": "Either `body` (SMS) or `template_id` (WhatsApp) is required."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campaign created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "description": "Create campaign\n\n**Permission:** `campaigns`.",
        "x-permission": "campaigns"
      }
    },
    "/campaigns/{id}": {
      "get": {
        "summary": "Get campaign",
        "operationId": "getCampaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign details"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign not found"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "description": "Get campaign\n\n**Permission:** `campaigns`.",
        "x-permission": "campaigns"
      },
      "patch": {
        "summary": "Update a campaign",
        "description": "Only a draft or scheduled campaign can be changed (409 CAMPAIGN_NOT_EDITABLE otherwise).\n\n**Permission:** `campaigns`.",
        "operationId": "updateCampaign",
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign id",
            "example": "cmp_…"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "sender_id": {
                    "type": "string"
                  },
                  "scheduled_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              },
              "example": {
                "scheduled_at": "2026-11-01T09:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated campaign",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "cmp_…",
                  "status": "scheduled"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Not editable in its current state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "CAMPAIGN_NOT_EDITABLE",
                    "message": "Only draft or scheduled campaigns can be updated",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "campaigns"
      },
      "delete": {
        "summary": "Cancel a campaign",
        "description": "Marks the campaign cancelled. Pause a running campaign first (409 CAMPAIGN_RUNNING).\n\n**Permission:** `campaigns`.",
        "operationId": "deleteCampaign",
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign id",
            "example": "cmp_…"
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "deleted": true,
                  "id": "cmp_…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "CAMPAIGN_RUNNING",
                    "message": "Cannot delete a running campaign. Pause it first.",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "campaigns"
      }
    },
    "/campaigns/{id}/send": {
      "post": {
        "summary": "Send campaign",
        "operationId": "sendCampaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Campaign queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "queued": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "description": "Send campaign\n\n**Permission:** `campaigns`.",
        "x-permission": "campaigns"
      }
    },
    "/optouts": {
      "get": {
        "summary": "List opt-outs",
        "operationId": "listOptouts",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Opt-out registry"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Compliance & Consent"
        ],
        "description": "List opt-outs\n\n**Permission:** `optouts`.",
        "x-permission": "optouts"
      },
      "post": {
        "summary": "Create opt-out",
        "operationId": "createOptout",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "example": "+212612345678"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Recipient opted out"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Compliance & Consent"
        ],
        "description": "Create opt-out\n\n**Permission:** `optouts`.",
        "x-permission": "optouts"
      }
    },
    "/optouts/{phone}": {
      "delete": {
        "summary": "Remove opt-out",
        "operationId": "deleteOptout",
        "parameters": [
          {
            "name": "phone",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recipient opted back in"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Compliance & Consent"
        ],
        "description": "Remove opt-out\n\n**Permission:** `optouts`.",
        "x-permission": "optouts"
      }
    },
    "/consents": {
      "get": {
        "summary": "List consent events",
        "description": "Append-only consent ledger: the evidence behind an opt-in or an opt-out\n(Law 09-08 art. 10, CNDP). Every grant and every withdrawal is one event\nwith its source and evidence; the current state of a number is derived\nfrom the latest event per purpose (see `GET /consents/{phone}`).\n\nThe platform writes events itself when it observes the person's own act:\nSTOP / START keywords on WhatsApp (`whatsapp_keyword`), STOP on SMS\n(`sms_keyword`), and the first customer-initiated WhatsApp conversation\n(`whatsapp_inbound`, purpose `service`). Everything the platform cannot\nsee — a web form, a signed import — is yours to record via `POST`.\n\nAdd `format=csv` to download every matching event as a file (capped at\n10,000 rows; when the cap bites the last row of the file says so).\n\n**Permission:** `contacts`.",
        "operationId": "listConsents",
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "description": "E.164 number to filter on",
            "schema": {
              "type": "string",
              "example": "+212612345678"
            }
          },
          {
            "name": "purpose",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "marketing",
                "transactional",
                "otp",
                "service"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "granted",
                "withdrawn"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only events recorded at or after this ISO 8601 timestamp",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "`csv` returns the full filtered set as a file",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Consent events, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConsentEvent"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Compliance & Consent"
        ],
        "x-permission": "contacts"
      },
      "post": {
        "summary": "Record a consent event",
        "description": "Records that a person granted or withdrew consent, with the evidence you\nhold (the wording shown, the page URL, the IP, a reference). `recorded_at`\nmay be backdated for an imported consent; it cannot be in the future.\nA `marketing` withdrawal is also placed on the opt-out list immediately.\n\n`source` is limited to `api`, `form`, `import` and `dashboard`: the\nkeyword and inbound sources are the platform's own observations and are\nreserved.\n\n**Permission:** `contacts`.",
        "operationId": "createConsent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone",
                  "purpose"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "example": "+212612345678"
                  },
                  "purpose": {
                    "type": "string",
                    "enum": [
                      "marketing",
                      "transactional",
                      "otp",
                      "service"
                    ]
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "granted",
                      "withdrawn"
                    ],
                    "default": "granted"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "whatsapp",
                      "sms",
                      "any"
                    ],
                    "default": "any"
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "api",
                      "form",
                      "import",
                      "dashboard"
                    ],
                    "default": "api"
                  },
                  "evidence": {
                    "type": "object",
                    "description": "Free-form JSON object under 4 KB — the wording shown, url, ip, reference",
                    "example": {
                      "text": "J'accepte de recevoir les offres par WhatsApp",
                      "url": "https://example.ma/inscription",
                      "ip": "41.140.0.1"
                    }
                  },
                  "recorded_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the person acted (defaults to now; may be backdated, never in the future)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Event recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConsentEvent"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_PHONE, INVALID_PURPOSE, INVALID_STATUS, INVALID_CHANNEL, INVALID_SOURCE, INVALID_EVIDENCE or INVALID_DATE"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Compliance & Consent"
        ],
        "x-permission": "contacts"
      }
    },
    "/consents/{phone}": {
      "get": {
        "summary": "Consent state and history for a number",
        "description": "The answer to \"show me this person's consent\": the current state per\npurpose (derived from the newest event — `unknown` when no event exists,\nnever assumed granted), whether the number sits on the opt-out list, and\nthe full event history, newest first.\n\n**Permission:** `contacts`.",
        "operationId": "getConsentForPhone",
        "parameters": [
          {
            "name": "phone",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "+212612345678"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Derived state and history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "phone": {
                      "type": "string"
                    },
                    "opted_out": {
                      "type": "boolean"
                    },
                    "opted_out_at": {
                      "type": "string",
                      "nullable": true
                    },
                    "state": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "status": {
                            "type": "string",
                            "enum": [
                              "granted",
                              "withdrawn",
                              "unknown"
                            ]
                          },
                          "channel": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string"
                          },
                          "recorded_at": {
                            "type": "string"
                          },
                          "record_id": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "history": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConsentEvent"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Compliance & Consent"
        ],
        "x-permission": "contacts"
      }
    },
    "/webhooks": {
      "get": {
        "summary": "List webhooks",
        "operationId": "listWebhooks",
        "responses": {
          "200": {
            "description": "Webhook list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "description": "List webhooks\n\n**Permission:** `webhooks`.",
        "x-permission": "webhooks"
      },
      "post": {
        "summary": "Create webhook",
        "operationId": "createWebhook",
        "description": "Message events POST one payload shape regardless of where in the pipeline the state change happened: `{ \"id\", \"to\", \"channel\", \"status\", \"error_code\", \"error_message\" }` — the two error fields are present only on failed/undeliverable events and carry the same values as the stored message (see the Message schema). `id` is the EnvoiSMS message id, the same one the send call returned. `metadata` supplied at send time is included on delivery-report events.\n\n**Permission:** `webhooks`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://yourapp.com/smsma-webhook"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "message.sent",
                        "message.delivered",
                        "message.failed",
                        "message.undeliverable",
                        "message.fallback",
                        "message.test",
                        "message.*"
                      ]
                    },
                    "default": [
                      "message.delivered",
                      "message.failed"
                    ]
                  },
                  "secret": {
                    "type": "string",
                    "description": "Optional HMAC secret. Generated and returned when omitted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "x-permission": "webhooks"
      }
    },
    "/webhooks/{id}": {
      "patch": {
        "summary": "Update webhook",
        "operationId": "updateWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "description": "Update webhook\n\n**Permission:** `webhooks`.",
        "x-permission": "webhooks"
      },
      "delete": {
        "summary": "Disable webhook",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook disabled"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "description": "Disable webhook\n\n**Permission:** `webhooks`.",
        "x-permission": "webhooks"
      }
    },
    "/webhooks/{id}/rotate-secret": {
      "post": {
        "summary": "Rotate webhook signing secret",
        "description": "Generates a new signing secret for the endpoint and returns it exactly once. Deliveries already queued were signed with the old secret, so accept both during a short rollover.\n\n**Permission:** `webhooks`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New secret (returned only here)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Webhook not found"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "operationId": "rotateWebhookSecret",
        "x-permission": "webhooks"
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "summary": "Test webhook",
        "operationId": "testWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Test webhook queued"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "description": "Test webhook\n\n**Permission:** `webhooks`.",
        "x-permission": "webhooks"
      }
    },
    "/webhooks/logs": {
      "get": {
        "summary": "List webhook delivery attempts",
        "description": "The last 100 delivery attempts on your account, newest first — one row per attempt, including retries. `payload` is the exact body that was sent. Deliveries are attempted up to `max_attempts` times (1 minute, 10 minutes, then 1 hour apart).\n\n**Permission:** `webhooks`.",
        "operationId": "listWebhookLogs",
        "parameters": [
          {
            "name": "webhook_id",
            "in": "query",
            "required": false,
            "description": "Only attempts for this endpoint",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery attempts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "max_attempts": {
                      "type": "integer",
                      "example": 4
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "webhook_id": {
                            "type": "string"
                          },
                          "event": {
                            "type": "string",
                            "example": "message.delivered"
                          },
                          "url": {
                            "type": "string"
                          },
                          "payload": {
                            "type": "string",
                            "description": "The JSON body that was sent"
                          },
                          "status_code": {
                            "type": "integer",
                            "nullable": true,
                            "description": "HTTP status your endpoint answered; 0 when it could not be reached"
                          },
                          "error_message": {
                            "type": "string",
                            "nullable": true
                          },
                          "attempt": {
                            "type": "integer"
                          },
                          "delivered": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "x-permission": "webhooks"
      }
    },
    "/webhooks/logs/{log_id}/replay": {
      "post": {
        "summary": "Resend a logged webhook delivery",
        "description": "Queues the same event and data again to the endpoint's current URL, as a new first attempt with a fresh signature timestamp (so it passes your replay-window check). The result appears in `/webhooks/logs`.\n\n**Permission:** `webhooks`.",
        "operationId": "replayWebhookDelivery",
        "parameters": [
          {
            "name": "log_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Resend queued"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Log not found, belongs to another account, or its endpoint was deleted"
          },
          "422": {
            "description": "REPLAY_UNAVAILABLE — no stored payload to resend"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Delivery Webhooks (DLR)"
        ],
        "x-permission": "webhooks"
      }
    },
    "/billing/balance": {
      "get": {
        "summary": "Get billing balance",
        "operationId": "getBillingBalance",
        "responses": {
          "200": {
            "description": "Current balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance_eur": {
                      "type": "number",
                      "example": 45.23
                    },
                    "balance_mad": {
                      "type": "number",
                      "example": 497.53
                    },
                    "currency": {
                      "type": "string",
                      "example": "EUR"
                    },
                    "plan": {
                      "type": "string",
                      "example": "business"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "description": "Get billing balance\n\n**Permission:** `balance`.",
        "x-permission": "balance"
      }
    },
    "/balance": {
      "get": {
        "summary": "Get account balance (alias)",
        "operationId": "getBalance",
        "responses": {
          "200": {
            "description": "Current balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance_eur": {
                      "type": "number",
                      "example": 45.23
                    },
                    "balance_mad": {
                      "type": "number",
                      "example": 497.53
                    },
                    "currency": {
                      "type": "string",
                      "example": "EUR"
                    },
                    "plan": {
                      "type": "string",
                      "example": "business"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "description": "Get account balance (alias)\n\n**Permission:** `balance`.",
        "x-permission": "balance"
      }
    },
    "/billing/transactions": {
      "get": {
        "summary": "List billing transactions",
        "operationId": "listBillingTransactions",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Billing transactions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "topup",
                              "usage",
                              "refund",
                              "adjustment",
                              "grant"
                            ],
                            "description": "grant = the welcome credit written once at signup; it is not a payment and cannot be receipted"
                          },
                          "amount_eur": {
                            "type": "number"
                          },
                          "amount_mad": {
                            "type": "number"
                          },
                          "balance_after_eur": {
                            "type": "number"
                          },
                          "description": {
                            "type": "string"
                          },
                          "reference": {
                            "type": "string",
                            "nullable": true
                          },
                          "payment_method": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "description": "List billing transactions\n\n**Permission:** `billing`.",
        "x-permission": "billing"
      }
    },
    "/billing/plans": {
      "get": {
        "summary": "List plan pricing",
        "operationId": "listBillingPlans",
        "responses": {
          "200": {
            "description": "Pricing rows by plan and channel",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "plan": {
                            "type": "string",
                            "example": "starter"
                          },
                          "channel": {
                            "type": "string",
                            "enum": [
                              "sms",
                              "whatsapp",
                              "telegram",
                              "voice",
                              "rcs"
                            ]
                          },
                          "price_eur": {
                            "type": "number",
                            "example": 0.04455
                          },
                          "price_mad": {
                            "type": "number",
                            "example": 0.49
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "description": "List plan pricing\n\n**Permission:** `billing`.",
        "x-permission": "billing"
      }
    },
    "/billing/packs": {
      "get": {
        "summary": "List prepaid SMS packs",
        "description": "Returns the sellable prepaid credit packs. What a pack buys is `amount_mad` of credit; `sms` is an ESTIMATE of how many messages that becomes at `price_per_sms_mad`, for a single-segment Moroccan message on the default sender. A custom sender ID bills at the `sms_custom` rate, and longer messages cost more than one segment, so the real count is usually lower. `sms_estimated` is always true — never treat `sms` as an entitlement. Completing a pack top-up also updates the account to the matching volume tier. `min_topup_mad` is the floor for a custom-amount top-up on POST /billing/topups.\n\n**Permission:** `billing`.",
        "operationId": "listBillingPacks",
        "responses": {
          "200": {
            "description": "SMS pack rows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "sms-1000"
                          },
                          "sms": {
                            "type": "integer",
                            "example": 1000,
                            "description": "Estimated message count, not a guaranteed quantity. See sms_estimated."
                          },
                          "sms_estimated": {
                            "type": "boolean",
                            "example": true,
                            "description": "Always true. The count varies with destination, sender type and segments."
                          },
                          "amount_eur": {
                            "type": "number",
                            "example": 34.55
                          },
                          "amount_mad": {
                            "type": "number",
                            "example": 380
                          },
                          "price_per_sms_mad": {
                            "type": "number",
                            "example": 0.38
                          },
                          "volume_tier": {
                            "type": "string",
                            "example": "business"
                          }
                        }
                      }
                    },
                    "min_topup_mad": {
                      "type": "number",
                      "example": 275,
                      "description": "Minimum accepted amount_mad for a custom top-up."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "x-permission": "billing"
      }
    },
    "/billing/payment-methods": {
      "get": {
        "summary": "List configured payment methods",
        "description": "Returns Stripe, crypto, and bank-transfer availability for the authenticated account. Missing configuration names are returned so operators know what to configure before taking live payments.\n\n**Permission:** `billing`.",
        "operationId": "listBillingPaymentMethods",
        "responses": {
          "200": {
            "description": "Payment method readiness",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "enum": [
                              "stripe",
                              "crypto",
                              "bank_transfer"
                            ]
                          },
                          "label": {
                            "type": "string"
                          },
                          "configured": {
                            "type": "boolean"
                          },
                          "missing": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "settlement": {
                            "type": "string",
                            "enum": [
                              "automatic",
                              "manual"
                            ]
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "x-permission": "billing"
      }
    },
    "/billing/topups": {
      "post": {
        "summary": "Create balance top-up or SMS pack",
        "description": "Creates a custom balance top-up or a prepaid SMS pack. Pack top-ups carry a volume tier, for example `sms-1000` unlocks the `business` SMS price when payment is completed.\n\n**Permission:** `billing`.",
        "operationId": "createBillingTopup",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount_eur": {
                    "type": "number",
                    "minimum": 10,
                    "example": 50,
                    "description": "Required for custom top-ups. Optional when pack_id is provided."
                  },
                  "pack_id": {
                    "type": "string",
                    "example": "sms-1000"
                  },
                  "payment_method": {
                    "type": "string",
                    "enum": [
                      "stripe",
                      "crypto",
                      "bank_transfer"
                    ],
                    "example": "stripe"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pending top-up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "topup_abcd"
                    },
                    "status": {
                      "type": "string",
                      "example": "pending"
                    },
                    "amount_eur": {
                      "type": "number",
                      "example": 50
                    },
                    "amount_mad": {
                      "type": "number",
                      "example": 550
                    },
                    "pack_id": {
                      "type": "string",
                      "nullable": true,
                      "example": "sms-1000"
                    },
                    "sms_count": {
                      "type": "integer",
                      "nullable": true,
                      "example": 1000
                    },
                    "volume_tier": {
                      "type": "string",
                      "nullable": true,
                      "example": "business"
                    },
                    "price_per_sms_mad": {
                      "type": "number",
                      "nullable": true,
                      "example": 0.38
                    },
                    "checkout_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "instructions": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid amount, pack, or payment method"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "Requested payment method is not configured. Configure the missing Stripe or bank-transfer fields before retrying."
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "x-permission": "billing"
      }
    },
    "/billing/topups/claim": {
      "post": {
        "summary": "Claim paid top-ups",
        "description": "Verifies the caller's pending top-ups against the payment provider and credits the ones that are confirmed paid. The dashboard calls this automatically on the checkout success redirect, so the balance reflects a payment within seconds even if webhook delivery lags. Idempotent — a top-up is only ever credited once, and only the caller's own top-ups are examined.\n\n**Permission:** `billing`.",
        "operationId": "claimBillingTopups",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session_id": {
                    "type": "string",
                    "description": "Checkout session id from the success redirect. Narrows the claim to that checkout."
                  },
                  "topup_id": {
                    "type": "string",
                    "description": "Alternatively, a specific top-up id to claim."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claim outcome and refreshed balances",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "claimed": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "topup_abcd"
                          },
                          "status": {
                            "type": "string",
                            "example": "completed"
                          }
                        }
                      }
                    },
                    "completed": {
                      "type": "integer",
                      "example": 1
                    },
                    "balance_eur": {
                      "type": "number",
                      "example": 61.82
                    },
                    "balance_mad": {
                      "type": "number",
                      "example": 680
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "x-permission": "billing"
      }
    },
    "/billing/quota": {
      "get": {
        "summary": "Get billing quota limit",
        "description": "Gets the monthly spending/sending quota limits configured for safety.\n\n**Permission:** `billing`.",
        "operationId": "getQuota",
        "responses": {
          "200": {
            "description": "Quota configuration",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monthly_limit_mad": {
                      "type": "number"
                    },
                    "monthly_usage_mad": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "x-permission": "billing"
      },
      "patch": {
        "summary": "Update billing quota limit",
        "description": "Updates the monthly spending and sending quota limit for the account.\n\n**Permission:** `billing`.",
        "operationId": "updateQuota",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "daily_quota": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 1,
                    "maximum": 1000000,
                    "description": "Messages per day; null removes the cap"
                  },
                  "monthly_quota": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 1,
                    "maximum": 10000000,
                    "description": "Messages per month; null removes the cap"
                  }
                }
              },
              "example": {
                "daily_quota": 5000,
                "monthly_quota": null
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Quota updated successfully"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Billing & Balances"
        ],
        "x-permission": "billing"
      }
    },
    "/analytics": {
      "get": {
        "summary": "Get usage analytics",
        "operationId": "getAnalytics",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 30,
              "maximum": 365
            },
            "description": "Window in days (default 30, max 365)"
          },
          {
            "name": "campaign_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict to one campaign"
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window_days": {
                      "type": "integer"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "delivered": {
                          "type": "integer"
                        },
                        "failed": {
                          "type": "integer"
                        },
                        "delivery_rate": {
                          "type": "number"
                        },
                        "cost_eur": {
                          "type": "number"
                        },
                        "cost_mad": {
                          "type": "number"
                        }
                      }
                    },
                    "channels": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "daily": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Analytics & Reporting"
        ],
        "description": "Get usage analytics\n\n**Permission:** `analytics`.",
        "x-permission": "analytics"
      }
    },
    "/api-keys": {
      "get": {
        "summary": "List API keys",
        "operationId": "listApiKeys",
        "responses": {
          "200": {
            "description": "API keys without full secret values"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "API Keys & Security"
        ],
        "description": "List API keys\n\n**Permission:** `api_keys`.",
        "x-permission": "api_keys"
      },
      "post": {
        "summary": "Create API key",
        "operationId": "createApiKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Production key"
                  },
                  "permissions": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "send",
                        "verify",
                        "status",
                        "balance",
                        "contacts",
                        "campaigns",
                        "webhooks",
                        "billing",
                        "analytics",
                        "api_keys",
                        "optouts"
                      ]
                    }
                  },
                  "ip_whitelist": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "rate_limit": {
                    "type": "integer",
                    "minimum": 10,
                    "maximum": 1000
                  },
                  "sandbox": {
                    "type": "boolean",
                    "default": false,
                    "description": "Mint a sandbox key, prefixed `env_test_` instead of `smr_`. Requests on a sandbox key are validated, priced and recorded exactly like live ones — opt-out list, sender rules, phishing screen and quota all apply — but nothing is delivered to a carrier, no balance is debited and no invoice line is created. A simulated delivery receipt follows a few seconds later, including the `delivered` webhook. Sends and OTPs made with a sandbox key are excluded from analytics and are only visible to sandbox keys. A key's mode is fixed at creation; create a new key to switch.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created. Full key is returned once."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "API Keys & Security"
        ],
        "description": "Create API key\n\n**Permission:** `api_keys`.",
        "x-permission": "api_keys"
      }
    },
    "/api-keys/{id}": {
      "patch": {
        "summary": "Update API key",
        "operationId": "updateApiKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "permissions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "ip_whitelist": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "rate_limit": {
                    "type": "integer"
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "API key updated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "API Keys & Security"
        ],
        "description": "Update API key\n\n**Permission:** `api_keys`.",
        "x-permission": "api_keys"
      },
      "delete": {
        "summary": "Revoke API key",
        "operationId": "revokeApiKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "API key revoked"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "API Keys & Security"
        ],
        "description": "Revoke API key\n\n**Permission:** `api_keys`.",
        "x-permission": "api_keys"
      }
    },
    "/health": {
      "get": {
        "summary": "Health check",
        "description": "Public service health endpoint. This route is served from the API root, not the /v1 prefix.",
        "operationId": "healthCheck",
        "servers": [
          {
            "url": "https://api.envoisms.ma",
            "description": "Production root"
          },
          {
            "url": "https://api-staging.envoisms.ma",
            "description": "Staging root configuration; deploy and verify before using"
          }
        ],
        "responses": {
          "200": {
            "description": "Service health",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "service": {
                      "type": "string",
                      "example": "envoisms-api"
                    },
                    "version": {
                      "type": "string",
                      "example": "1.0.0"
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        },
        "tags": [
          "System & Utilities"
        ],
        "security": []
      }
    },
    "/search": {
      "get": {
        "summary": "Natural language search",
        "description": "Search EnvoiSMS.ma features, pricing, documentation, integrations, and operator details using a natural language query. Helpful for AI search engine grounding.",
        "operationId": "searchFeatures",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Search term or natural language query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "WooCommerce"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "title": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "tags": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        },
        "tags": [
          "System & Utilities"
        ],
        "security": []
      }
    },
    "/recommend": {
      "get": {
        "summary": "Get system recommendations",
        "description": "Recommends the best pricing pack, channel, integration code, and strategy based on your industry, use case, or expected message volume.",
        "operationId": "getRecommendations",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Natural language use case or industry description (e.g. ecommerce shipping, otp)",
            "required": false,
            "schema": {
              "type": "string",
              "example": "ecommerce in Morocco"
            }
          },
          {
            "name": "volume",
            "in": "query",
            "description": "Expected monthly message volume",
            "required": false,
            "schema": {
              "type": "integer",
              "example": 5000
            }
          },
          {
            "name": "industry",
            "in": "query",
            "description": "Business industry",
            "required": false,
            "schema": {
              "type": "string",
              "example": "retail"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "System recommendations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recommendation": {
                      "type": "object",
                      "properties": {
                        "channel": {
                          "type": "string",
                          "enum": [
                            "sms",
                            "whatsapp",
                            "cascade"
                          ]
                        },
                        "pricing_pack": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "sms": {
                              "type": "integer"
                            },
                            "price_mad": {
                              "type": "number"
                            },
                            "price_per_sms_mad": {
                              "type": "number"
                            }
                          }
                        },
                        "reasoning": {
                          "type": "string"
                        },
                        "setup_guidelines": {
                          "type": "object",
                          "properties": {
                            "engine": {
                              "type": "string"
                            },
                            "sample_code": {
                              "type": "string"
                            }
                          }
                        },
                        "whatsapp_chat_url": {
                          "type": "string"
                        },
                        "buy_url": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        },
        "tags": [
          "System & Utilities"
        ],
        "security": []
      }
    },
    "/qualified-leads/{id}": {
      "patch": {
        "summary": "Update a qualified lead",
        "operationId": "updateQualifiedLead",
        "tags": [
          "AI Qualified Leads"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Lead id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "new",
                      "contacted",
                      "qualified",
                      "Fournisseur",
                      "won",
                      "lost"
                    ]
                  },
                  "notes": {
                    "type": "string"
                  },
                  "contact_name": {
                    "type": "string"
                  },
                  "value_minor": {
                    "type": "integer",
                    "description": "Deal value in minor units (centimes)"
                  },
                  "currency": {
                    "type": "string",
                    "example": "MAD"
                  }
                }
              },
              "example": {
                "status": "contacted",
                "notes": "Called back on 22/10"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated lead",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "lead": {
                    "id": "lead_…",
                    "status": "contacted",
                    "notes": "Called back on 22/10"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such lead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "LEAD_NOT_FOUND",
                    "message": "Lead not found",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Update a qualified lead\n\n**Permission:** any valid API key.",
        "x-permission": "any"
      }
    },
    "/inbound-webhooks/{id}": {
      "delete": {
        "summary": "Delete an inbound webhook",
        "operationId": "deleteInboundWebhook",
        "tags": [
          "Inbound Webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Inbound webhook id"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Delete an inbound webhook\n\n**Permission:** any valid API key.",
        "x-permission": "any"
      }
    },
    "/verify/void": {
      "post": {
        "summary": "Cancel a verification",
        "description": "Ends a pending verification so its code can no longer be checked (e.g. the user changed number).\n\n**Permission:** `status`.",
        "operationId": "voidVerify",
        "tags": [
          "Phone Verification (OTP)"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id"
                ],
                "properties": {
                  "session_id": {
                    "type": "string",
                    "example": "vrf_2c7e9a14b0d3"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "session_id": "vrf_2c7e9a14b0d3",
                  "status": "expired",
                  "voided": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SESSION_NOT_FOUND",
                    "message": "Verify session not found",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already verified or cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INVALID_STATE",
                    "message": "Session is already verified",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "status"
      }
    },
    "/campaigns/{id}/pause": {
      "post": {
        "summary": "Pause a campaign",
        "operationId": "pauseCampaign",
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign id",
            "example": "cmp_…"
          }
        ],
        "responses": {
          "200": {
            "description": "Paused",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "cmp_…",
                  "status": "paused"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Not running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "NOT_RUNNING",
                    "message": "Campaign is not running",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Pause a campaign\n\n**Permission:** `campaigns`.",
        "x-permission": "campaigns"
      }
    },
    "/campaigns/{id}/resume": {
      "post": {
        "summary": "Resume a campaign",
        "operationId": "resumeCampaign",
        "tags": [
          "Campaigns & Bulk Dispatch"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign id",
            "example": "cmp_…"
          }
        ],
        "responses": {
          "200": {
            "description": "Resumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "cmp_…",
                  "status": "running"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Not paused",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "NOT_PAUSED",
                    "message": "Campaign is not paused",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Resume a campaign\n\n**Permission:** `campaigns`.",
        "x-permission": "campaigns"
      }
    },
    "/contacts/lists/{id}": {
      "post": {
        "summary": "Rename a contact list",
        "operationId": "updateContactList",
        "tags": [
          "Contacts & Audience"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "List id",
            "example": "lst_…"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "lst_…",
                  "name": "VIP customers"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Rename a contact list\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      },
      "delete": {
        "summary": "Delete a contact list",
        "operationId": "deleteContactList",
        "tags": [
          "Contacts & Audience"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "List id",
            "example": "lst_…"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "deleted": true,
                  "id": "lst_…"
                }
              }
            }
          },
          "400": {
            "description": "Used by an active campaign",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "LIST_IN_USE",
                    "message": "Cannot delete contact list because it is currently in use by active campaigns: Summer sale",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Delete a contact list\n\n**Permission:** `contacts`.",
        "x-permission": "contacts"
      }
    },
    "/templates": {
      "get": {
        "summary": "List templates",
        "operationId": "listTemplates",
        "tags": [
          "Templates"
        ],
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "sms, whatsapp, telegram or any"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Template category"
          }
        ],
        "responses": {
          "200": {
            "description": "Active templates, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "data": [
                    {
                      "id": "tpl_…",
                      "name": "order_shipped",
                      "channel": "whatsapp",
                      "category": "utility",
                      "language": "fr",
                      "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
                      "status": "approved",
                      "created_at": "2026-10-22T10:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "List templates\n\n**Permission:** `send`.",
        "x-permission": "send"
      },
      "post": {
        "summary": "Create a template",
        "description": "A WhatsApp template is created in status `pending_admin`: EnvoiSMS reviews it, then submits it to Meta (status follows Meta: pending, approved, rejected, paused). Track it with `GET /templates/{id}` or `POST /templates/sync`. SMS templates are usable at once.\n\n**Permission:** `send`.",
        "operationId": "createTemplate",
        "tags": [
          "Templates"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "channel"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Lowercase letters, digits and underscores (normalised)",
                    "example": "order_shipped"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "sms",
                      "whatsapp",
                      "telegram",
                      "any"
                    ]
                  },
                  "body": {
                    "type": "string",
                    "description": "Text with numbered ({{1}}, {{2}}) or named ({{first_name}}) variables. Not used by WhatsApp authentication templates.",
                    "example": "Bonjour {{1}}, votre commande {{2}} est expédiée."
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "marketing",
                      "utility",
                      "authentication",
                      "transactional",
                      "otp",
                      "general"
                    ],
                    "description": "WhatsApp bills by category (transactional = utility, otp = authentication)"
                  },
                  "language": {
                    "type": "string",
                    "example": "fr"
                  },
                  "sample_values": {
                    "description": "One example per variable (array in order, or object keyed by variable). Meta refuses a template without them.",
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        }
                      }
                    ],
                    "example": [
                      "Karim",
                      "#1042"
                    ]
                  },
                  "header_type": {
                    "type": "string",
                    "enum": [
                      "none",
                      "text",
                      "image",
                      "document",
                      "video"
                    ]
                  },
                  "header_text": {
                    "type": "string"
                  },
                  "header_sample": {
                    "type": "string",
                    "description": "Example for a variable in header_text"
                  },
                  "header_example_url": {
                    "type": "string",
                    "description": "Public https link to a sample file for a media header"
                  },
                  "footer": {
                    "type": "string"
                  },
                  "buttons": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "object",
                      "required": [
                        "type"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "quick_reply",
                            "url",
                            "phone_number",
                            "copy_code",
                            "flow"
                          ]
                        },
                        "text": {
                          "type": "string",
                          "maxLength": 25
                        },
                        "url": {
                          "type": "string"
                        },
                        "url_example": {
                          "type": "string"
                        },
                        "phone_number": {
                          "type": "string"
                        },
                        "copy_code": {
                          "type": "string",
                          "maxLength": 15
                        },
                        "flow_id": {
                          "type": "string",
                          "description": "FLOW button: id of one of your published WhatsApp Flows"
                        },
                        "navigate_screen": {
                          "type": "string",
                          "description": "FLOW button: first screen (default LEAD)"
                        }
                      }
                    }
                  },
                  "carousel": {
                    "type": "object",
                    "description": "Media-card carousel: 2–10 cards, one media type, each card ≤160 characters with the same 1–2 buttons (quick_reply or url), no variables in cards.",
                    "properties": {
                      "header_type": {
                        "type": "string",
                        "enum": [
                          "image",
                          "video"
                        ]
                      },
                      "cards": {
                        "type": "array",
                        "minItems": 2,
                        "maxItems": 10,
                        "items": {
                          "type": "object",
                          "required": [
                            "header_example_url",
                            "body",
                            "buttons"
                          ],
                          "properties": {
                            "header_example_url": {
                              "type": "string"
                            },
                            "body": {
                              "type": "string",
                              "maxLength": 160
                            },
                            "buttons": {
                              "type": "array",
                              "minItems": 1,
                              "maxItems": 2,
                              "items": {
                                "type": "object"
                              }
                            }
                          }
                        }
                      }
                    }
                  },
                  "waba_id": {
                    "type": "string",
                    "description": "Which of your connected WhatsApp Business Accounts the template is for"
                  }
                }
              },
              "example": {
                "name": "order_shipped",
                "channel": "whatsapp",
                "category": "utility",
                "language": "fr",
                "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
                "sample_values": [
                  "Karim",
                  "#1042"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "tpl_…",
                  "name": "order_shipped",
                  "channel": "whatsapp",
                  "category": "utility",
                  "language": "fr",
                  "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
                  "status": "pending_admin",
                  "created_at": "2026-10-22T10:00:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Same name and language already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "TEMPLATE_EXISTS",
                    "message": "A WhatsApp template named order_shipped already exists in language fr.",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "send"
      }
    },
    "/templates/{id}": {
      "get": {
        "summary": "Get a template",
        "operationId": "getTemplate",
        "tags": [
          "Templates"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Template id",
            "example": "tpl_…"
          }
        ],
        "responses": {
          "200": {
            "description": "The template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "tpl_…",
                  "name": "order_shipped",
                  "channel": "whatsapp",
                  "category": "utility",
                  "language": "fr",
                  "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
                  "status": "approved",
                  "created_at": "2026-10-22T10:00:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Get a template\n\n**Permission:** `send`.",
        "x-permission": "send"
      },
      "patch": {
        "summary": "Edit a template",
        "description": "Meta freezes the name, the language and (once approved) the category; edits are accepted only when Meta allows them (409 TEMPLATE_EDIT_NOT_ALLOWED otherwise).\n\n**Permission:** `send`.",
        "operationId": "updateTemplate",
        "tags": [
          "Templates"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Template id",
            "example": "tpl_…"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Lowercase letters, digits and underscores (normalised)",
                    "example": "order_shipped"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "sms",
                      "whatsapp",
                      "telegram",
                      "any"
                    ]
                  },
                  "body": {
                    "type": "string",
                    "description": "Text with numbered ({{1}}, {{2}}) or named ({{first_name}}) variables. Not used by WhatsApp authentication templates.",
                    "example": "Bonjour {{1}}, votre commande {{2}} est expédiée."
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "marketing",
                      "utility",
                      "authentication",
                      "transactional",
                      "otp",
                      "general"
                    ],
                    "description": "WhatsApp bills by category (transactional = utility, otp = authentication)"
                  },
                  "language": {
                    "type": "string",
                    "example": "fr"
                  },
                  "sample_values": {
                    "description": "One example per variable (array in order, or object keyed by variable). Meta refuses a template without them.",
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        }
                      }
                    ],
                    "example": [
                      "Karim",
                      "#1042"
                    ]
                  },
                  "header_type": {
                    "type": "string",
                    "enum": [
                      "none",
                      "text",
                      "image",
                      "document",
                      "video"
                    ]
                  },
                  "header_text": {
                    "type": "string"
                  },
                  "header_sample": {
                    "type": "string",
                    "description": "Example for a variable in header_text"
                  },
                  "header_example_url": {
                    "type": "string",
                    "description": "Public https link to a sample file for a media header"
                  },
                  "footer": {
                    "type": "string"
                  },
                  "buttons": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "object",
                      "required": [
                        "type"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "quick_reply",
                            "url",
                            "phone_number",
                            "copy_code",
                            "flow"
                          ]
                        },
                        "text": {
                          "type": "string",
                          "maxLength": 25
                        },
                        "url": {
                          "type": "string"
                        },
                        "url_example": {
                          "type": "string"
                        },
                        "phone_number": {
                          "type": "string"
                        },
                        "copy_code": {
                          "type": "string",
                          "maxLength": 15
                        },
                        "flow_id": {
                          "type": "string",
                          "description": "FLOW button: id of one of your published WhatsApp Flows"
                        },
                        "navigate_screen": {
                          "type": "string",
                          "description": "FLOW button: first screen (default LEAD)"
                        }
                      }
                    }
                  },
                  "carousel": {
                    "type": "object",
                    "description": "Media-card carousel: 2–10 cards, one media type, each card ≤160 characters with the same 1–2 buttons (quick_reply or url), no variables in cards.",
                    "properties": {
                      "header_type": {
                        "type": "string",
                        "enum": [
                          "image",
                          "video"
                        ]
                      },
                      "cards": {
                        "type": "array",
                        "minItems": 2,
                        "maxItems": 10,
                        "items": {
                          "type": "object",
                          "required": [
                            "header_example_url",
                            "body",
                            "buttons"
                          ],
                          "properties": {
                            "header_example_url": {
                              "type": "string"
                            },
                            "body": {
                              "type": "string",
                              "maxLength": 160
                            },
                            "buttons": {
                              "type": "array",
                              "minItems": 1,
                              "maxItems": 2,
                              "items": {
                                "type": "object"
                              }
                            }
                          }
                        }
                      }
                    }
                  },
                  "waba_id": {
                    "type": "string",
                    "description": "Which of your connected WhatsApp Business Accounts the template is for"
                  }
                }
              },
              "example": {
                "body": "Bonjour {{1}}, votre commande {{2}} est en route."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "tpl_…",
                  "name": "order_shipped",
                  "channel": "whatsapp",
                  "category": "utility",
                  "language": "fr",
                  "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
                  "status": "pending_admin",
                  "created_at": "2026-10-22T10:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Frozen field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "TEMPLATE_IMMUTABLE_FIELD",
                    "message": "Meta does not allow renaming a template or changing its language; create a new template instead.",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Meta does not accept edits now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "TEMPLATE_EDIT_NOT_ALLOWED",
                    "message": "Meta does not accept edits while a template is pending.",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "send"
      },
      "delete": {
        "summary": "Delete a template",
        "description": "Also deletes it at Meta for a WhatsApp template.\n\n**Permission:** `send`.",
        "operationId": "deleteTemplate",
        "tags": [
          "Templates"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Template id",
            "example": "tpl_…"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "deleted": true,
                  "id": "tpl_…",
                  "meta_deleted": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "send"
      }
    },
    "/templates/{id}/submit-meta": {
      "post": {
        "summary": "Submit a WhatsApp template to Meta",
        "description": "Submits a reviewed template. A template still in EnvoiSMS review returns 409 TEMPLATE_PENDING_REVIEW; one rejected by that review returns 409 TEMPLATE_REJECTED_BY_REVIEW (edit it to resubmit).\n\n**Permission:** `send`.",
        "operationId": "submitTemplateToMeta",
        "tags": [
          "Templates"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Template id",
            "example": "tpl_…"
          }
        ],
        "responses": {
          "200": {
            "description": "Submitted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "template": {
                    "id": "tpl_…",
                    "name": "order_shipped",
                    "channel": "whatsapp",
                    "category": "utility",
                    "language": "fr",
                    "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
                    "status": "pending",
                    "created_at": "2026-10-22T10:00:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Not submittable now",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "TEMPLATE_PENDING_REVIEW",
                    "message": "This template is waiting for EnvoiSMS review; it is submitted to Meta once approved.",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "send"
      }
    },
    "/templates/sync": {
      "post": {
        "summary": "Sync template statuses from Meta",
        "operationId": "syncTemplates",
        "tags": [
          "Templates"
        ],
        "responses": {
          "200": {
            "description": "Sync summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "wabas": [
                    "1029384756"
                  ],
                  "fetched": 12,
                  "updated": 3,
                  "inserted": 1,
                  "deduped": 0,
                  "marked_deleted": 0,
                  "complete": true,
                  "errors": []
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Sync template statuses from Meta\n\n**Permission:** `send`.",
        "x-permission": "send"
      }
    },
    "/sender-ids": {
      "get": {
        "summary": "List Sender ID registrations",
        "operationId": "listSenderIds",
        "tags": [
          "Sender IDs"
        ],
        "responses": {
          "200": {
            "description": "Your registrations and a suggested name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "data": [
                    {
                      "id": "sid_…",
                      "sender_id": "MABOUTIQUE",
                      "status": "pending"
                    }
                  ],
                  "suggestedSenderId": "MABOUTIQUE"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "List Sender ID registrations\n\n**Permission:** `sender_id`.",
        "x-permission": "sender_id"
      },
      "post": {
        "summary": "Register a Sender ID",
        "description": "Registers an alphanumeric sender name (3–11 characters) with the Moroccan carriers. Multipart form; the trade-register extract is a PDF up to 5 MB. Allow 15 to 30 days.\n\n**Permission:** `sender_id`.",
        "operationId": "registerSenderId",
        "tags": [
          "Sender IDs"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "sender_id",
                  "file"
                ],
                "properties": {
                  "sender_id": {
                    "type": "string",
                    "example": "MABOUTIQUE"
                  },
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Trade-register extract (PDF, ≤5 MB)"
                  },
                  "company_website": {
                    "type": "string"
                  },
                  "sample_content": {
                    "type": "string"
                  },
                  "message_type": {
                    "type": "string"
                  },
                  "estimated_volume": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registration received",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "sid_…",
                  "sender_id": "MABOUTIQUE",
                  "status": "pending"
                }
              }
            }
          },
          "400": {
            "description": "Generic or invalid name, or bad file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SENDER_ID_GENERIC",
                    "message": "INFO is a generic header, not a brand. Use your brand name.",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Already pending or approved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "SENDER_ID_PENDING",
                    "message": "Sender ID MABOUTIQUE is already pending review",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "sender_id"
      }
    },
    "/whatsapp/waba-connection": {
      "get": {
        "summary": "Get your WhatsApp connection",
        "description": "Your connected WhatsApp Business numbers and, for the default one, `readiness`: whether Meta lets it send (`can_send`), the blockers to fix, business verification and approved template count. Read-only with an API key: connecting or disconnecting numbers, relay URL and secret, and every other `/whatsapp` change need a dashboard session.\n\n**Permission:** any valid API key.",
        "operationId": "getWabaConnection",
        "tags": [
          "WhatsApp Business Profile"
        ],
        "responses": {
          "200": {
            "description": "Connection state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "connected": true,
                  "mode": "custom_waba",
                  "waba_id": "1029384756",
                  "phone_number_id": "1433348623189601",
                  "phone_number": "+212 600-000000",
                  "name": "Ma Boutique",
                  "status": "connected",
                  "quality_rating": "GREEN",
                  "messaging_limit_tier": "TIER_1K",
                  "readiness": {
                    "waba_id": "1029384756",
                    "payment_method": "unchecked",
                    "can_send": "AVAILABLE",
                    "blockers": [],
                    "business_verification": "verified",
                    "approved_templates": 4,
                    "checked_at": "2026-10-22T10:00:00Z"
                  },
                  "connections": [
                    {
                      "phone_number_id": "1433348623189601",
                      "phone_number": "+212 600-000000",
                      "is_default": true,
                      "status": "connected"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-permission": "any"
      }
    },
    "/machine-payments/status": {
      "get": {
        "summary": "Machine payments status",
        "description": "Public. Whether an agent may pay per deposit instead of holding a key. A paid route (`/messages`, `/waba`, `/verify`, `/mcp`) called with no Authorization header AND with `X-Payment-Method: mpp` (or `Accept: application/problem+json`) answers 402 with `WWW-Authenticate: Payment`; without that header it is an ordinary 401. Pay the USDC deposit, retry with `Authorization: Payment <credential>`, and keep the scoped key returned in `X-Machine-Session-Key`.",
        "operationId": "getMachinePaymentsStatus",
        "tags": [
          "System & Utilities"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "enabled": true,
                  "deposit_usd": 5,
                  "credit_mad": 47.29,
                  "networks": [
                    "base",
                    "tempo"
                  ],
                  "permissions": [
                    "send",
                    "status",
                    "balance",
                    "verify"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/moderation/appeals": {
      "post": {
        "summary": "Appeal a content refusal",
        "description": "Public (a suspended account has no working key). Use `error.appeal.id` and `error.appeal.token` from the CONTENT_REFUSED / CONTENT_BLOCKED / SPAM_OR_PHISHING_DETECTED response.",
        "operationId": "appealModeration",
        "tags": [
          "System & Utilities"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "token"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "token": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string",
                    "description": "What the message was for"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Appeal received",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "received",
                  "id": "appeal id"
                }
              }
            }
          },
          "400": {
            "description": "Missing or wrong id/token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "INVALID_APPEAL",
                    "message": "id and token are required",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already appealed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "APPEAL_ALREADY_FILED",
                    "message": "An appeal is already open for this refusal",
                    "docs": "https://envoisms.ma/en/docs#errors"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "SMS & WhatsApp Messaging",
      "description": "Send SMS and WhatsApp messages, one or up to 10,000 per call, and read their status."
    },
    {
      "name": "Phone Verification (OTP)",
      "description": "One-time codes: generated, delivered, attempt-counted and expired for you."
    },
    {
      "name": "Number Intelligence & HLR",
      "description": "Validate a number, resolve its carrier, or ask the network itself whether it is reachable (billed per lookup)."
    },
    {
      "name": "Templates",
      "description": "SMS and WhatsApp templates; WhatsApp ones are reviewed, then submitted to Meta."
    },
    {
      "name": "Sender IDs",
      "description": "Register an alphanumeric sender name with the Moroccan carriers."
    },
    {
      "name": "Conversations & Live Chat",
      "description": "Two-way WhatsApp and SMS threads and replies within the 24-hour window."
    },
    {
      "name": "WhatsApp Business Profile",
      "description": "Read your connected WhatsApp numbers, their readiness and business profile."
    },
    {
      "name": "AI Qualified Leads",
      "description": "Leads qualified by your WhatsApp qualification bot."
    },
    {
      "name": "Inbound Webhooks",
      "description": "Catch leads from forms and ad platforms and message them automatically."
    },
    {
      "name": "Contacts & Audience",
      "description": "Contacts, lists and imports."
    },
    {
      "name": "Campaigns & Bulk Dispatch",
      "description": "Scheduled SMS and WhatsApp template campaigns to a contact list."
    },
    {
      "name": "Compliance & Consent",
      "description": "Opt-outs (STOP) and an append-only consent ledger (Law 09-08)."
    },
    {
      "name": "Delivery Webhooks (DLR)",
      "description": "Signed callbacks for statuses, replies and opt-outs; logs and replay."
    },
    {
      "name": "Billing & Balances",
      "description": "Balance, transactions, top-ups and sending quotas."
    },
    {
      "name": "Analytics & Reporting",
      "description": "Delivery and cost statistics over a window."
    },
    {
      "name": "API Keys & Security",
      "description": "Live and sandbox keys, permissions, IP allow-lists and rate limits."
    },
    {
      "name": "System & Utilities",
      "description": "Health, search, machine payments and moderation appeals."
    }
  ],
  "x-tagGroups": [
    {
      "name": "Messaging",
      "tags": [
        "SMS & WhatsApp Messaging",
        "Phone Verification (OTP)",
        "Number Intelligence & HLR",
        "Templates",
        "Sender IDs"
      ]
    },
    {
      "name": "WhatsApp & conversations",
      "tags": [
        "Conversations & Live Chat",
        "WhatsApp Business Profile",
        "AI Qualified Leads",
        "Inbound Webhooks"
      ]
    },
    {
      "name": "Audience & webhooks",
      "tags": [
        "Contacts & Audience",
        "Campaigns & Bulk Dispatch",
        "Compliance & Consent",
        "Delivery Webhooks (DLR)"
      ]
    },
    {
      "name": "Account",
      "tags": [
        "Billing & Balances",
        "Analytics & Reporting",
        "API Keys & Security",
        "System & Utilities"
      ]
    }
  ],
  "x-generated-from": "docs/api/openapi.yaml — generated by scripts/generate-openapi-json.mjs, do not edit by hand"
}
