{
  "openapi": "3.1.0",
  "info": {
    "title": "Virtnumber SMS API & MCP Server",
    "version": "2.1.0",
    "description": "Rent virtual phone numbers, read the SMS they receive, send SMS, and check pricing, paid from your prepaid balance.\n\nREST resources, one URL/verb per action, RFC 9457 `application/problem+json` error bodies, and `RateLimit-*` response headers on every request. The same operations are available to AI agents through the MCP server at https://www.virtnumber.com/mcp.",
    "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. Requires ≥1 EUR balance, like every API call except the balance."
    },
    {
      "name": "Pricing",
      "description": "Read-only catalog. Requires ≥1 EUR balance, like every API call except the balance."
    },
    {
      "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."
    },
    {
      "name": "Balance",
      "description": "Your prepaid balance. The account owner tops it up on the website. The only call that also works below 1 EUR."
    }
  ],
  "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. Requires ≥1 EUR balance.",
        "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"
          },
          "403": {
            "$ref": "#/components/responses/LowBalance"
          },
          "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. Requires ≥1 EUR balance.",
        "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"
          },
          "403": {
            "$ref": "#/components/responses/LowBalance"
          },
          "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"
                    },
                    "numberstatus": {
                      "type": "string",
                      "enum": [
                        "active",
                        "pending"
                      ],
                      "description": "active = ready to use. pending = still being set up: the number is a placeholder with X digits and receives nothing yet; it changes to the real number and becomes active once set up."
                    }
                  }
                },
                "example": {
                  "phone": "447911123456",
                  "plan": "30d",
                  "price": 9.9,
                  "days": 30,
                  "numberstatus": "active"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "description": "Balance below the 1 EUR API minimum, or no billing details saved on the account (every number needs the buyer on record; plans with requiresAddress need an address in that country instead).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "lowBalance": {
                    "value": {
                      "type": "https://virtnumber.com/developer/errors/low-balance",
                      "title": "Account balance below minimum required to use the API",
                      "status": 403
                    }
                  },
                  "billingRequired": {
                    "value": {
                      "type": "https://virtnumber.com/developer/errors/billing-required",
                      "title": "Billing details required",
                      "status": 403,
                      "detail": "Every number needs the buyer's billing details on record. Add them at https://www.virtnumber.com/users/orders and retry.",
                      "billingUrl": "https://www.virtnumber.com/users/orders"
                    }
                  }
                }
              }
            }
          },
          "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"
          },
          "409": {
            "$ref": "#/components/responses/NumberPending"
          },
          "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 SMS. One SMS is up to 160 characters, or up to 70 if the text has non-Latin characters; a longer text is billed as several SMS (153 or 67 characters each). Requires ≥1 EUR balance.",
        "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 SMS (160 characters, or 70 with non-Latin characters)."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "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"
          },
          "403": {
            "$ref": "#/components/responses/LowBalance"
          },
          "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`. Every message is checked automatically before it is sent; a message that may break the messaging rules is held (422) and still charged. A recipient who replied STOP to the number is blocked permanently (403).",
        "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",
                      "description": "EUR charged: the price per SMS times segments."
                    },
                    "segments": {
                      "type": "integer",
                      "description": "How many SMS the text was billed as: 160 characters each (70 with non-Latin characters), 153 / 67 each when longer."
                    }
                  }
                },
                "example": {
                  "sid": "SM1234567890abcdef1234567890abcdef",
                  "to": "+14155551234",
                  "price": 0.0284,
                  "segments": 1
                }
              }
            }
          },
          "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"
          },
          "403": {
            "description": "Recipient opted out: they replied STOP to this number, so it can no longer send them messages (problem type recipient-opted-out). Not charged.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Message held by the automatic content check and not sent (problem type message-held): status pending_review or rejected, with a reason. The price is still charged.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending_review",
                        "rejected"
                      ]
                    },
                    "reason": {
                      "type": "string"
                    },
                    "price": {
                      "type": "number"
                    },
                    "segments": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "Read messages",
        "operationId": "v2ListMessages",
        "description": "Reads SMS received on (and sent from) one of your numbers, newest first. With `after_id`, returns only newer messages, oldest first. With `wait` (1 to 55 seconds) the request waits until a new incoming message arrives and returns it, so one request replaces repeated polling.",
        "x-limits": [
          {
            "limit": "API requests",
            "value": "20 / 15 min"
          }
        ],
        "parameters": [
          {
            "name": "number",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "447911123456",
            "description": "One of your numbers, with or without +."
          },
          {
            "name": "after_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Only messages with a larger id."
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only messages whose sender contains this text."
          },
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "in",
                "out"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "wait",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 55
            },
            "description": "Wait up to this many seconds for the next incoming message."
          }
        ],
        "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": {
                    "number": {
                      "type": "string"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    },
                    "latestId": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "number": "447911123456",
                  "messages": [
                    {
                      "id": 48213,
                      "direction": "in",
                      "from": "Google",
                      "to": "447911123456",
                      "text": "G-482913 is your Google verification code.",
                      "receivedAt": "2026-09-24T10:15:02.000Z",
                      "codes": [
                        "482913"
                      ]
                    }
                  ],
                  "latestId": 48213
                }
              }
            }
          },
          "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"
          }
        }
      }
    },
    "/api/v2/developer/balance": {
      "get": {
        "tags": [
          "Balance"
        ],
        "summary": "Get balance",
        "operationId": "v2GetBalance",
        "description": "Your balance in EUR. 1 credit = 1 EUR. The account owner tops up the balance on the website; API clients and agents spend the existing balance only.",
        "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": {
                    "balanceEur": {
                      "type": "number"
                    }
                  }
                },
                "example": {
                  "balanceEur": 12.5
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "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 with PKCE, set up automatically by the client. Headless agents can send x-api-user and x-api-token headers instead.",
    "tools": [
      {
        "name": "get_balance",
        "scope": "(any)",
        "description": "Balance, this app's spending limit, permissions"
      },
      {
        "name": "get_pricing",
        "scope": "pricing:read",
        "description": "Countries, plans and prices"
      },
      {
        "name": "list_my_numbers",
        "scope": "numbers:read",
        "description": "Your rented numbers"
      },
      {
        "name": "list_messages",
        "scope": "messages:read",
        "description": "Read SMS on a number, with detected codes"
      },
      {
        "name": "wait_for_sms",
        "scope": "messages:read",
        "description": "Wait up to 55 s for the next SMS"
      },
      {
        "name": "buy_number",
        "scope": "numbers:buy",
        "description": "Rent a number (idempotency_key supported)"
      },
      {
        "name": "send_sms",
        "scope": "messages:send",
        "description": "Send an SMS (a long text is billed as several SMS)"
      },
      {
        "name": "delete_number",
        "scope": "numbers:delete",
        "description": "Cancel a number"
      }
    ],
    "connect": [
      {
        "client": "Claude (web and desktop)",
        "kind": "steps",
        "value": "Settings, Connectors, Add custom connector, paste the server URL."
      },
      {
        "client": "Claude Code",
        "kind": "command",
        "value": "claude mcp add --transport http virtnumber https://www.virtnumber.com/mcp"
      },
      {
        "client": "Cursor (~/.cursor/mcp.json)",
        "kind": "config",
        "value": "{\n  \"mcpServers\": {\n    \"virtnumber\": { \"url\": \"https://www.virtnumber.com/mcp\" }\n  }\n}"
      },
      {
        "client": "VS Code (.vscode/mcp.json)",
        "kind": "config",
        "value": "{\n  \"servers\": {\n    \"virtnumber\": { \"type\": \"http\", \"url\": \"https://www.virtnumber.com/mcp\" }\n  }\n}"
      },
      {
        "client": "Headless agent (API key)",
        "kind": "config",
        "value": "POST https://www.virtnumber.com/mcp\nx-api-user: <your user id>\nx-api-token: <your API token>"
      }
    ]
  },
  "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",
            "enum": [
              "active",
              "pending"
            ],
            "description": "active = ready to use. pending = still being set up: the number is a placeholder with X digits and receives nothing yet; it changes to the real number and becomes active once set up.",
            "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"
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Increasing message id. Use as after_id to fetch only newer messages."
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ]
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "receivedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "UTC"
          },
          "codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Digit groups of 4 to 8 found in the text, e.g. a code. The full text is always in text."
          }
        }
      }
    },
    "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"
            }
          }
        }
      },
      "NumberPending": {
        "description": "The number is still being set up and can't be cancelled yet.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://virtnumber.com/developer/errors/number-pending",
              "title": "This number is still being set up",
              "status": 409,
              "detail": "The number is still being set up and cannot be deleted yet."
            }
          }
        }
      }
    }
  }
}
