{
  "openapi": "3.1.0",
  "info": {
    "title": "Noir SMS API",
    "version": "1.0.0",
    "summary": "Buy temporary numbers and read SMS verification codes.",
    "description": "REST API to buy temporary virtual numbers and read SMS verification codes from your own system, app or reseller panel. Same prices, balance and automatic refunds as the dashboard. Human-readable docs: https://noirsms.com/api/docs/\n\nRate limit: 120 requests per minute per API key. Use the API only for your own legitimate accounts or, as a reseller, for customers who follow the Acceptable Use Policy (https://noirsms.com/acceptable-use).",
    "termsOfService": "https://noirsms.com/terms",
    "contact": { "name": "Noir SMS support", "url": "https://noirsms.com/support", "email": "support@noirsms.com" }
  },
  "externalDocs": { "description": "API docs", "url": "https://noirsms.com/api/docs/" },
  "servers": [{ "url": "https://noirsms.com/api/v1" }],
  "security": [{ "bearerAuth": [] }, { "apiKeyHeader": [] }],
  "tags": [
    { "name": "Account", "description": "Balance of the account that owns the key." },
    { "name": "Catalog", "description": "Countries and services in stock, with live prices." },
    { "name": "Numbers", "description": "Buy a number, read its SMS, cancel, resend or finish it." }
  ],
  "paths": {
    "/balance": {
      "get": {
        "tags": ["Account"],
        "operationId": "getBalance",
        "summary": "Your current balance",
        "responses": {
          "200": { "description": "Balance in US dollars.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balance" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/countries": {
      "get": {
        "tags": ["Catalog"],
        "operationId": "listCountries",
        "summary": "Countries with numbers in stock right now",
        "responses": {
          "200": { "description": "Countries in stock and how many services each has.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CountryList" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "502": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/services": {
      "get": {
        "tags": ["Catalog"],
        "operationId": "listServices",
        "summary": "Services in stock for one country, with price and stock",
        "description": "Prices follow supply and demand and can change during the day; read them at runtime or send max_price when buying.",
        "parameters": [
          { "name": "country", "in": "query", "required": false, "description": "2-letter ISO country code.", "schema": { "type": "string", "default": "us", "example": "us" } }
        ],
        "responses": {
          "200": { "description": "Services with the price you pay per number.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ServiceList" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "502": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/numbers": {
      "post": {
        "tags": ["Numbers"],
        "operationId": "buyNumber",
        "summary": "Buy a number",
        "description": "The price is debited from your balance and refunded automatically if no SMS arrives within 20 minutes.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuyRequest" } } }
        },
        "responses": {
          "201": { "description": "The number you bought.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Number" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "402": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "502": { "$ref": "#/components/responses/Error" },
          "503": { "$ref": "#/components/responses/Error" }
        }
      },
      "get": {
        "tags": ["Numbers"],
        "operationId": "listNumbers",
        "summary": "Numbers you bought through the API, newest first",
        "parameters": [
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": ["active", "waiting", "received", "canceled", "expired", "all"], "default": "active" }, "description": "active = numbers that are still open." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } },
          { "name": "before", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Pagination: numbers created before this time (use the last created_at)." }
        ],
        "responses": {
          "200": { "description": "A page of numbers.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NumberList" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/numbers/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/NumberId" }],
      "get": {
        "tags": ["Numbers"],
        "operationId": "getNumber",
        "summary": "Status of one number and every SMS it received",
        "description": "Checks the carrier in real time while the number is open. Poll every 3 to 5 seconds until status is received, or use the sms.received webhook.",
        "responses": {
          "200": { "description": "The number.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Number" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/numbers/{id}/cancel": {
      "parameters": [{ "$ref": "#/components/parameters/NumberId" }],
      "post": {
        "tags": ["Numbers"],
        "operationId": "cancelNumber",
        "summary": "Cancel a number that has not received an SMS and get the price back",
        "description": "Allowed from cancelable_at (2 minutes after buying). If the SMS arrived in the meantime, nothing is refunded and the code is returned.",
        "responses": {
          "200": { "description": "The canceled number.", "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/Number" }, { "type": "object", "properties": { "refunded": { "type": "boolean" } } }] } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "502": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/numbers/{id}/resend": {
      "parameters": [{ "$ref": "#/components/parameters/NumberId" }],
      "post": {
        "tags": ["Numbers"],
        "operationId": "resendSms",
        "summary": "Ask for another SMS on the same number, free, while it is open",
        "description": "Only after the first SMS arrived. Some carriers refuse it; you then get resend_refused.",
        "responses": {
          "200": { "description": "The number.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Number" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/numbers/{id}/finish": {
      "parameters": [{ "$ref": "#/components/parameters/NumberId" }],
      "post": {
        "tags": ["Numbers"],
        "operationId": "finishNumber",
        "summary": "Tell us you are done with a number that received its code",
        "description": "Optional, but it releases the number sooner.",
        "responses": {
          "200": { "description": "The number.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Number" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "webhooks": {
    "sms.received": {
      "post": {
        "summary": "An SMS arrived on a number you bought through the API",
        "description": "Sent to the HTTPS URL set on the API page of your dashboard, retried once after 3 seconds if your server fails. Verify the X-NoirSMS-Signature header: t=<unix time>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with your webhook secret>, and reject timestamps older than 5 minutes.",
        "parameters": [
          { "name": "X-NoirSMS-Signature", "in": "header", "required": true, "schema": { "type": "string", "example": "t=1760425563,v1=5f2b…" } }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SmsReceivedEvent" } } } },
        "responses": { "2XX": { "description": "Any 2xx status acknowledges the event." } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "description": "Authorization: Bearer nsk_live_YOUR_KEY. Generate the key on the API page of your dashboard." },
      "apiKeyHeader": { "type": "apiKey", "in": "header", "name": "X-API-Key" }
    },
    "parameters": {
      "NumberId": { "name": "id", "in": "path", "required": true, "description": "Number id returned by POST /numbers.", "schema": { "type": "string", "format": "uuid" } }
    },
    "responses": {
      "Error": {
        "description": "Error with a stable code you can switch on.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Balance": {
        "type": "object",
        "required": ["balance", "currency", "status"],
        "properties": {
          "balance": { "type": "number", "example": 24.37 },
          "currency": { "type": "string", "const": "USD" },
          "status": { "type": "string", "example": "active" }
        }
      },
      "CountryList": {
        "type": "object",
        "properties": {
          "server": { "type": "integer", "example": 1 },
          "countries": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "country": { "type": "string", "description": "2-letter ISO code.", "example": "us" },
                "services": { "type": "integer", "description": "Services in stock in this country.", "example": 264 }
              }
            }
          }
        }
      },
      "ServiceList": {
        "type": "object",
        "properties": {
          "server": { "type": "integer", "example": 1 },
          "country": { "type": "string", "example": "us" },
          "currency": { "type": "string", "const": "USD" },
          "services": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "service": { "type": "string", "description": "Service code to send when buying.", "example": "wa" },
                "name": { "type": "string", "example": "Whatsapp" },
                "price": { "type": "number", "description": "Price per number in USD.", "example": 0.73 },
                "stock": { "type": "integer", "example": 91562 }
              }
            }
          }
        }
      },
      "BuyRequest": {
        "type": "object",
        "required": ["service", "country"],
        "properties": {
          "service": { "type": "string", "description": "Service code from GET /services.", "example": "wa" },
          "country": { "type": "string", "description": "2-letter ISO code.", "example": "us" },
          "max_price": { "type": "number", "exclusiveMinimum": 0, "description": "Refuse the purchase (HTTP 409 price_above_max) if the current price is higher.", "example": 1.5 }
        }
      },
      "Sms": {
        "type": "object",
        "properties": {
          "code": { "type": ["string", "null"], "description": "The code we extracted from the text.", "example": "482915" },
          "text": { "type": "string", "example": "Your WhatsApp code: 482-915" },
          "received_at": { "type": "string", "format": "date-time" }
        }
      },
      "Number": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "phone": { "type": "string", "description": "Number in international format, without +.", "example": "12025550147" },
          "service": { "type": "string", "example": "wa" },
          "country": { "type": "string", "example": "us" },
          "server": { "type": "integer", "example": 1 },
          "price": { "type": "number", "example": 0.73 },
          "currency": { "type": "string", "const": "USD" },
          "status": { "type": "string", "enum": ["waiting", "received", "canceled", "expired"] },
          "created_at": { "type": "string", "format": "date-time" },
          "expires_at": { "type": "string", "format": "date-time", "description": "20 minutes after buying." },
          "cancelable_at": { "type": "string", "format": "date-time", "description": "2 minutes after buying." },
          "sms": { "type": "array", "items": { "$ref": "#/components/schemas/Sms" } }
        }
      },
      "NumberList": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Number" } },
          "has_more": { "type": "boolean" }
        }
      },
      "SmsReceivedEvent": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "sms.received" },
          "created_at": { "type": "string", "format": "date-time" },
          "data": {
            "type": "object",
            "properties": {
              "number_id": { "type": "string", "format": "uuid" },
              "phone": { "type": "string" },
              "service": { "type": "string" },
              "country": { "type": "string" },
              "status": { "type": "string" },
              "expires_at": { "type": "string", "format": "date-time" },
              "sms": { "$ref": "#/components/schemas/Sms" }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable error code.",
                "examples": ["invalid_api_key", "insufficient_balance", "price_above_max", "no_numbers", "too_early", "already_closed", "no_sms_yet", "resend_refused", "invalid_request", "invalid_json", "invalid_country", "invalid_status", "not_found", "account_suspended", "rate_limited", "provider_error", "restocking"]
              },
              "message": { "type": "string" },
              "needed": { "type": "number", "description": "With insufficient_balance: the price you need." },
              "waitSeconds": { "type": "integer", "description": "With too_early: seconds until you can cancel." }
            },
            "additionalProperties": true
          }
        }
      }
    }
  }
}
