{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenSMS API",
    "version": "1.0.0",
    "description": "REST API for sending OTP, transactional, and marketing SMS through Android gateways. Authenticate with an API key in the x-api-key header or a Firebase bearer token. Select the sandbox environment with the x-opensms-environment header. Full guides at https://opensms.cloud/docs",
    "contact": {
      "name": "OpenSMS Support",
      "email": "support@opensms.cloud",
      "url": "https://opensms.cloud"
    }
  },
  "servers": [
    { "url": "https://api.opensms.cloud", "description": "Production API" }
  ],
  "security": [{ "apiKey": [] }, { "firebaseBearer": [] }],
  "tags": [
    { "name": "Messages", "description": "Send and track outbound SMS" },
    { "name": "OTP", "description": "One-time password challenges" },
    { "name": "Transactional", "description": "Template-based transactional SMS" },
    { "name": "Marketing", "description": "Consent-confirmed campaign sends" },
    { "name": "Gateways", "description": "Android gateway fleet management" },
    { "name": "Account", "description": "API keys and webhooks" },
    { "name": "System", "description": "Health and readiness" }
  ],
  "paths": {
    "/v1/messages/send": {
      "post": {
        "operationId": "sendMessage",
        "tags": ["Messages"],
        "summary": "Send an outbound SMS",
        "description": "Normalizes the Philippine recipient number, detects the network, routes to an online Android gateway (or fallback provider), and returns the pending message.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["to", "content"],
                "properties": {
                  "to": { "type": "string", "description": "Recipient phone number (09xx or +639xx)." },
                  "content": { "type": "string", "description": "SMS body." },
                  "from": { "type": "string", "description": "Optional sender gateway phone number." },
                  "request_id": { "type": "string", "description": "Optional customer trace reference." },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Message accepted; response includes message id and status." },
          "401": { "description": "Missing or invalid credentials." },
          "422": { "description": "Invalid recipient or content." }
        }
      }
    },
    "/v1/messages": {
      "get": {
        "operationId": "listMessages",
        "tags": ["Messages"],
        "summary": "List outbound messages",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50 } }
        ],
        "responses": {
          "200": { "description": "Messages for the authenticated account, newest first." },
          "401": { "description": "Missing or invalid credentials." }
        }
      }
    },
    "/v1/messages/inbound": {
      "get": {
        "operationId": "listInboundMessages",
        "tags": ["Messages"],
        "summary": "List received (inbound) SMS",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50 } }
        ],
        "responses": {
          "200": { "description": "Inbound messages, newest first." },
          "401": { "description": "Missing or invalid credentials." }
        }
      }
    },
    "/v1/messages/{id}": {
      "get": {
        "operationId": "getMessage",
        "tags": ["Messages"],
        "summary": "Fetch message status",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Message with delivery attempts." },
          "404": { "description": "Message not found." }
        }
      }
    },
    "/v1/admin/balance": {
      "get": {
        "operationId": "getAdminBalance",
        "tags": ["Messages"],
        "summary": "Provider balances for admins",
        "description": "Returns balances for providers that support balance checks (for example Semaphore). Requires an admin Firebase session.",
        "security": [{ "firebaseBearer": [] }],
        "responses": {
          "200": { "description": "Balance per provider." },
          "403": { "description": "Admin access required." }
        }
      }
    },
    "/v1/otp/send": {
      "post": {
        "operationId": "sendOtp",
        "tags": ["OTP"],
        "summary": "Send an OTP code",
        "description": "Creates an OTP challenge and delivers the code by SMS. Returns an otp_id used later for verification. In the sandbox environment the response also includes test_code so flows can be verified without reading the SMS.",
        "externalDocs": { "url": "https://opensms.cloud/docs/otp" },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["to"],
                "properties": {
                  "to": { "type": "string", "description": "Recipient phone number." },
                  "brand": { "type": "string", "description": "Product or company name shown in the SMS." },
                  "purpose": { "type": "string", "description": "Use case label such as login or checkout." },
                  "ttl_seconds": { "type": "number", "default": 300, "description": "Code lifetime in seconds (60-1800)." },
                  "request_id": { "type": "string" },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "OTP challenge created; response includes otp_id and, in sandbox, test_code." },
          "401": { "description": "Missing or invalid credentials." },
          "429": { "description": "OTP send rate limit exceeded." }
        }
      }
    },
    "/v1/otp/verify": {
      "post": {
        "operationId": "verifyOtp",
        "tags": ["OTP"],
        "summary": "Verify an OTP code",
        "description": "Verifies the code the user entered against an existing OTP challenge. Max 5 attempts per challenge.",
        "externalDocs": { "url": "https://opensms.cloud/docs/otp" },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["otp_id", "code"],
                "properties": {
                  "otp_id": { "type": "string", "description": "ID returned by /v1/otp/send." },
                  "code": { "type": "string", "description": "Code entered by the user." },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Verification result with status verified, pending, failed, or expired." },
          "401": { "description": "Missing or invalid credentials." },
          "429": { "description": "OTP verify rate limit exceeded." }
        }
      }
    },
    "/v1/otp": {
      "get": {
        "operationId": "listOtps",
        "tags": ["OTP"],
        "summary": "List OTP challenges",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 100 } }
        ],
        "responses": {
          "200": { "description": "OTP challenges for the account, newest first. Codes are never returned." }
        }
      }
    },
    "/v1/transactional/templates": {
      "get": {
        "operationId": "listTransactionalTemplates",
        "tags": ["Transactional"],
        "summary": "List transactional templates",
        "responses": {
          "200": { "description": "Templates for the account." }
        }
      },
      "post": {
        "operationId": "createTransactionalTemplate",
        "tags": ["Transactional"],
        "summary": "Create a transactional SMS template",
        "externalDocs": { "url": "https://opensms.cloud/docs/transactional" },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "content"],
                "properties": {
                  "name": { "type": "string", "description": "Readable name for the dashboard." },
                  "content": { "type": "string", "description": "Template body with placeholders like {{name}}." },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Template created; response includes template_id." },
          "401": { "description": "Missing or invalid credentials." }
        }
      }
    },
    "/v1/transactional/templates/{id}": {
      "get": {
        "operationId": "getTransactionalTemplate",
        "tags": ["Transactional"],
        "summary": "Fetch a template",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Template detail." },
          "404": { "description": "Template not found." }
        }
      },
      "patch": {
        "operationId": "updateTransactionalTemplate",
        "tags": ["Transactional"],
        "summary": "Update a template",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Updated template." }
        }
      }
    },
    "/v1/transactional/templates/{id}/activate": {
      "post": {
        "operationId": "activateTransactionalTemplate",
        "tags": ["Transactional"],
        "summary": "Activate a template",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Template activated." } }
      }
    },
    "/v1/transactional/templates/{id}/archive": {
      "post": {
        "operationId": "archiveTransactionalTemplate",
        "tags": ["Transactional"],
        "summary": "Archive a template",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Template archived." } }
      }
    },
    "/v1/transactional/send": {
      "post": {
        "operationId": "sendTransactionalSms",
        "tags": ["Transactional"],
        "summary": "Send a transactional SMS",
        "description": "Sends from an active template (template_id + variables) or with inline content.",
        "externalDocs": { "url": "https://opensms.cloud/docs/transactional" },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["to"],
                "properties": {
                  "template_id": { "type": "string", "description": "OpenSMS template identifier. Omit to send inline content." },
                  "to": { "type": "string", "description": "Single recipient phone number." },
                  "content": { "type": "string", "description": "Inline body used when template_id is omitted." },
                  "variables": { "type": "object", "description": "Named values for the placeholders declared by the template." },
                  "request_id": { "type": "string", "description": "Optional customer trace reference." },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Message accepted for delivery." },
          "401": { "description": "Missing or invalid credentials." }
        }
      }
    },
    "/v1/marketing/send": {
      "post": {
        "operationId": "sendMarketingCampaign",
        "tags": ["Marketing"],
        "summary": "Send a marketing SMS batch",
        "description": "Sends the same content to up to 100 consent-confirmed recipients. Set async=true to receive HTTP 202 immediately and process in the background; track results through /v1/messages using the campaign request-id prefix.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["recipients", "content", "consent_confirmed"],
                "properties": {
                  "recipients": { "type": "array", "items": { "type": "string" }, "maxItems": 100 },
                  "content": { "type": "string" },
                  "campaign_name": { "type": "string" },
                  "consent_confirmed": { "type": "boolean", "description": "Must be true. Confirms recipients opted in." },
                  "request_id_prefix": { "type": "string" },
                  "async": { "type": "boolean", "default": false, "description": "Accept immediately and process in the background." },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Campaign processed synchronously with per-recipient results." },
          "202": { "description": "Campaign accepted for background processing (async=true)." },
          "422": { "description": "Missing consent, content, or recipients." }
        }
      }
    },
    "/v1/gateways/register": {
      "post": {
        "operationId": "registerGateway",
        "tags": ["Gateways"],
        "summary": "Register an Android gateway phone",
        "description": "Returns a one-time device token, an opensms:// pairing URI, and a QR data URI. The token is only shown once.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["phone_number"],
                "properties": {
                  "name": { "type": "string" },
                  "phone_number": { "type": "string" },
                  "mode": { "type": "string", "enum": ["send_only", "two_way"], "default": "send_only" },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Gateway registered; response includes one-time token and pairing QR." }
        }
      }
    },
    "/v1/gateways": {
      "get": {
        "operationId": "listGateways",
        "tags": ["Gateways"],
        "summary": "List gateway devices",
        "responses": {
          "200": { "description": "Gateways with online/offline status, battery, and network." }
        }
      }
    },
    "/v1/account/api-keys": {
      "get": {
        "operationId": "listApiKeys",
        "tags": ["Account"],
        "summary": "List managed API keys",
        "responses": { "200": { "description": "Key metadata only; secrets are never re-shown." } }
      },
      "post": {
        "operationId": "createApiKey",
        "tags": ["Account"],
        "summary": "Create a managed API key",
        "responses": { "200": { "description": "Plaintext osms_ key returned once." } }
      }
    },
    "/v1/account/api-keys/{id}": {
      "delete": {
        "operationId": "revokeApiKey",
        "tags": ["Account"],
        "summary": "Revoke an API key",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Key revoked." } }
      }
    },
    "/v1/account/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": ["Account"],
        "summary": "List webhook endpoints",
        "responses": { "200": { "description": "Endpoints with metadata and secret_prefix only." } }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": ["Account"],
        "summary": "Create a webhook endpoint",
        "description": "HTTPS URLs only. Returns a whsec_ signing secret once. Deliveries are signed with HMAC-SHA256 over timestamp + '.' + raw body in the OpenSMS-Signature header.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "format": "uri" },
                  "event_types": {
                    "type": "array",
                    "items": { "type": "string", "enum": ["message.sent", "message.delivered", "message.failed", "message.received"] }
                  },
                  "environment": { "type": "string", "enum": ["sandbox", "live"] }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Endpoint created; whsec_ secret returned once." } }
      }
    },
    "/v1/account/webhooks/{id}": {
      "patch": {
        "operationId": "updateWebhook",
        "tags": ["Account"],
        "summary": "Update a webhook endpoint",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Endpoint updated." } }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "tags": ["Account"],
        "summary": "Delete a webhook endpoint",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Endpoint deleted." } }
      }
    },
    "/v1/account/webhooks/{id}/rotate-secret": {
      "post": {
        "operationId": "rotateWebhookSecret",
        "tags": ["Account"],
        "summary": "Rotate the signing secret",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "New whsec_ secret returned once." } }
      }
    },
    "/v1/account/webhook-deliveries": {
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": ["Account"],
        "summary": "List webhook delivery attempts",
        "parameters": [
          { "name": "endpoint_id", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } }
        ],
        "responses": { "200": { "description": "Delivery attempts with status and retry schedule." } }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": ["System"],
        "summary": "Liveness probe",
        "security": [],
        "responses": {
          "200": { "description": "Process is up." }
        }
      }
    },
    "/ready": {
      "get": {
        "operationId": "getReady",
        "tags": ["System"],
        "summary": "Readiness probe",
        "description": "Returns 200 only when the storage backend is reachable.",
        "security": [],
        "responses": {
          "200": { "description": "Service is ready." },
          "503": { "description": "Storage backend unreachable." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "API key issued in the OpenSMS dashboard (Settings > API keys)."
      },
      "firebaseBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Firebase ID token for dashboard sessions. Issuer: https://securetoken.google.com/opensms-ba8dd"
      }
    }
  }
}
