{
  "openapi": "3.1.0",
  "info": {
    "title": "Virtnumber SMS API & MCP Server",
    "version": "2.0.0",
    "description": "Buy and manage virtual phone numbers, check pricing, and receive SMS by webhook.\n\nREST resources, one URL/verb per action, RFC 9457 `application/problem+json` error bodies, and `RateLimit-*` response headers on every request.",
    "contact": {
      "url": "https://www.virtnumber.com/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://www.virtnumber.com/terms"
    }
  },
  "servers": [
    { "url": "https://www.virtnumber.com", "description": "Production" }
  ],
  "security": [{ "apiUser": [], "apiToken": [] }],
  "tags": [
    { "name": "Countries", "description": "Read-only catalog. No balance required." },
    { "name": "Pricing", "description": "Read-only catalog. No balance required." },
    { "name": "Numbers", "description": "Buy, list, and cancel numbers. Requires ≥1 EUR balance." },
    { "name": "Messages", "description": "Send SMS from an owned number. Only virtual numbers can send." }
  ],
  "paths": {
    "/api/v2/developer/countries": {
      "get": {
        "tags": ["Countries"],
        "summary": "List available countries",
        "description": "Every country currently on sale, with its ISO2 code and calling code. No account balance required.",
        "operationId": "v2ListCountries",
        "responses": {
          "200": {
            "description": "OK",
            "headers": { "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" } },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "countryid": { "type": "string", "example": "GB", "description": "ISO2 code. Use this as `countryid` on every other endpoint." },
                          "countryfull": { "type": "string", "example": "United Kingdom" },
                          "countrycode": { "type": "string", "example": "44", "description": "Calling code, no leading +." }
                        }
                      }
                    }
                  }
                },
                "example": { "countries": [{ "countryid": "GB", "countryfull": "United Kingdom", "countrycode": "44" }, { "countryid": "US", "countryfull": "United States", "countrycode": "1" }] }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v2/developer/pricing": {
      "get": {
        "tags": ["Pricing"],
        "summary": "List pricing plans",
        "description": "Every plan (7/30/180/360-day, etc.) grouped by country. No account balance required.",
        "operationId": "v2ListPricing",
        "parameters": [
          { "name": "countryid", "in": "query", "required": false, "description": "Filter to one country (case-insensitive).", "schema": { "type": "string", "example": "GB" } }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": { "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" } },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pricing": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "countryid": { "type": "string", "example": "GB" },
                          "countryfull": { "type": "string", "example": "United Kingdom" },
                          "countrycode": { "type": "string", "example": "44" },
                          "plans": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": { "type": "integer", "example": 51, "description": "Plan ID. Pass this as `id` when buying a number." },
                                "days": { "type": "integer", "example": 30 },
                                "price": { "type": "string", "example": "9.90" },
                                "salestatus": { "type": "string", "example": "yes" },
                                "requiresAddress": { "type": "boolean", "example": false, "description": "True if buying this plan requires a saved billing address on the account (see /account/addresses) — the purchase fails with a 502 otherwise." }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": { "pricing": [{ "countryid": "GB", "countryfull": "United Kingdom", "countrycode": "44", "plans": [{ "id": 51, "days": 30, "price": "9.90", "salestatus": "yes", "requiresAddress": false }] }] }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidInput" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v2/developer/numbers": {
      "get": {
        "tags": ["Numbers"],
        "summary": "List your active numbers",
        "operationId": "v2ListNumbers",
        "responses": {
          "200": {
            "description": "OK",
            "headers": { "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" } },
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "numbers": { "type": "array", "items": { "$ref": "#/components/schemas/OwnedNumber" } } } },
                "example": { "numbers": [{ "number": "447911123456", "numberstatus": "active", "firstbuy": "2026-08-01 10:00:00", "expirationday": "2026-08-30 22:00:00", "webhook": "https://example.com/webhook" }] }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/LowBalance" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Numbers"],
        "summary": "Buy a number",
        "description": "Orders the next available number for the given plan. Get valid `id`/`countryid` pairs from `GET /api/v2/developer/pricing` first.",
        "operationId": "v2BuyNumber",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["id", "countryid"],
                "properties": {
                  "id": { "description": "Plan ID from the pricing endpoint.", "oneOf": [{ "type": "string" }, { "type": "integer" }], "example": 51 },
                  "countryid": { "type": "string", "example": "GB" },
                  "webhook": { "type": "string", "format": "uri", "default": "", "description": "Where incoming SMS to this number get POSTed. Can be set/changed later in number settings." }
                }
              },
              "example": { "id": 51, "countryid": "GB", "webhook": "https://example.com/webhook" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Purchased",
            "headers": { "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" } },
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "phone": { "type": "string" }, "plan": { "type": "string" }, "price": { "type": "number" }, "days": { "type": "integer" } } },
                "example": { "phone": "447911123456", "plan": "30d", "price": 9.9, "days": 30 }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidInput" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "403": { "$ref": "#/components/responses/LowBalance" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderUnavailable" }
        }
      },
      "delete": {
        "tags": ["Numbers"],
        "summary": "Cancel a number",
        "operationId": "v2DeleteNumber",
        "parameters": [
          { "name": "number", "in": "query", "required": true, "schema": { "type": "string" }, "example": "447911123456" }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "headers": { "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" } },
            "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } }, "example": { "message": "Number has been deleted" } } }
          },
          "400": { "$ref": "#/components/responses/InvalidInput" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/LowBalance" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderUnavailable" }
        }
      }
    },
    "/api/v2/developer/send-pricing": {
      "get": {
        "tags": ["Messages"],
        "summary": "List send pricing",
        "description": "For every country you can send *from* (virtual numbers only), every destination that number is allowed to send to and the price per message. No account balance required.",
        "operationId": "v2ListSendPricing",
        "responses": {
          "200": {
            "description": "OK",
            "headers": { "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" } },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sendPricing": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "countryid": { "type": "string", "example": "US", "description": "Country the sending number is from." },
                          "countryfull": { "type": "string", "example": "United States" },
                          "destinations": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "destination": { "type": "string", "example": "MX" },
                                "destinationName": { "type": "string", "example": "Mexico" },
                                "price": { "type": "number", "example": 0.6219, "description": "EUR per message." }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "sendPricing": [
                    {
                      "countryid": "US",
                      "countryfull": "United States",
                      "destinations": [
                        { "destination": "US", "destinationName": "United States", "price": 0.0284 },
                        { "destination": "CA", "destinationName": "Canada", "price": 0.0284 },
                        { "destination": "MX", "destinationName": "Mexico", "price": 0.6219 }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v2/developer/messages": {
      "post": {
        "tags": ["Messages"],
        "summary": "Send a message",
        "description": "Sends an SMS from one of your owned numbers. Only virtual numbers can send, and only to destinations listed for that number's country in `GET /api/v2/developer/send-pricing`.",
        "operationId": "v2SendMessage",
        "x-limits": [
          { "limit": "API requests", "value": "20 / 15 min" },
          { "limit": "New conversations", "value": "4 / day, per sending number" },
          { "limit": "Replies to an existing contact", "value": "2 / min, per sending number" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["number", "to", "text"],
                "properties": {
                  "number": { "type": "string", "example": "447911123456", "description": "One of your owned, active phone numbers. With or without a leading +." },
                  "to": { "type": "string", "example": "+14155551234", "description": "Destination number, E.164 or local format." },
                  "text": { "type": "string", "example": "Your table is ready." }
                }
              },
              "example": { "number": "447911123456", "to": "+14155551234", "text": "Your table is ready." }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent",
            "headers": { "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" } },
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "sid": { "type": "string" }, "to": { "type": "string" }, "price": { "type": "number" } } },
                "example": { "sid": "SM1234567890abcdef1234567890abcdef", "to": "+14155551234", "price": 0.0284 }
              }
            }
          },
          "400": { "$ref": "#/components/responses/InvalidInput" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/CannotSend" },
          "429": {
            "description": "One of three limits: the general 20-per-15-minute API limit (`RateLimit-*` headers), the 4-new-conversations-per-day cap, or the 2-replies-per-minute cap.",
            "headers": { "Retry-After": { "schema": { "type": "integer" } } },
            "content": {
              "application/problem+json": {
                "schema": { "allOf": [{ "$ref": "#/components/schemas/Problem" }, { "type": "object", "properties": { "resetSeconds": { "type": "integer" } } }] },
                "examples": {
                  "apiRateLimit": { "summary": "General API rate limit", "value": { "type": "https://virtnumber.com/developer/errors/rate-limited", "title": "Too many requests", "status": 429, "detail": "Rate limit exceeded. Please slow down and retry later.", "resetSeconds": 300 } },
                  "dailyLimit": { "summary": "New-conversation daily cap", "value": { "type": "https://virtnumber.com/developer/errors/daily-limit-reached", "title": "Daily new-conversation limit reached", "status": 429, "detail": "Daily limit of 4 new conversations reached for this number. You can still reply to contacts that have already messaged it." } },
                  "replyRateLimit": { "summary": "Reply-per-minute cap", "value": { "type": "https://virtnumber.com/developer/errors/send-rate-limited", "title": "Too many messages sent to this contact", "status": 429, "detail": "Max 2 messages per minute to an existing contact." } }
                }
              }
            }
          },
          "502": { "$ref": "#/components/responses/ProviderUnavailable" }
        }
      }
    }
  },
  "webhooks": {
    "incomingSms": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Incoming SMS",
        "operationId": "webhookIncomingSms",
        "description": "Sent to the `webhook` URL set on a number (via `numbers`, or later in number settings) every time it receives an SMS.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": { "type": "string", "example": "ok" },
                  "from": { "type": "string", "description": "Sender's number." },
                  "to": { "type": "string", "description": "Your virtnumber number." },
                  "text": { "type": "string" },
                  "datetime": { "type": "string", "example": "2030-05-06 21:37:28" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Any 2xx response is treated as delivered." } },
        "security": [],
        "x-signature-verification": "Every webhook request carries an `X-Virtnumber-Signature` header: `sha256=` + HMAC-SHA256 of the raw request body, keyed by `sha256(your API token)`. Recompute and compare (constant-time) before trusting the payload."
      }
    }
  },
  "x-mcp": {
    "url": "https://www.virtnumber.com/mcp",
    "auth": "OAuth 2.1 + PKCE, Bearer token",
    "tools": [
      { "name": "get_pricing", "scope": "pricing:read" },
      { "name": "list_my_numbers", "scope": "numbers:read" },
      { "name": "buy_number", "scope": "numbers:buy" },
      { "name": "delete_number", "scope": "numbers:delete" },
      { "name": "send_sms", "scope": "messages:send" }
    ]
  },
  "components": {
    "securitySchemes": {
      "apiUser": { "type": "apiKey", "in": "header", "name": "x-api-user", "description": "Your user ID. Find it in the developer panel." },
      "apiToken": { "type": "apiKey", "in": "header", "name": "x-api-token", "description": "Generate in the developer panel. Shown once, right after generation. The panel can't display an existing token again." }
    },
    "headers": {
      "RateLimit-Limit": { "schema": { "type": "integer" }, "description": "Requests allowed per window." },
      "RateLimit-Remaining": { "schema": { "type": "integer" }, "description": "Requests left in the current window." },
      "RateLimit-Reset": { "schema": { "type": "integer" }, "description": "Seconds until the window resets." }
    },
    "schemas": {
      "OwnedNumber": {
        "type": "object",
        "properties": {
          "number": { "type": "string" },
          "numberstatus": { "type": "string", "example": "active" },
          "firstbuy": { "type": "string" },
          "expirationday": { "type": "string" },
          "webhook": { "type": "string" }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 `application/problem+json`.",
        "properties": {
          "type": { "type": "string", "format": "uri" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "detail": { "type": "string" },
          "instance": { "type": "string", "format": "uri" }
        }
      }
    },
    "responses": {
      "Unauthorized": { "description": "Missing or invalid API credentials.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://virtnumber.com/developer/errors/unauthorized", "title": "Missing or invalid API credentials", "status": 401 } } } },
      "LowBalance": { "description": "Account balance below the 1 EUR minimum required to use the API.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://virtnumber.com/developer/errors/low-balance", "title": "Account balance below minimum required to use the API", "status": 403 } } } },
      "InsufficientBalance": { "description": "Balance too low for this specific purchase.", "content": { "application/problem+json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/Problem" }, { "type": "object", "properties": { "price": { "type": "number" }, "balance": { "type": "number" } } }] }, "example": { "type": "https://virtnumber.com/developer/errors/insufficient-balance", "title": "Balance too low for this purchase", "status": 402, "detail": "This purchase costs 9.90 EUR; your balance is 3.20 EUR.", "price": 9.9, "balance": 3.2 } } } },
      "NotFound": { "description": "Resource not found.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
      "InvalidInput": { "description": "Request failed validation.", "content": { "application/problem+json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/Problem" }, { "type": "object", "properties": { "errors": { "type": "array", "items": { "type": "object" } } } }] } } } },
      "RateLimited": { "description": "Too many requests: 20 per 15 minutes, shared with the legacy v1 endpoint.", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
      "ProviderUnavailable": { "description": "Upstream number provider couldn't fulfill the request.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
      "CannotSend": { "description": "This number can't send messages.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://virtnumber.com/developer/errors/cannot-send", "title": "This number cannot send messages", "status": 409, "detail": "Only virtual numbers can send messages" } } } }
    }
  }
}
