{
  "openapi": "3.0.3",
  "info": {
    "title": "EnvoiSMS.ma API",
    "description": "# EnvoiSMS.ma — L'API SMS #1 au Maroc\n\nEnvoyez des SMS via notre routage premium. L'envoi de messages WhatsApp n'est pas disponible actuellement sur les endpoints /messages — la verification OTP WhatsApp reste disponible via /verify/send avec `channel: \"whatsapp\"`. Telegram n'est pas propose.\n\n## Authentication\nAll API requests require an `Authorization: Bearer YOUR_API_KEY` header.\n\n## Base URL\n```\nhttps://api.envoisms.ma/v1\n```\n\n## Rate Limiting\nRate limits are applied per API key, over a rolling one-minute window.\nDefault: **100 requests/minute** per key (dashboard sessions: 600/minute).\nHigher limits are available per key — contact support.\n\nEvery authenticated response carries the current state, so your client can\nbuild backpressure without guessing:\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 timestamp (seconds) when the window resets |\n\nWhen the limit is exceeded the API returns `429 RATE_LIMITED` with a\n`Retry-After` header (seconds). Honor it — retrying earlier only consumes\nthe next window.\n\n## Idempotency\n`POST /messages` and `POST /messages/bulk` accept an optional\n`Idempotency-Key` header (any unique string up to 255 characters, e.g. a\nUUID). If a request times out or your client crashes mid-call, retry with\nthe **same key**: the platform guarantees the send executes at most once,\nand the retry returns the stored original response with an\n`Idempotency-Replayed: true` header instead of sending (and billing) again.\n\n- Keys are scoped to your account and expire after 24 hours.\n- Reusing a key with a **different** request body returns\n  `422 IDEMPOTENCY_KEY_REUSED` — a key names one request, not a channel.\n- If the original request is still executing, the retry returns\n  `409 IDEMPOTENCY_IN_FLIGHT` with `Retry-After`.\n\n## Webhook signatures\nEvery webhook delivery is signed with your endpoint's secret (returned once\nat creation, rotatable via `POST /webhooks/{id}/rotate-secret`). Verify the\n`X-EnvoiSMS-Signature-256` header, format `t=<unix-seconds>,v1=<hex>`:\n\n1. Split out `t` and `v1`.\n2. Compute `HMAC-SHA256(secret, \"{t}.\" + raw_request_body)` (hex).\n3. Compare to `v1` with a constant-time comparison, and reject if\n   `|now - t|` exceeds your tolerance (we recommend 5 minutes) — the\n   timestamp is inside the signed payload, so a captured delivery cannot\n   be replayed later.\n\nThe legacy `X-EnvoiSMS-Signature` header (HMAC-SHA256 over the body alone)\nis still sent for existing integrations; new code should verify the\ntimestamped header.\n\n## Error Format\n```json\n{\n  \"error\": {\n    \"code\": \"ERROR_CODE\",\n    \"message\": \"Human-readable message\",\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 over time. Common codes:\n\n| Code | HTTP | Meaning |\n|---|---|---|\n| `UNAUTHORIZED` | 401 | Missing or invalid API key |\n| `FORBIDDEN` | 403 | The key lacks the required permission scope, or the account is suspended |\n| `INSUFFICIENT_BALANCE` | 402 | Account balance too low for this send |\n| `OPTED_OUT` | 403 | The recipient unsubscribed from your messages |\n| `INVALID_PHONE` | 400 | `to` must be E.164, e.g. `+212612345678` |\n| `INVALID_CHANNEL` / `CHANNEL_NOT_CONFIGURED` | 400 | Channel unknown, or not available on the platform |\n| `SENDER_ID_NOT_APPROVED` / `SENDER_ID_PENDING` / `SENDER_ID_IN_REVIEW` / `SENDER_ID_REJECTED` | 403 | Sender ID not usable yet — see Settings → Sender IDs |\n| `MISSING_FIELD` / `INVALID_FIELD` / `INVALID_JSON` | 400 | Request body problems; the message names the field |\n| `INVALID_CODE` / `EXPIRED_CODE` / `MAX_ATTEMPTS` | 400/404/429 | OTP verification outcomes |\n| `RATE_LIMITED` | 429 | Too many requests — honor `Retry-After` |\n| `INVALID_IDEMPOTENCY_KEY` | 400 | `Idempotency-Key` header too long (max 255 chars) |\n| `IDEMPOTENCY_IN_FLIGHT` | 409 | Original request with this key still executing — retry shortly |\n| `IDEMPOTENCY_KEY_REUSED` | 422 | Key already used with a different request body |\n| `STRIPE_ERROR` | 502 | Payment page could not be created; nothing was charged |\n| `NOT_FOUND` | 404 | Resource does not exist or belongs to another account |\n| `INTERNAL_ERROR` | 500 | Failure on our side — safe to retry; contact support if it persists |\n\nCodes not in this table follow the same conventions; treat an unknown code\nas a terminal failure of the request, not of your integration.\n",
    "version": "1.0.0",
    "contact": {
      "name": "EnvoiSMS.ma Support",
      "email": "contact@envoisms.ma",
      "url": "https://envoisms.ma/support"
    },
    "license": {
      "name": "Commercial"
    }
  },
  "servers": [
    {
      "url": "https://api.envoisms.ma/v1",
      "description": "Production"
    },
    {
      "url": "https://api-staging.envoisms.ma/v1",
      "description": "Staging configuration; deploy and verify before using"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your SMS Maroc API key"
      }
    },
    "schemas": {
      "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",
              "telegram",
              "voice",
              "rcs"
            ],
            "description": "sms is generally available with multi-operator routing. whatsapp sending is not currently available on the messages endpoints (WhatsApp OTP verification is available via /verify/send). voice and rcs are available on request depending on account configuration. telegram is not currently offered."
          },
          "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
              }
            }
          }
        }
      },
      "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. 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",
          "message"
        ],
        "properties": {
          "to": {
            "type": "string",
            "description": "Destination phone in E.164 format",
            "example": "+212612345678"
          },
          "message": {
            "type": "string",
            "description": "Message content (max 1600 chars)",
            "example": "Bonjour! Votre commande #1234 est confirmée."
          },
          "from": {
            "type": "string",
            "description": "Sender ID (alphanumeric, max 11 chars)",
            "example": "MonApp"
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "whatsapp",
              "telegram",
              "voice",
              "rcs"
            ],
            "default": "sms",
            "description": "Use sms by default. whatsapp sending is not currently available on this endpoint — requests with channel whatsapp are rejected; WhatsApp OTP verification is available via /verify/send with channel whatsapp. telegram is not offered."
          },
          "cascade": {
            "type": "boolean",
            "description": "A cascade order is configured in production, but only channels that are actually configured and enabled are used — in practice sends fall through to SMS.",
            "default": false
          },
          "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": "Meta Cloud API v22.0 interactive message payload (list, flow, product, product_list, or carousel).",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "list",
                  "button",
                  "product",
                  "product_list",
                  "flow",
                  "carousel"
                ]
              },
              "header": {
                "type": "object"
              },
              "body": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string"
                  }
                }
              },
              "footer": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string"
                  }
                }
              },
              "action": {
                "type": "object"
              }
            }
          }
        }
      },
      "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. Use whatsapp for WhatsApp bulk sending after credentials are configured and the provider instance is authorized."
          }
        }
      },
      "VerifySendRequest": {
        "type": "object",
        "required": [
          "to"
        ],
        "properties": {
          "to": {
            "type": "string",
            "example": "+212612345678"
          },
          "channel": {
            "type": "string",
            "enum": [
              "sms",
              "whatsapp",
              "telegram",
              "voice",
              "rcs"
            ],
            "default": "sms",
            "description": "whatsapp = managed verification (recommended, cheapest): EnvoiSMS generates the code, delivers it on WhatsApp from a verified sender, then falls back to SMS automatically if unconfirmed within 60s — code valid 5 minutes, 3 attempts, nothing to store on your side; code_length, expiry 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. Accounts that want their own brand on the managed SMS fallback can request it at contact@envoisms.ma."
          },
          "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 — managed whatsapp verifications always use 6 digits"
          },
          "expiry": {
            "type": "integer",
            "description": "Code validity in seconds (sms channel only — managed whatsapp verifications are valid 300s)",
            "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"
          },
          "cascade": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ordered channel cascade list for validation routing (e.g. ['whatsapp', 'sms', 'voice']). ignored if app_id is set.",
            "example": [
              "whatsapp",
              "sms"
            ]
          },
          "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"
          }
        }
      },
      "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"
          }
        }
      }
    }
  },
  "paths": {
    "/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",
        "security": [],
        "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"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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",
        "security": [],
        "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"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Health check",
        "description": "Public service health endpoint. This route is served from the API root, not the /v1 prefix.",
        "operationId": "healthCheck",
        "security": [],
        "servers": [
          {
            "url": "https://api.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"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/billing/quota": {
      "get": {
        "summary": "Get billing quota limit",
        "description": "Gets the monthly spending/sending quota limits configured for safety.",
        "operationId": "getQuota",
        "responses": {
          "200": {
            "description": "Quota configuration",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monthly_limit_mad": {
                      "type": "number"
                    },
                    "monthly_usage_mad": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update billing quota limit",
        "description": "Updates the monthly limit (account owner or authorized admins/agents only).",
        "operationId": "updateQuota",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "monthly_limit_mad"
                ],
                "properties": {
                  "monthly_limit_mad": {
                    "type": "number",
                    "example": 5000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Quota updated successfully"
          }
        }
      }
    },
    "/messages": {
      "post": {
        "summary": "Send a message",
        "description": "Send one SMS through premium routing. WhatsApp sending is not currently available on this endpoint (WhatsApp OTP verification is 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).",
        "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
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Message queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient balance"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "get": {
        "summary": "List messages",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "sent",
                "delivered",
                "failed",
                "undeliverable"
              ]
            }
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "from_date",
            "in": "query",
            "description": "Start date (ISO 8601)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to_date",
            "in": "query",
            "description": "End date (ISO 8601)",
            "schema": {
              "type": "string"
            }
          },
          {
            "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"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of messages"
          }
        }
      }
    },
    "/messages/bulk": {
      "post": {
        "summary": "Send bulk messages",
        "description": "Send up to 10,000 messages in one request. WhatsApp sending is not currently available on this endpoint (WhatsApp OTP verification is available via /verify/send with channel `whatsapp`).",
        "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"
              },
              "examples": {
                "sms_bulk": {
                  "summary": "SMS bulk",
                  "value": {
                    "channel": "sms",
                    "from": "MaMarque",
                    "messages": [
                      {
                        "to": "+212612345678",
                        "message": "Bonjour Amal"
                      },
                      {
                        "to": "+212698765432",
                        "message": "Bonjour Youssef"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Batch queued"
          }
        }
      }
    },
    "/messages/{id}": {
      "get": {
        "summary": "Get message status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageRecord"
                }
              }
            }
          },
          "404": {
            "description": "Message not found"
          }
        }
      }
    },
    "/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.",
        "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)"
          },
          "402": {
            "description": "Insufficient balance"
          }
        }
      }
    },
    "/account/export": {
      "get": {
        "summary": "Export account data (data rights)",
        "description": "Self-service data export (Law 09-08 / GDPR): returns the account's personal data — profile, ledger, message logs, contacts, webhooks, verification sessions, support cases, opt-outs — as one JSON artefact. Dashboard session only; API keys are refused. Tables over the export cap are truncated and named in `truncated_tables` inside the file itself.",
        "operationId": "exportAccountData",
        "responses": {
          "200": {
            "description": "JSON export bundle (Content-Disposition attachment)"
          },
          "403": {
            "description": "Only available from a dashboard session"
          }
        }
      }
    },
    "/account/deletion-request": {
      "post": {
        "summary": "Request account deletion (data rights)",
        "description": "Opens a tracked erasure request (Law 09-08 / GDPR), processed within 30 days by an operator: personal data is erased or anonymised, invoicing records are retained for the legal accounting period. Dashboard session only. Idempotent — a second call returns the already-open case.",
        "operationId": "requestAccountDeletion",
        "responses": {
          "200": {
            "description": "A deletion request was already open"
          },
          "202": {
            "description": "Deletion request recorded, case id returned"
          },
          "403": {
            "description": "Only available from a dashboard session"
          }
        }
      }
    },
    "/verify/send": {
      "post": {
        "summary": "Send OTP verification code",
        "operationId": "sendVerify",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifySendRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "OTP sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "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."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/verify/check": {
      "post": {
        "summary": "Check OTP code",
        "operationId": "checkVerify",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id",
                  "code"
                ],
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "example": "123456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code verified",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "verified": {
                      "type": "boolean"
                    },
                    "verified_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Wrong code (error code INVALID_CODE; the message states how many attempts remain)"
          },
          "404": {
            "description": "Session not found, or the code expired (EXPIRED_CODE)"
          },
          "429": {
            "description": "Maximum verification attempts exceeded (MAX_ATTEMPTS); the session is closed"
          }
        }
      }
    },
    "/verify/lookup": {
      "post": {
        "summary": "Validate phone number (EnvoiSMS Lookup Engine)",
        "description": "Validates phone number format, carrier, and location using the operator database.",
        "operationId": "lookupNumber",
        "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"
          }
        }
      }
    },
    "/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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Session not found"
          }
        }
      }
    },
    "/balance": {
      "get": {
        "summary": "Get account balance (alias)",
        "operationId": "getBalance",
        "responses": {
          "200": {
            "description": "Current balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance_eur": {
                      "type": "number",
                      "example": 45.23
                    },
                    "balance_mad": {
                      "type": "number",
                      "example": 497.53
                    },
                    "currency": {
                      "type": "string",
                      "example": "EUR"
                    },
                    "plan": {
                      "type": "string",
                      "example": "business"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/billing/balance": {
      "get": {
        "summary": "Get billing balance",
        "operationId": "getBillingBalance",
        "responses": {
          "200": {
            "description": "Current balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance_eur": {
                      "type": "number",
                      "example": 45.23
                    },
                    "balance_mad": {
                      "type": "number",
                      "example": 497.53
                    },
                    "currency": {
                      "type": "string",
                      "example": "EUR"
                    },
                    "plan": {
                      "type": "string",
                      "example": "business"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/billing/transactions": {
      "get": {
        "summary": "List billing transactions",
        "operationId": "listBillingTransactions",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Billing transactions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "topup",
                              "usage",
                              "refund",
                              "adjustment"
                            ]
                          },
                          "amount_eur": {
                            "type": "number"
                          },
                          "amount_mad": {
                            "type": "number"
                          },
                          "balance_after_eur": {
                            "type": "number"
                          },
                          "description": {
                            "type": "string"
                          },
                          "reference": {
                            "type": "string",
                            "nullable": true
                          },
                          "payment_method": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/billing/plans": {
      "get": {
        "summary": "List plan pricing",
        "operationId": "listBillingPlans",
        "responses": {
          "200": {
            "description": "Pricing rows by plan and channel",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "plan": {
                            "type": "string",
                            "example": "starter"
                          },
                          "channel": {
                            "type": "string",
                            "enum": [
                              "sms",
                              "whatsapp",
                              "telegram",
                              "voice",
                              "rcs"
                            ]
                          },
                          "price_eur": {
                            "type": "number",
                            "example": 0.04455
                          },
                          "price_mad": {
                            "type": "number",
                            "example": 0.49
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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.",
        "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."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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.",
        "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"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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.",
        "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"
          },
          "503": {
            "description": "Requested payment method is not configured. Configure the missing Stripe or bank-transfer fields before retrying."
          }
        }
      }
    },
    "/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",
        "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
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/analytics": {
      "get": {
        "summary": "Get usage analytics",
        "operationId": "getAnalytics",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30,
              "maximum": 365
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window_days": {
                      "type": "integer"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "delivered": {
                          "type": "integer"
                        },
                        "failed": {
                          "type": "integer"
                        },
                        "delivery_rate": {
                          "type": "number"
                        },
                        "cost_eur": {
                          "type": "number"
                        },
                        "cost_mad": {
                          "type": "number"
                        }
                      }
                    },
                    "channels": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "daily": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/contacts": {
      "get": {
        "summary": "List contacts",
        "operationId": "listContacts",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "list_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact list"
          }
        }
      },
      "post": {
        "summary": "Create or update contact",
        "operationId": "upsertContact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "example": "+212612345678"
                  },
                  "name": {
                    "type": "string",
                    "example": "Sara"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "list_id": {
                    "type": "string"
                  },
                  "custom_fields": {
                    "type": "object",
                    "properties": {
                      "custom1": {
                        "type": "string"
                      },
                      "custom2": {
                        "type": "string"
                      },
                      "custom3": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact created"
          }
        }
      }
    },
    "/contacts/import": {
      "post": {
        "summary": "Import contacts",
        "operationId": "importContacts",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contacts"
                ],
                "properties": {
                  "list_id": {
                    "type": "string"
                  },
                  "contacts": {
                    "type": "array",
                    "maxItems": 5000,
                    "items": {
                      "type": "object",
                      "required": [
                        "phone"
                      ],
                      "properties": {
                        "phone": {
                          "type": "string",
                          "example": "+212612345678"
                        },
                        "name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Import accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "imported": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/contacts/lists": {
      "get": {
        "summary": "List contact lists",
        "operationId": "listContactLists",
        "responses": {
          "200": {
            "description": "Contact lists"
          }
        }
      },
      "post": {
        "summary": "Create contact list",
        "operationId": "createContactList",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Customers"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact list created"
          }
        }
      }
    },
    "/contacts/{id}": {
      "delete": {
        "summary": "Delete contact",
        "operationId": "deleteContact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact deleted"
          }
        }
      }
    },
    "/optouts": {
      "get": {
        "summary": "List opt-outs",
        "operationId": "listOptouts",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Opt-out registry"
          }
        }
      },
      "post": {
        "summary": "Create opt-out",
        "operationId": "createOptout",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "example": "+212612345678"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Recipient opted out"
          }
        }
      }
    },
    "/optouts/{phone}": {
      "delete": {
        "summary": "Remove opt-out",
        "operationId": "deleteOptout",
        "parameters": [
          {
            "name": "phone",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recipient opted back in"
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "summary": "List campaigns",
        "operationId": "listCampaigns",
        "responses": {
          "200": {
            "description": "Campaign list"
          }
        }
      },
      "post": {
        "summary": "Create campaign",
        "operationId": "createCampaign",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "sms",
                      "whatsapp",
                      "telegram",
                      "voice",
                      "rcs"
                    ],
                    "default": "sms"
                  },
                  "list_id": {
                    "type": "string"
                  },
                  "template_id": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "sender_id": {
                    "type": "string",
                    "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"
                    }
                  },
                  "metadata": {
                    "type": "object",
                    "description": "Custom metadata (e.g. throttling options: { throttling: '50_min' })",
                    "example": {
                      "throttling": "50_min"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campaign created"
          }
        }
      }
    },
    "/campaigns/{id}": {
      "get": {
        "summary": "Get campaign",
        "operationId": "getCampaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign details"
          },
          "404": {
            "description": "Campaign not found"
          }
        }
      }
    },
    "/campaigns/{id}/send": {
      "post": {
        "summary": "Send campaign",
        "operationId": "sendCampaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Campaign queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "queued": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "summary": "List webhooks",
        "operationId": "listWebhooks",
        "responses": {
          "200": {
            "description": "Webhook list"
          }
        }
      },
      "post": {
        "summary": "Create webhook",
        "operationId": "createWebhook",
        "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.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://yourapp.com/smsma-webhook"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "message.sent",
                        "message.delivered",
                        "message.failed",
                        "message.undeliverable",
                        "message.test",
                        "message.*"
                      ]
                    },
                    "default": [
                      "message.delivered",
                      "message.failed"
                    ]
                  },
                  "secret": {
                    "type": "string",
                    "description": "Optional HMAC secret. Generated and returned when omitted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "patch": {
        "summary": "Update webhook",
        "operationId": "updateWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated"
          }
        }
      },
      "delete": {
        "summary": "Disable webhook",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook disabled"
          }
        }
      }
    },
    "/webhooks/{id}/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.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New secret (returned only here)"
          },
          "404": {
            "description": "Webhook not found"
          }
        }
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "summary": "Test webhook",
        "operationId": "testWebhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Test webhook queued"
          }
        }
      }
    },
    "/api-keys": {
      "get": {
        "summary": "List API keys",
        "operationId": "listApiKeys",
        "responses": {
          "200": {
            "description": "API keys without full secret values"
          }
        }
      },
      "post": {
        "summary": "Create API key",
        "operationId": "createApiKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Production key"
                  },
                  "permissions": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "send",
                        "verify",
                        "status",
                        "balance",
                        "contacts",
                        "campaigns",
                        "webhooks",
                        "billing",
                        "analytics",
                        "api_keys",
                        "optouts"
                      ]
                    }
                  },
                  "ip_whitelist": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "rate_limit": {
                    "type": "integer",
                    "minimum": 10,
                    "maximum": 1000
                  },
                  "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."
          }
        }
      }
    },
    "/api-keys/{id}": {
      "patch": {
        "summary": "Update API key",
        "operationId": "updateApiKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "permissions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "ip_whitelist": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "rate_limit": {
                    "type": "integer"
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "API key updated"
          }
        }
      },
      "delete": {
        "summary": "Revoke API key",
        "operationId": "revokeApiKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "API key revoked"
          }
        }
      }
    },
    "/admin/monitoring": {
      "get": {
        "summary": "Admin launch monitoring snapshot",
        "description": "Admin-only operational view covering recent failures, pending top-ups, webhook issues, open support cases, configured channels, launch-readiness blockers, and the current cascade policy.",
        "operationId": "getAdminMonitoring",
        "responses": {
          "200": {
            "description": "Operational monitoring snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "action_required",
                        "critical"
                      ]
                    },
                    "checks": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "boolean"
                      }
                    },
                    "launch_readiness": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "ready",
                            "blocked",
                            "needs_canary"
                          ]
                        },
                        "ready_for_public_traffic": {
                          "type": "boolean"
                        },
                        "blocked_count": {
                          "type": "integer"
                        },
                        "needs_canary_count": {
                          "type": "integer"
                        },
                        "items": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "state": {
                                "type": "string",
                                "enum": [
                                  "ready",
                                  "blocked",
                                  "needs_canary",
                                  "guarded"
                                ]
                              },
                              "owner": {
                                "type": "string",
                                "enum": [
                                  "dns",
                                  "engineering",
                                  "ops",
                                  "finance",
                                  "support"
                                ]
                              },
                              "message": {
                                "type": "string"
                              },
                              "verified_at": {
                                "type": "string"
                              },
                              "missing": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "channels": {
                      "type": "object"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/admin/messages": {
      "get": {
        "summary": "Admin list messages",
        "description": "Admin-only message search for failed-send support and incident review.",
        "operationId": "listAdminMessages",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "sent",
                "delivered",
                "failed",
                "undeliverable"
              ]
            }
          },
          {
            "name": "account_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "sms",
                "whatsapp",
                "telegram",
                "voice",
                "rcs"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Admin message rows"
          }
        }
      }
    },
    "/admin/messages/{id}/retry": {
      "post": {
        "summary": "Retry a failed message",
        "description": "Admin-only retry action. Use SMS while multi-channel cascade remains disabled.",
        "operationId": "retryAdminMessage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel": {
                    "type": "string",
                    "enum": [
                      "sms",
                      "whatsapp",
                      "telegram",
                      "voice",
                      "rcs"
                    ],
                    "default": "sms"
                  },
                  "force": {
                    "type": "boolean"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Message retry queued"
          }
        }
      }
    },
    "/admin/topups": {
      "get": {
        "summary": "Admin list top-ups",
        "operationId": "listAdminTopups",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "account_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "payment_method",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Admin top-up rows"
          }
        }
      }
    },
    "/admin/topups/{id}/resolve": {
      "post": {
        "summary": "Resolve a manual top-up",
        "description": "Complete, fail, or cancel a pending top-up after settlement review. `reconcile` is the provider-verified variant of `complete`: it credits only what the payment provider confirms was collected, through the same idempotent path the crons and the claim endpoint use — reach for it first, and keep `complete` for payments the provider cannot vouch for.\n",
        "operationId": "resolveAdminTopup",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "complete",
                      "fail",
                      "cancel",
                      "reconcile"
                    ],
                    "default": "complete"
                  },
                  "reference": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Top-up resolved"
          }
        }
      }
    },
    "/admin/accounts/{id}/credit": {
      "post": {
        "summary": "Admin credit account",
        "operationId": "creditAdminAccount",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount_eur": {
                    "type": "number"
                  },
                  "amount_mad": {
                    "type": "number"
                  },
                  "note": {
                    "type": "string"
                  },
                  "reference": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account credited"
          }
        }
      }
    },
    "/admin/accounts/{id}/refund": {
      "post": {
        "summary": "Admin refund account",
        "description": "Creates a completed refund transaction and credits the customer balance.",
        "operationId": "refundAdminAccount",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount_eur": {
                    "type": "number"
                  },
                  "amount_mad": {
                    "type": "number"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "reference": {
                    "type": "string"
                  },
                  "message_id": {
                    "type": "string"
                  },
                  "transaction_id": {
                    "type": "string"
                  },
                  "support_case_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refund credited"
          }
        }
      }
    },
    "/admin/accounts/{id}/debit": {
      "post": {
        "summary": "Admin debit account",
        "description": "Takes balance off an account and writes the paired negative `adjustment`\nledger row. The only operator-driven way a balance goes down.\n\nA `reason` is mandatory — it is the sole record of why the customer lost\nbalance. The debit is refused with `409 INSUFFICIENT_BALANCE` if it would\ntake the account negative, unless `allow_negative` is set (a negative\nbalance blocks every send on the account). The action always raises an\nalerting ops event.\n\nAmounts: the currency you supply moves exactly; the derived currency is\nconverted at the configured `eur_mad_rate` and rounded up.\n",
        "operationId": "debitAdminAccount",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "amount_mad": {
                    "type": "number",
                    "description": "Amount in MAD; moves exactly as supplied"
                  },
                  "amount_eur": {
                    "type": "number",
                    "description": "Alternative to amount_mad"
                  },
                  "reason": {
                    "type": "string",
                    "description": "Mandatory — why the balance was taken"
                  },
                  "reference": {
                    "type": "string"
                  },
                  "allow_negative": {
                    "type": "boolean",
                    "default": false,
                    "description": "Permit the balance to go below zero"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Balance debited"
          },
          "400": {
            "description": "Missing amount or missing reason"
          },
          "409": {
            "description": "Debit would overdraw the account and allow_negative was not set"
          }
        }
      }
    },
    "/admin/verify-sessions": {
      "get": {
        "summary": "Admin list OTP verify sessions",
        "description": "Lists verify_sessions across all accounts, joined with account email and app name, with filtering and search.",
        "operationId": "listAdminVerifySessions",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "sms",
                "whatsapp",
                "telegram",
                "voice",
                "rcs"
              ]
            }
          },
          {
            "name": "purpose",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "account_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Matches against destination number, account email, or session id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verify sessions list with summary counts"
          }
        }
      }
    },
    "/admin/verify-sessions/{id}/void": {
      "post": {
        "summary": "Admin void a pending OTP verify session",
        "description": "Expires a pending verify session and deletes its KV code so the customer can no longer submit it. Fails with 409 if the session is not (or no longer) pending.",
        "operationId": "voidAdminVerifySession",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session voided"
          },
          "409": {
            "description": "Session is not pending"
          }
        }
      }
    },
    "/admin/support-cases": {
      "get": {
        "summary": "Admin list support cases",
        "operationId": "listAdminSupportCases",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "account_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Support case rows"
          }
        }
      },
      "post": {
        "summary": "Admin create support case",
        "operationId": "createAdminSupportCase",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "account_id",
                  "subject"
                ],
                "properties": {
                  "account_id": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "failed_send",
                      "refund",
                      "topup",
                      "dispute",
                      "webhook",
                      "support"
                    ]
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "normal",
                      "high",
                      "urgent"
                    ]
                  },
                  "subject": {
                    "type": "string"
                  },
                  "related_message_id": {
                    "type": "string"
                  },
                  "related_transaction_id": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Support case created"
          }
        }
      }
    },
    "/admin/support-cases/{id}": {
      "patch": {
        "summary": "Admin update support case",
        "operationId": "updateAdminSupportCase",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "investigating",
                      "waiting_customer",
                      "resolved",
                      "closed"
                    ]
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "normal",
                      "high",
                      "urgent"
                    ]
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Support case updated"
          }
        }
      }
    }
  },
  "x-generated-from": "docs/api/openapi.yaml — generated by scripts/generate-openapi-json.mjs, do not edit by hand"
}
