{
  "openapi": "3.1.1",
  "info": {
    "title": "readynum API",
    "version": "0.1.0-draft",
    "summary": "The contract, published before the endpoints.",
    "description": "The readynum REST API is **not callable yet**. This document is the contract we are building to, published early so it can be reviewed, generated against and argued with while changing it is still cheap.\n\nEvery operation carries `x-implemented: false`. Every example value in this document is illustrative. The field shapes are the contract; prices, rates, numbers and timestamps are placeholders, not measurements.\n\nThree rules the response shapes enforce:\n\n1. A success figure is never a bare number — `success_rate` is state-tagged (`measured` with its sample size and window, `supplier_reported` with the supplier named, or `no_data`). Where the state is `no_data`, show your users the words, not a dash; ours are \"We don't have data for this route yet\".\n2. Stock is binary. There is no count field and there never will be.\n3. Refunds are a field on the order and a webhook event, not a support ticket.\n\nNo base URL is published here: printing one would imply a server that answers. It ships with the endpoints.",
    "contact": {
      "name": "readynum — API contract feedback",
      "url": "https://readynum.com/contact?intent=api-review"
    }
  },
  "externalDocs": {
    "description": "The same contract, in prose.",
    "url": "https://readynum.com/api-docs"
  },
  "x-implemented": false,
  "x-contract-status": "draft — specified, not callable",
  "tags": [
    {
      "name": "Prices",
      "description": "Public route prices, stock and state."
    },
    {
      "name": "Orders",
      "description": "Rent, poll, cancel, refund."
    },
    {
      "name": "Webhooks",
      "description": "Events we push to you."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/prices": {
      "get": {
        "operationId": "getPrices",
        "summary": "Price, stock and route state for one route",
        "description": "One call per route: the price, binary stock, and the success-rate object in whichever of its three states is true. No key required — public prices are the point. At launch `no_data` is the majority state, and the API says so rather than inventing a number.\n\nNot callable yet. This operation is specified, not implemented: the API ships with live ordering, and until then requests to it will not be answered.",
        "tags": [
          "Prices"
        ],
        "security": [],
        "parameters": [
          {
            "name": "service",
            "in": "query",
            "required": true,
            "description": "Service slug, e.g. `whatsapp`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": true,
            "description": "ISO 3166-1 alpha-2 country code, lower case.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The route, in whichever data state is true for it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoutePrice"
                },
                "example": {
                  "service": "whatsapp",
                  "country": "us",
                  "price": {
                    "amount": "0.45",
                    "currency": "USD"
                  },
                  "stock": true,
                  "success_rate": {
                    "state": "no_data"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such service × country route.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-implemented": false
      }
    },
    "/v1/orders": {
      "post": {
        "operationId": "createOrder",
        "summary": "Rent a number — quote before buy",
        "description": "Send back the price you saw as `max_price`. If the route's current price is higher when the order lands, we reject it and charge nothing: the quote you saw is the most you can ever pay. Retries reuse the same `Idempotency-Key`, so a network timeout cannot double-charge.\n\nNot callable yet. This operation is specified, not implemented: the API ships with live ordering, and until then requests to it will not be answered.",
        "tags": [
          "Orders"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Client-generated UUID. Replaying it returns the original order instead of creating a second one.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderRequest"
              },
              "example": {
                "service": "whatsapp",
                "country": "us",
                "max_price": "0.45"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order created; the number is listening.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": "ord_3f8a1c",
                  "state": "waiting_for_sms",
                  "service": "whatsapp",
                  "country": "us",
                  "number": "+12025550123",
                  "price": {
                    "amount": "0.45",
                    "currency": "USD"
                  },
                  "expires_at": "2026-07-28T12:20:00Z",
                  "refund": null
                }
              }
            }
          },
          "409": {
            "description": "`max_price` guardrail: the current price is above your ceiling. No order was created and nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "max_price_exceeded",
                  "message": "The current price is above your max_price. No order was created and nothing was charged.",
                  "current_price": {
                    "amount": "0.52",
                    "currency": "USD"
                  }
                }
              }
            }
          }
        },
        "x-implemented": false
      }
    },
    "/v1/orders/{id}": {
      "get": {
        "operationId": "getOrder",
        "summary": "Poll status and read received SMS",
        "description": "The order carries its lifecycle state and every received message, with the verification code extracted. Message bodies auto-delete after 24 hours — read codes promptly, or use webhooks and skip polling.\n\nNot callable yet. This operation is specified, not implemented: the API ships with live ordering, and until then requests to it will not be answered.",
        "tags": [
          "Orders"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Order id returned by `createOrder`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The order in its current state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": "ord_3f8a1c",
                  "state": "code_received",
                  "number": "+12025550123",
                  "sms": [
                    {
                      "received_at": "2026-07-28T12:06:41Z",
                      "sender": "WhatsApp",
                      "body": "Your WhatsApp code is 123-456",
                      "code": "123456"
                    }
                  ],
                  "refund": null
                }
              }
            }
          },
          "404": {
            "description": "No such order for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-implemented": false
      }
    },
    "/v1/orders/{id}/cancel": {
      "post": {
        "operationId": "cancelOrder",
        "summary": "Cancel and get refunded",
        "description": "Cancel any time before a code arrives; the refund is in the response, credited to the wallet, not opened as a ticket. Orders that already received a code cannot be cancelled — the service was delivered.\n\nNot callable yet. This operation is specified, not implemented: the API ships with live ordering, and until then requests to it will not be answered.",
        "tags": [
          "Orders"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Order id to cancel.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled and refunded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": "ord_3f8a1c",
                  "state": "refunded",
                  "refund": {
                    "reason": "cancelled_by_user",
                    "amount": {
                      "amount": "0.45",
                      "currency": "USD"
                    },
                    "credited_to": "wallet",
                    "credited_at": "2026-07-28T12:09:12Z"
                  }
                }
              }
            }
          },
          "409": {
            "description": "A code already arrived, so the order cannot be cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-implemented": false
      }
    }
  },
  "webhooks": {
    "code.received": {
      "post": {
        "operationId": "oncodereceived",
        "summary": "A verification code arrived",
        "description": "Pushed as soon as a message lands on one of your orders, with the code extracted. Deliveries are signed with an HMAC header — verify before trusting — and retried with backoff, so handle events idempotently by `id`.\n\nNot callable yet. This operation is specified, not implemented: the API ships with live ordering, and until then requests to it will not be answered.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              },
              "example": {
                "id": "evt_7c2d91",
                "type": "code.received",
                "created_at": "2026-07-28T12:06:41Z",
                "data": {
                  "order_id": "ord_3f8a1c",
                  "service": "whatsapp",
                  "country": "us",
                  "code": "123456",
                  "received_at": "2026-07-28T12:06:41Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return 2xx to acknowledge. Anything else is retried with backoff."
          }
        },
        "x-implemented": false
      }
    },
    "order.refunded": {
      "post": {
        "operationId": "onorderrefunded",
        "summary": "An order was refunded",
        "description": "Pushed when an order is refunded, whether because no valid code arrived or because you cancelled it. The reason says which.\n\nNot callable yet. This operation is specified, not implemented: the API ships with live ordering, and until then requests to it will not be answered.",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              },
              "example": {
                "id": "evt_a41f08",
                "type": "order.refunded",
                "created_at": "2026-07-28T12:25:03Z",
                "data": {
                  "order_id": "ord_9b2e7d",
                  "reason": "no_code_received",
                  "amount": {
                    "amount": "0.45",
                    "currency": "USD"
                  },
                  "credited_to": "wallet"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return 2xx to acknowledge. Anything else is retried with backoff."
          }
        },
        "x-implemented": false
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer key from your account. Issued at launch; `getPrices` needs none at all."
      }
    },
    "schemas": {
      "Price": {
        "type": "object",
        "description": "Money is a decimal STRING plus a currency — never a float.",
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "examples": [
              "0.45"
            ]
          },
          "currency": {
            "type": "string",
            "examples": [
              "USD"
            ]
          }
        }
      },
      "NoDataRate": {
        "type": "object",
        "description": "No order history and no usable supplier figure — the launch-day default for most routes. Render the sentence, never a dash, \"N/A\" or 0%: ours is \"We don't have data for this route yet\".",
        "required": [
          "state"
        ],
        "properties": {
          "state": {
            "const": "no_data"
          }
        }
      },
      "MeasuredRate": {
        "type": "object",
        "description": "Measured from OUR OWN orders. `n` and `window` are mandatory: a value that renders without them misrepresents the source. Example values are illustrative.",
        "required": [
          "state",
          "value",
          "n",
          "window"
        ],
        "properties": {
          "state": {
            "const": "measured"
          },
          "value": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Whole percent."
          },
          "n": {
            "type": "integer",
            "minimum": 1,
            "description": "Sample size. Mandatory."
          },
          "window": {
            "type": "string",
            "description": "Measurement window, e.g. `24h`. Mandatory.",
            "examples": [
              "24h"
            ]
          }
        }
      },
      "SupplierReportedRate": {
        "type": "object",
        "description": "A named supplier's published figure, passed through — supplier figure, not our measurement. Never colour-code it and never sort on it. Supplier default buckets (30, 65, 100) are placeholders and are returned as `no_data` instead.",
        "required": [
          "state",
          "value",
          "source",
          "as_of"
        ],
        "properties": {
          "state": {
            "const": "supplier_reported"
          },
          "value": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "source": {
            "type": "string",
            "description": "Supplier name, unabbreviated. Mandatory.",
            "examples": [
              "smspool"
            ]
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "When the supplier reading was taken. Mandatory."
          }
        }
      },
      "SuccessRate": {
        "description": "State-tagged union. There is no shape in this API that returns a bare percentage.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/NoDataRate"
          },
          {
            "$ref": "#/components/schemas/MeasuredRate"
          },
          {
            "$ref": "#/components/schemas/SupplierReportedRate"
          }
        ],
        "discriminator": {
          "propertyName": "state",
          "mapping": {
            "no_data": "#/components/schemas/NoDataRate",
            "measured": "#/components/schemas/MeasuredRate",
            "supplier_reported": "#/components/schemas/SupplierReportedRate"
          }
        }
      },
      "RoutePrice": {
        "type": "object",
        "required": [
          "service",
          "country",
          "price",
          "stock",
          "success_rate"
        ],
        "properties": {
          "service": {
            "type": "string",
            "examples": [
              "whatsapp"
            ]
          },
          "country": {
            "type": "string",
            "examples": [
              "us"
            ]
          },
          "price": {
            "$ref": "#/components/schemas/Price"
          },
          "stock": {
            "type": "boolean",
            "description": "Binary. `true` means buyable right now, `false` means it is not. There is no count field — not ours, not a supplier's passed through."
          },
          "success_rate": {
            "$ref": "#/components/schemas/SuccessRate"
          }
        }
      },
      "OrderRequest": {
        "type": "object",
        "required": [
          "service",
          "country",
          "max_price"
        ],
        "properties": {
          "service": {
            "type": "string",
            "examples": [
              "whatsapp"
            ]
          },
          "country": {
            "type": "string",
            "examples": [
              "us"
            ]
          },
          "max_price": {
            "type": "string",
            "description": "The price you were quoted. If the route costs more when the order lands, the order is rejected and nothing is charged.",
            "examples": [
              "0.45"
            ]
          }
        }
      },
      "Sms": {
        "type": "object",
        "description": "A received message. Bodies auto-delete after 24 hours (docs/07 §10.1).",
        "required": [
          "received_at",
          "sender",
          "body"
        ],
        "properties": {
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "sender": {
            "type": "string",
            "examples": [
              "WhatsApp"
            ]
          },
          "body": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "The extracted verification code, when we found one."
          }
        }
      },
      "Refund": {
        "type": "object",
        "description": "If a code doesn't arrive, you don't pay. We auto-refund your wallet within 5 minutes when a number fails to receive a valid code. No form, no waiting. Refunds go to your readynum balance instantly and never expire. Prefer a card refund? Ask support and we'll send it back the way you paid within 5 business days.",
        "required": [
          "reason",
          "amount",
          "credited_to",
          "credited_at"
        ],
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "no_code_received",
              "cancelled_by_user"
            ]
          },
          "amount": {
            "$ref": "#/components/schemas/Price"
          },
          "credited_to": {
            "type": "string",
            "enum": [
              "wallet",
              "card"
            ]
          },
          "credited_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Order": {
        "type": "object",
        "required": [
          "id",
          "state"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "ord_3f8a1c"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "waiting_for_sms",
              "code_received",
              "refunded"
            ],
            "description": "`waiting_for_sms` — number issued, listening. `code_received` — at least one message arrived. `refunded` — no valid code, or you cancelled."
          },
          "service": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "number": {
            "type": "string",
            "examples": [
              "+12025550123"
            ]
          },
          "price": {
            "$ref": "#/components/schemas/Price"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "sms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Sms"
            }
          },
          "refund": {
            "description": "Null until the order is refunded.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Refund"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable event id. Deliveries are retried, so handle events idempotently by this.",
            "examples": [
              "evt_7c2d91"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "code.received",
              "order.refunded"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable code.",
            "examples": [
              "max_price_exceeded"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation, safe to surface."
          },
          "current_price": {
            "$ref": "#/components/schemas/Price"
          }
        }
      }
    }
  }
}