{
  "openapi": "3.1.0",
  "info": {
    "title": "nothanumber API for AI agents: UK phone numbers, SMS and (planned) voice",
    "version": "1.0.0",
    "summary": "Send and receive SMS from an assigned UK phone number, with prepaid pay-as-you-go billing.",
    "description": "HTTP API for software and AI agents. LIVE: send and receive SMS, list your numbers and what each can do, check your prepaid balance. PLANNED (not in this file, returns 404 today): voice calls, an event stream, buying numbers by API. Access is invite-only: email support@nothanumber.com. Money is always an integer in minor units (pence for GBP). Numbers are E.164 (+447700900123). On a 402 the agent must stop and ask a human to top up; do not retry in a loop. Full guide for LLMs: https://nothanumber.com/developers/api.md",
    "contact": {
      "name": "nothanumber support",
      "email": "support@nothanumber.com",
      "url": "https://nothanumber.com/developers/"
    },
    "termsOfService": "https://nothanumber.com/terms.html"
  },
  "externalDocs": {
    "description": "Full API reference (also available as plain Markdown at /developers/api.md)",
    "url": "https://nothanumber.com/developers/api/"
  },
  "servers": [
    {
      "url": "https://app.nothanumber.com/api/v1/wholesale",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "SMS",
      "description": "Send and read text messages."
    },
    {
      "name": "Account",
      "description": "Your numbers and your prepaid balance."
    }
  ],
  "paths": {
    "/balance": {
      "get": {
        "operationId": "getBalance",
        "tags": [
          "Account"
        ],
        "summary": "Check your prepaid balance",
        "description": "Free and read-only. Call this first to confirm your key works. A 200 means authenticated. availableMinor is what you can still spend. Ask a human to top up when lowBalance is true.",
        "responses": {
          "200": {
            "description": "Your balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated (always the same vague message).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait Retry-After seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "API switched off or briefly unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/numbers": {
      "get": {
        "operationId": "listNumbers",
        "tags": [
          "Account"
        ],
        "summary": "List your senders and what each can do",
        "description": "Use one of these as `from` when sending. Numbers are two-way. Sender IDs are one-way names. `voice` is true only for numbers where voice calling has been enabled (calls themselves are not live yet).",
        "responses": {
          "200": {
            "description": "Your numbers and sender IDs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Numbers"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sms": {
      "post": {
        "operationId": "sendSms",
        "tags": [
          "SMS"
        ],
        "summary": "Send one SMS",
        "description": "`from` must be one of your senders (see /numbers). 1 to 3 segments: up to 459 plain characters or 201 with emoji. Charged per segment from your wallet when accepted. There is no idempotency key: if a request times out, look for the message with GET /sms before resending, or you may send and pay twice. Put a unique clientRef on every send.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendRequest"
              },
              "example": {
                "from": "+447537000002",
                "to": "+447700900123",
                "body": "Your code is 123456",
                "clientRef": "order-42"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid field, or longer than 3 segments. Fix the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Not enough available balance. STOP and ask a human to top up. Do not retry in a loop.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InsufficientFunds"
                }
              }
            }
          },
          "403": {
            "description": "`from` is not one of your senders. Call GET /numbers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No price is configured for that destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit, or another send from your account is in progress. Retry after the Retry-After header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Our SMS gateway is down. Nothing was sent or charged. Retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "API switched off or briefly unavailable. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listSms",
        "tags": [
          "SMS"
        ],
        "summary": "List messages, newest first (also how to catch up on replies and missed webhooks)",
        "parameters": [
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "inbound",
                "outbound"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "ISO 8601 date-time. Only messages sent at or after this.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Pass the previous response's nextCursor to get the next page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of messages",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sms/{id}": {
      "get": {
        "operationId": "getSms",
        "tags": [
          "SMS"
        ],
        "summary": "Read one message (poll this for delivery status)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/Message"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "nthk_live_<id>_<secret>",
        "description": "Your API key, shown once when it was created. Treat it like a password: keep it in an environment variable, never in code, logs or a URL. (Signed requests are an alternative; see the full reference.)"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "description": "A message, or field errors.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object"
              }
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "Balance": {
        "type": "object",
        "required": [
          "balanceMinor",
          "creditLimitMinor",
          "availableMinor",
          "currency",
          "lowBalance",
          "lowBalanceThresholdMinor",
          "topUpUrl"
        ],
        "properties": {
          "balanceMinor": {
            "type": "integer",
            "description": "Your balance in minor units (pence). Negative means you owe money (only possible with a credit limit)."
          },
          "creditLimitMinor": {
            "type": "integer",
            "description": "How far below zero you may go. Normally 0 (you pre-pay)."
          },
          "availableMinor": {
            "type": "integer",
            "description": "balanceMinor + creditLimitMinor: what you can still spend."
          },
          "currency": {
            "type": "string",
            "example": "GBP"
          },
          "lowBalance": {
            "type": "boolean",
            "description": "True when availableMinor is at or below lowBalanceThresholdMinor. Ask a human to top up soon."
          },
          "lowBalanceThresholdMinor": {
            "type": "integer",
            "example": 500
          },
          "topUpUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "A link to add money, or null if there is none yet (then ask the administrator)."
          }
        }
      },
      "Numbers": {
        "type": "object",
        "required": [
          "numbers",
          "senderIds"
        ],
        "properties": {
          "numbers": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "e164",
                "countryCode",
                "twoWay",
                "sms",
                "voice"
              ],
              "properties": {
                "e164": {
                  "type": "string",
                  "example": "+447537000002"
                },
                "countryCode": {
                  "type": "string",
                  "example": "GB"
                },
                "twoWay": {
                  "type": "boolean",
                  "const": true
                },
                "sms": {
                  "type": "boolean",
                  "const": true
                },
                "voice": {
                  "type": "boolean",
                  "description": "True when voice calling is enabled on this number. Calls can only ever be made from a number assigned to your account with voice true."
                }
              }
            }
          },
          "senderIds": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "senderId",
                "twoWay",
                "sms",
                "voice"
              ],
              "properties": {
                "senderId": {
                  "type": "string",
                  "example": "ACME"
                },
                "twoWay": {
                  "type": "boolean",
                  "const": false
                },
                "sms": {
                  "type": "boolean",
                  "const": true
                },
                "voice": {
                  "type": "boolean",
                  "const": false
                }
              }
            }
          }
        }
      },
      "SendRequest": {
        "type": "object",
        "required": [
          "from",
          "to",
          "body"
        ],
        "properties": {
          "from": {
            "type": "string",
            "description": "One of your senders from GET /numbers.",
            "maxLength": 16
          },
          "to": {
            "type": "string",
            "description": "International E.164 number. Premium-rate and special-service numbers are refused.",
            "pattern": "^\\+[1-9][0-9]{6,14}$"
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 459,
            "description": "1 to 3 SMS segments: up to 459 plain (GSM-7) characters, or 201 if the text contains emoji or other non-GSM characters. Longer is refused with 400."
          },
          "clientRef": {
            "type": "string",
            "maxLength": 100,
            "description": "Your own label, echoed on the message. Not an idempotency key."
          }
        }
      },
      "SendResponse": {
        "type": "object",
        "required": [
          "id",
          "status",
          "from",
          "to",
          "segments",
          "encoding",
          "priceMinor",
          "currency",
          "availableMinor",
          "lowBalance"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "SENT",
              "DELIVERED"
            ]
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "segments": {
            "type": "integer",
            "minimum": 1,
            "maximum": 3
          },
          "encoding": {
            "type": "string",
            "enum": [
              "GSM7",
              "UCS2"
            ]
          },
          "priceMinor": {
            "type": "integer",
            "description": "What this message cost, in minor units."
          },
          "currency": {
            "type": "string"
          },
          "availableMinor": {
            "type": "integer",
            "description": "What you can still spend after this message."
          },
          "lowBalance": {
            "type": "boolean",
            "description": "True when availableMinor is low. Ask a human to top up soon."
          }
        }
      },
      "InsufficientFunds": {
        "type": "object",
        "required": [
          "error",
          "code",
          "retryable"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "Credit limit reached"
          },
          "code": {
            "type": "string",
            "enum": [
              "wallet_empty",
              "credit_limit_reached"
            ]
          },
          "retryable": {
            "type": "boolean",
            "const": false
          },
          "balanceMinor": {
            "type": "integer"
          },
          "creditLimitMinor": {
            "type": "integer"
          },
          "availableMinor": {
            "type": "integer"
          },
          "requiredMinor": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "topUpUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "nextStep": {
            "type": "string",
            "description": "What to do. In short: stop, ask a human to add money, do not retry in a loop."
          }
        }
      },
      "Message": {
        "type": "object",
        "required": [
          "id",
          "direction",
          "from",
          "to",
          "body",
          "status",
          "segments",
          "encoding",
          "priceMinor",
          "currency",
          "clientRef",
          "errorCode",
          "sentAt",
          "deliveredAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "QUEUED",
              "SENT",
              "DELIVERED",
              "FAILED",
              "RECEIVED"
            ]
          },
          "segments": {
            "type": "integer"
          },
          "encoding": {
            "type": "string",
            "enum": [
              "GSM7",
              "UCS2"
            ]
          },
          "priceMinor": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "clientRef": {
            "type": [
              "string",
              "null"
            ]
          },
          "errorCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "sentAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the message was sent. For an inbound message this is when we received it."
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "MessageList": {
        "type": "object",
        "required": [
          "messages",
          "nextCursor"
        ],
        "properties": {
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Message"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page; null means no more."
          }
        }
      }
    }
  }
}
