{
  "openapi": "3.1.0",
  "info": {
    "title": "Proxylexus API",
    "version": "1.0.0",
    "summary": "Proxy users, targeting and traffic balance, for automations.",
    "description": "The Proxylexus API. One key authenticates one account; every response is scoped to it, and\nno request may name an account, user or customer.\n\n## Authentication\n\nSend `Authorization: Bearer <key>`. The header is the only accepted location — a key in a\nquery string is refused with `credential_in_url`, valid or not, because by then it is in an\naccess log. Keys are created and revoked in the dashboard, are shown exactly once, and are\nstored only as a digest. Revoking one stops it on the very next request.\n\n## What this API will not do\n\n- **Spend money.** No endpoint buys traffic, changes a plan or touches a price. A key found in a leaked build log cannot charge a card, and there is no confirm-flag that unlocks it.\n- **Create or revoke API keys.** Key management is in the dashboard, behind your password. A key that could mint a key could not be revoked — you cut the first one and the second, minted moments earlier, keeps working. That is also why the ceiling is 10: whatever happens, every key is on one screen.\n- **Change the account.** No password change, no closure, no export. Those are the operations that make a password worth stealing, and a key is not a password with better ergonomics.\n- **Read back a secret.** Neither a proxy user’s secret nor an API key. Both are stored as digests only. No endpoint can return one, and no future endpoint can be added that does.\n- **Attribute usage per proxy user.** The meter counts bytes for the account, not per credential. There is no per-user or per-key byte figure anywhere, so rather than estimate one we do not offer it.\n- **Be called from a browser.** No CORS headers and no preflight handler, on purpose. An API key belongs on a server, and refusing the browser is a small push away from pasting one into front-end JavaScript.\n\n## Limits\n\n- **Destinations are limited to ports 80, 443, 8080, 8443.** Anything else is refused at the gateway, on both the HTTP and the SOCKS5 path. Without that rule the product is also an open mail relay and an SSH jump host, and the complaints land on the addresses every customer shares.\n- **There is no https:// proxy scheme.** As a proxy scheme, https:// means TLS to the proxy itself, and there is no TLS listener to speak it to. The protocol parameter still accepts the value, because it mirrors the grammar the network behind us publishes — so it is possible to build a line that has nothing to connect to. Use http:// or socks5://, both on the same port. None of this touches your destinations: https:// URLs are the normal case and ride a CONNECT tunnel over the http:// line, encrypted end to end. We cannot read them and do not want to.\n- **UDP is refused.** A SOCKS5 client asking for UDP ASSOCIATE, or for BIND, gets \"command not supported\" at the handshake. The path to the network is a TCP tunnel, so no answer at that handshake could make UDP work — and claiming it and then dropping the datagrams is worse than saying no.\n- **A proxy user’s secret is shown once.** It exists in the response that created it and nowhere else afterwards. We keep a SHA-256 digest, which is enough to check a connection and not enough to reconstruct anything. No endpoint can return it, support cannot recover it, and a dump of our database does not hand anyone working proxy access.\n- **A top-up is not instant at the gateway.** The gateway caches an account’s balance for about fifteen seconds. After a purchase, connections may be refused for a few seconds more before the new balance is seen. Nothing is lost and nothing needs reissuing — wait, then retry.\n- **Revoking a proxy user takes up to 30 seconds.** The gateway authenticates every connection and therefore holds a credential that long before asking the database again, and it never cuts a connection already open. The DELETE response returns the figure as stopsServingWithinSeconds so an automation can compare it to a clock rather than to an adverb. Revoking an API KEY is a different promise: nothing caches that table, so the very next API call with it fails.\n- **Country is required, and city and state are exclusive.** Every connection names a country. Then a city or a state, never both — sending both is a request whose meaning depends on which one a parser reads last, so it is refused instead. Send the value the location catalogue gives you rather than a name you typed: names are letters, digits and underscores, spaces become underscores, and a place carrying an accent, a dash or an apostrophe cannot be expressed in a proxy username at all. Those are left out of the catalogue and counted, so you can see what is missing instead of guessing.\n- **A deleted proxy user keeps its name for ever.** Labels are unique across every proxy user an account has ever created, deleted ones included. That is deliberate — it is what keeps \"scraper_prod spent 40 GB\" unambiguous a year later — and it means a create-and-delete loop burns names permanently. Label by run rather than by role: scraper_prod_2026_09 can be created again, scraper_prod cannot.\n- **Usage is account-wide, and today has not settled.** usedBytes moves as traffic passes; billableBytes counts complete days only, so the two differ by however much of today has happened. Reconcile against billableBytes. Neither figure splits by proxy user, country or destination — the meter records bytes and a timestamp, and our privacy policy commits to not keeping the addresses you connect to.\n- **One account, many connections, one ceiling.** Up to 50 live proxy users and 10 live API keys per account, neither of them charged for. The gateway also caps how many connections one account may hold open at once, because every proxy user is another parallel channel spending the same balance.\n\n## Rate limits\n\nCounted per key, not per address. Reads 300/min, writes\n30/min, the location catalogue 30/min. Failed\nauthentications are counted per source address instead — 30 in\n15 minutes — and the count is cleared the moment a key works.\n`RateLimit-Reset` and `Retry-After` are SECONDS FROM NOW, never an epoch timestamp.\n\n## Versioning\n\nFor the life of v1: a field keeps its name, type and meaning; `code` values keep their\nmeaning; paths, methods and success statuses do not move; auth stays bearer. New fields, new\n`code` values, new endpoints and new optional parameters WILL be added — ignore what you do\nnot recognise, and fall back on the HTTP status for a code you have not seen. The `error`\nstring is for humans and may be reworded at any time; never branch on it.",
    "contact": {
      "name": "Proxylexus support",
      "email": "support@proxylexus.com",
      "url": "https://proxylexus.com/contact"
    }
  },
  "servers": [
    {
      "url": "https://proxylexus.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "The balance and the meter."
    },
    {
      "name": "Proxy users",
      "description": "The credentials your jobs connect with."
    },
    {
      "name": "Catalogue",
      "description": "What is stocked, and where."
    }
  ],
  "paths": {
    "/account": {
      "get": {
        "operationId": "getAccount",
        "summary": "Read the account behind the key",
        "description": "The first call to make: it confirms the key works and tells you what it is attached to. Reading a balance is the one money-adjacent thing this API does, and it only reads.\n\nThere is no account id in the request and there is nowhere to put one. The key identifies exactly one account, every response is scoped to it, and a parameter that tried to say otherwise is refused with caller_supplied_identity.\n\nRate limit: 300 requests per 60 seconds, counted per API key.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Read the account behind the key",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "planId",
                    "status",
                    "periodStart",
                    "quotaBytes",
                    "remainingBytes"
                  ],
                  "properties": {
                    "planId": {
                      "description": "The bundle the most recent purchase came from. Informational — the money is the byte count.",
                      "type": "string"
                    },
                    "status": {
                      "description": "Always \"active\" here — a suspended or cancelled account leaves by the 409 below, which carries a status field of the same name. Read body.status on either path and you never have to know which one you are on.",
                      "type": "string",
                      "enum": [
                        "active"
                      ]
                    },
                    "periodStart": {
                      "description": "Where the meter counts from. A purchase never moves it.",
                      "type": "string",
                      "format": "date-time"
                    },
                    "quotaBytes": {
                      "description": "Bytes bought and not yet spent, at periodStart.",
                      "type": "integer"
                    },
                    "remainingBytes": {
                      "description": "What is left right now. This is the number to alert on.",
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "planId": "standard",
                  "status": "active",
                  "periodStart": "2026-08-14T09:12:44.108Z",
                  "quotaBytes": 53687091200,
                  "remainingBytes": 40265318400
                }
              }
            }
          },
          "400": {
            "description": "`credential_in_url` — A key appeared in the query string. Refused whether or not it was valid — by the time we see it, it is already in an access log. Move it to the header and revoke the key you sent.\n\n`caller_supplied_identity` — The request tried to name an account, user or customer. Never honoured: your key decides whose data comes back, and refusing loudly beats ignoring it quietly.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "credential_in_url": {
                    "summary": "credential_in_url",
                    "value": {
                      "error": "An API key must never appear in a URL — it is written to every access log it passes through. Send it as `Authorization: Bearer <key>` instead, and revoke the key you just sent: treat it as leaked.",
                      "code": "credential_in_url"
                    }
                  },
                  "caller_supplied_identity": {
                    "summary": "caller_supplied_identity",
                    "value": {
                      "error": "This API never accepts \"account_id\". Your key already identifies the account, and every response is scoped to it.",
                      "code": "caller_supplied_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "`not_provisioned` — The account has never bought traffic, so there is no balance to report.\n\n`account_suspended` — The subscription is suspended or cancelled. Carries status, the same field the 200 carries.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_provisioned": {
                    "summary": "not_provisioned",
                    "value": {
                      "error": "This account has never bought traffic. Buy gigabytes in the dashboard first.",
                      "code": "not_provisioned"
                    }
                  },
                  "account_suspended": {
                    "summary": "account_suspended",
                    "value": {
                      "error": "This account is suspended.",
                      "code": "account_suspended"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "description": "`unavailable` — The balance could not be read. We will not print a figure we could not take, and we will not send null either — an automation comparing null to a threshold pages a human during OUR outage.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "unavailable",
                    "value": {
                      "error": "Your balance cannot be read right now. The account is unaffected and the balance is unchanged — retry shortly.",
                      "code": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Read the meter",
        "description": "Bytes sent plus bytes received, counted on our own gateway as your connections pass through it. This is the endpoint to build an alert on.\n\nWhen the meter cannot be read we answer 503 with the code unavailable — never a reading of zero. The distinction is the whole point of the branch: internally a reading we cannot take is treated as exhausted, because anything about to spend money should refuse on doubt. Printed on a dashboard, or fed to an automation that pages somebody, that same value is a lie about your balance. So it never leaves this endpoint as a number.\n\nRate limit: 300 requests per 60 seconds, counted per API key.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Read the meter",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "state",
                    "usedBytes",
                    "quotaBytes",
                    "remainingBytes",
                    "fractionUsed",
                    "billableBytes",
                    "estimated"
                  ],
                  "properties": {
                    "state": {
                      "description": "warn at 80% spent; exhausted means connections are no longer served.",
                      "type": "string",
                      "enum": [
                        "ok",
                        "warn",
                        "exhausted"
                      ]
                    },
                    "usedBytes": {
                      "description": "Spent since periodStart.",
                      "type": "integer"
                    },
                    "quotaBytes": {
                      "description": "Bought.",
                      "type": "integer"
                    },
                    "remainingBytes": {
                      "description": "quotaBytes minus usedBytes, floored at zero.",
                      "type": "integer"
                    },
                    "fractionUsed": {
                      "description": "usedBytes over quotaBytes, 0 to 1.",
                      "type": "number"
                    },
                    "billableBytes": {
                      "description": "The settled portion — complete days only. Lower than usedBytes by however much of today has not settled.",
                      "type": "integer"
                    },
                    "estimated": {
                      "description": "True when the figure rests on an estimate rather than on attributable data. Do not reconcile an invoice against a true value.",
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "state": "ok",
                  "usedBytes": 13421772800,
                  "quotaBytes": 53687091200,
                  "remainingBytes": 40265318400,
                  "fractionUsed": 0.25,
                  "billableBytes": 12884901888,
                  "estimated": false
                }
              }
            }
          },
          "400": {
            "description": "`credential_in_url` — A key appeared in the query string. Refused whether or not it was valid — by the time we see it, it is already in an access log. Move it to the header and revoke the key you sent.\n\n`caller_supplied_identity` — The request tried to name an account, user or customer. Never honoured: your key decides whose data comes back, and refusing loudly beats ignoring it quietly.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "credential_in_url": {
                    "summary": "credential_in_url",
                    "value": {
                      "error": "An API key must never appear in a URL — it is written to every access log it passes through. Send it as `Authorization: Bearer <key>` instead, and revoke the key you just sent: treat it as leaked.",
                      "code": "credential_in_url"
                    }
                  },
                  "caller_supplied_identity": {
                    "summary": "caller_supplied_identity",
                    "value": {
                      "error": "This API never accepts \"account_id\". Your key already identifies the account, and every response is scoped to it.",
                      "code": "caller_supplied_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "`not_provisioned` — Nothing bought yet, so nothing to meter.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_provisioned": {
                    "summary": "not_provisioned",
                    "value": {
                      "error": "This account has never bought traffic. Buy gigabytes in the dashboard first.",
                      "code": "not_provisioned"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "description": "`unavailable` — The meter could not be read. Your balance is unchanged and nothing has been spent — retry, and do not alert on it as an exhausted account. The body carries quotaBytes, which is known even when the meter is not.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "unavailable",
                    "value": {
                      "error": "Usage cannot be read right now. Your balance is unchanged — retry shortly. This is our outage, not an exhausted account: do not alert on it as one.",
                      "code": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxy-users": {
      "get": {
        "operationId": "listProxyUsers",
        "summary": "List your proxy users",
        "description": "Your automation’s inventory: every live proxy user on the account, oldest first. Revoked ones are not listed.\n\nNo secret is returned, and none can be. We store only a SHA-256 digest of it, so after the response that issued it there is nothing on our side to return. That is a property of the design rather than a gap in this endpoint — a secret we could read back is a secret a database dump would hand to somebody else.\n\nRate limit: 300 requests per 60 seconds, counted per API key.",
        "tags": [
          "Proxy users"
        ],
        "responses": {
          "200": {
            "description": "List your proxy users",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "properties": {
                    "items": {
                      "description": "Live proxy users, oldest first.",
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "label",
                          "username",
                          "createdAt",
                          "lastUsedAt"
                        ],
                        "properties": {
                          "id": {
                            "description": "Identifies the proxy user in the paths that take one.",
                            "type": "integer"
                          },
                          "label": {
                            "description": "The name you gave it.",
                            "type": "string"
                          },
                          "username": {
                            "description": "The credential half of the proxy username. Targeting is appended to it.",
                            "type": "string"
                          },
                          "createdAt": {
                            "description": "When it was issued.",
                            "type": "string",
                            "format": "date-time"
                          },
                          "lastUsedAt": {
                            "description": "When the gateway last authenticated it. Null until it is first used, and written at most once a minute.",
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": 41,
                      "label": "scraper_prod",
                      "username": "8f3c9a1d4b7e2f6a5c0d9e81",
                      "createdAt": "2026-09-01T08:31:02.884Z",
                      "lastUsedAt": "2026-09-01T09:04:55.210Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`credential_in_url` — A key appeared in the query string. Refused whether or not it was valid — by the time we see it, it is already in an access log. Move it to the header and revoke the key you sent.\n\n`caller_supplied_identity` — The request tried to name an account, user or customer. Never honoured: your key decides whose data comes back, and refusing loudly beats ignoring it quietly.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "credential_in_url": {
                    "summary": "credential_in_url",
                    "value": {
                      "error": "An API key must never appear in a URL — it is written to every access log it passes through. Send it as `Authorization: Bearer <key>` instead, and revoke the key you just sent: treat it as leaked.",
                      "code": "credential_in_url"
                    }
                  },
                  "caller_supplied_identity": {
                    "summary": "caller_supplied_identity",
                    "value": {
                      "error": "This API never accepts \"account_id\". Your key already identifies the account, and every response is scoped to it.",
                      "code": "caller_supplied_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "description": "`unavailable` — We could not reach something we depend on. Back off and try again.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "unavailable",
                    "value": {
                      "error": "We could not reach something we depend on. Back off and try again.",
                      "code": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProxyUser",
        "summary": "Create a proxy user",
        "description": "Issues a credential against our gateway. This response is the only moment the secret exists in readable form anywhere in our system — store it before you do anything else. If you lose it, the answer is to create another and revoke this one, and that is the correct answer rather than a weakness.\n\nCreating one spends nothing and reserves nothing, but it does open a channel that can spend, so it is the one endpoint gated on your balance: at zero it is refused with insufficient_balance while everything else on this page keeps working. The ceiling is 50 live proxy users per account.\n\nA label may use letters, numbers, spaces and underscores. No dashes — the proxy username is dash-delimited, and one character class for both means the two can never drift into disagreeing about what is safe. An unknown field in the body is refused rather than ignored.\n\nA label is taken for ever, not until you delete it. The uniqueness index deliberately covers deleted proxy users too, so that \"scraper_prod spent 40 GB\" is still unambiguous a year later — which means a create/delete loop burns names permanently. Label by run, not by role: scraper_prod_2026_09 can be re-created, scraper_prod cannot.\n\nRate limit: 30 requests per 60 seconds, counted per API key.",
        "tags": [
          "Proxy users"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "label"
                ],
                "properties": {
                  "label": {
                    "description": "1 to 40 characters: letters, numbers, spaces and underscores, no dashes. Unique across every proxy user this account has ever created, deleted ones included — a name is not released by deleting it.",
                    "type": "string"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "label": "scraper_prod"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Create a proxy user",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "label",
                    "username",
                    "createdAt",
                    "lastUsedAt",
                    "secret"
                  ],
                  "properties": {
                    "id": {
                      "description": "Identifies the proxy user in the paths that take one.",
                      "type": "integer"
                    },
                    "label": {
                      "description": "The name you gave it.",
                      "type": "string"
                    },
                    "username": {
                      "description": "The credential half of the proxy username. Targeting is appended to it.",
                      "type": "string"
                    },
                    "createdAt": {
                      "description": "When it was issued.",
                      "type": "string",
                      "format": "date-time"
                    },
                    "lastUsedAt": {
                      "description": "When the gateway last authenticated it. Null until it is first used, and written at most once a minute.",
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "secret": {
                      "description": "Returned here and never again. Not stored in readable form, not shown in the dashboard, not recoverable by support.",
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": 41,
                  "label": "scraper_prod",
                  "username": "8f3c9a1d4b7e2f6a5c0d9e81",
                  "createdAt": "2026-09-01T08:31:02.884Z",
                  "lastUsedAt": null,
                  "secret": "u7Qm2XvL9pRt4Kd0YbN6cWzA1sJfH3eG"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — The body was not a JSON object, the label is missing or malformed, or an unknown field was sent. Carries field.\n\n`credential_in_url` — A key appeared in the query string. Refused whether or not it was valid — by the time we see it, it is already in an access log. Move it to the header and revoke the key you sent.\n\n`caller_supplied_identity` — The request tried to name an account, user or customer. Never honoured: your key decides whose data comes back, and refusing loudly beats ignoring it quietly.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "invalid_request",
                    "value": {
                      "error": "The body was not a JSON object, the label is missing or malformed, or an unknown field was sent. Carries field.",
                      "code": "invalid_request"
                    }
                  },
                  "credential_in_url": {
                    "summary": "credential_in_url",
                    "value": {
                      "error": "An API key must never appear in a URL — it is written to every access log it passes through. Send it as `Authorization: Bearer <key>` instead, and revoke the key you just sent: treat it as leaked.",
                      "code": "credential_in_url"
                    }
                  },
                  "caller_supplied_identity": {
                    "summary": "caller_supplied_identity",
                    "value": {
                      "error": "This API never accepts \"account_id\". Your key already identifies the account, and every response is scoped to it.",
                      "code": "caller_supplied_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`insufficient_balance` — The balance is spent, so no new spending channel is opened. Carries remainingBytes. Nothing else about the account has changed, and every other endpoint here still answers.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "insufficient_balance": {
                    "summary": "insufficient_balance",
                    "value": {
                      "error": "Traffic balance exhausted. Top up in the dashboard to create proxy users again.",
                      "code": "insufficient_balance"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`conflict` — The label is taken — including by a deleted proxy user, which carries field: \"label\" — or the account is at 50 live proxy users, which carries limit and current.\n\n`not_provisioned` — The account has never bought traffic.\n\n`account_suspended` — The subscription is suspended or cancelled. Carries status.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "conflict": {
                    "summary": "conflict",
                    "value": {
                      "error": "The label is taken — including by a deleted proxy user, which carries field: \"label\" — or the account is at 50 live proxy users, which carries limit and current.",
                      "code": "conflict"
                    }
                  },
                  "not_provisioned": {
                    "summary": "not_provisioned",
                    "value": {
                      "error": "This account has never bought traffic. Buy gigabytes in the dashboard first.",
                      "code": "not_provisioned"
                    }
                  },
                  "account_suspended": {
                    "summary": "account_suspended",
                    "value": {
                      "error": "This account is suspended.",
                      "code": "account_suspended"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "description": "`unavailable` — The balance could not be confirmed, so nothing was created. Deliberately not a 402: a meter we could not read is doubt, and sending a customer with a full balance to a top-up page is the wrong kind of wrong.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "unavailable",
                    "value": {
                      "error": "Your balance cannot be confirmed right now, so this was not created. Retry shortly — nothing about your account has changed.",
                      "code": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxy-users/{id}": {
      "delete": {
        "operationId": "deleteProxyUser",
        "summary": "Revoke a proxy user",
        "description": "Rotation is the reason most people want this API at all. Revoking is final: nothing can un-revoke a credential, because you still hold the secret and restoring it would restore a working proxy.\n\nIt does not stop instantly, and the response says so as a number rather than an adverb: stopsServingWithinSeconds is 30. The gateway authenticates every connection and therefore caches a credential that long before asking the database again, and it never cuts a connection already open. An automation that revokes and then asserts the proxy is dead has to wait that long first — which is why the figure is in the body, where it can be compared to a clock.\n\nAn id that belongs to somebody else returns not_found, exactly as an id that does not exist does. Ownership is part of the query rather than a check before it, so there is no window in between.\n\nRate limit: 30 requests per 60 seconds, counted per API key.",
        "tags": [
          "Proxy users"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The proxy user id, from the list endpoint.",
            "schema": {
              "type": "integer"
            },
            "example": 41
          }
        ],
        "responses": {
          "200": {
            "description": "Revoke a proxy user",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "revoked",
                    "stopsServingWithinSeconds"
                  ],
                  "properties": {
                    "id": {
                      "description": "The proxy user that was revoked.",
                      "type": "integer"
                    },
                    "revoked": {
                      "description": "True. The row is revoked, and nothing can un-revoke it.",
                      "type": "boolean"
                    },
                    "stopsServingWithinSeconds": {
                      "description": "How long the gateway may keep serving this credential. Wait it out before asserting the proxy is dead.",
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "id": 41,
                  "revoked": true,
                  "stopsServingWithinSeconds": 30
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — The id in the path is not a positive integer. Distinct from 404: the URL is malformed rather than merely pointing at nothing. Carries field: \"id\".\n\n`credential_in_url` — A key appeared in the query string. Refused whether or not it was valid — by the time we see it, it is already in an access log. Move it to the header and revoke the key you sent.\n\n`caller_supplied_identity` — The request tried to name an account, user or customer. Never honoured: your key decides whose data comes back, and refusing loudly beats ignoring it quietly.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "invalid_request",
                    "value": {
                      "error": "The id in the path must be a positive integer.",
                      "code": "invalid_request"
                    }
                  },
                  "credential_in_url": {
                    "summary": "credential_in_url",
                    "value": {
                      "error": "An API key must never appear in a URL — it is written to every access log it passes through. Send it as `Authorization: Bearer <key>` instead, and revoke the key you just sent: treat it as leaked.",
                      "code": "credential_in_url"
                    }
                  },
                  "caller_supplied_identity": {
                    "summary": "caller_supplied_identity",
                    "value": {
                      "error": "This API never accepts \"account_id\". Your key already identifies the account, and every response is scoped to it.",
                      "code": "caller_supplied_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "`not_found` — No live proxy user with that id on your account. Deleting one twice returns this the second time, which is not an error worth retrying.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "summary": "not_found",
                    "value": {
                      "error": "No such proxy user. It may already have been deleted — deleting one twice is not an error worth retrying.",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "description": "`unavailable` — We could not reach something we depend on. Back off and try again.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "unavailable",
                    "value": {
                      "error": "We could not reach something we depend on. Back off and try again.",
                      "code": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxy-users/{id}/connection": {
      "post": {
        "operationId": "buildConnection",
        "summary": "Assemble a proxy endpoint",
        "description": "Turns targeting into the line your client connects with. No network call happens — it is string assembly, validated against exactly the rules the gateway will apply when your connection arrives, so anything this accepts will connect and anything it refuses would have failed at connect time instead.\n\nThe line comes back as a TEMPLATE with the secret’s position named: replace secretPlaceholder with the secret you saved when you created the proxy user. We hold only a digest of it and cannot fill it in. A placeholder is returned rather than an unannounced dummy or a null because those two produce, respectively, a customer who pasted a placeholder into production and a client that rendered \"null\" into the password.\n\nIt also echoes the targeting back, after defaults and normalisation. Compare the echo with the username when something surprises you: rotation -1 appears in the echo and NOT in the username, and that is correct. Written out, rotation--1 makes the proxy answer 407; expressed by omission it means a new address per request, which is what -1 means and is also the default.\n\nWhy POST for something that reads nothing: targeting is a structured object with mutually exclusive fields, and the alternative is a query string. A query string is exactly where an API key must never go, and the surest way to stop somebody putting one there is to give them no URL to append it to.\n\nRate limit: 300 requests per 60 seconds, counted per API key.",
        "tags": [
          "Proxy users"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The proxy user whose credential the username is built on.",
            "schema": {
              "type": "integer"
            },
            "example": 41
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "country"
                ],
                "properties": {
                  "country": {
                    "description": "ISO 3166-1 alpha-2, case-insensitive. Required on every connection — the network has no \"anywhere\".",
                    "type": "string"
                  },
                  "proxyType": {
                    "description": "Defaults to residential. All three bill at the same rate against the same balance.",
                    "type": "string",
                    "enum": [
                      "residential",
                      "mobile",
                      "datacenter"
                    ]
                  },
                  "protocol": {
                    "description": "Defaults to http. `https` is REFUSED with a 400 rather than accepted: https:// as a proxy scheme means TLS to the proxy itself and there is no TLS listener, so a line built with it could never connect. https:// destinations are unaffected and are the normal case — they ride a CONNECT tunnel through either scheme.",
                    "type": "string",
                    "enum": [
                      "http",
                      "socks5"
                    ]
                  },
                  "state": {
                    "description": "A state from the location catalogue. Mutually exclusive with city.",
                    "type": "string"
                  },
                  "city": {
                    "description": "A city from the location catalogue. Letters, digits and underscores only — spaces become underscores, and a name with an accent or a dash has no representation in the username at all.",
                    "type": "string"
                  },
                  "asn": {
                    "description": "Digits only, no AS prefix. There is no catalogue to browse — this is for somebody who already knows the number.",
                    "type": "string"
                  },
                  "rotation": {
                    "description": "-1 is a new address per request and is the default. 0 is sticky. The rest are minutes.",
                    "type": "integer",
                    "enum": [
                      -1,
                      0,
                      5,
                      10,
                      15,
                      20,
                      60
                    ]
                  },
                  "session": {
                    "description": "Names a sticky session so you can come back to the same address. Requires rotation 0; letters, digits and underscores only.",
                    "type": "string"
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "country": "DE",
                "proxyType": "residential",
                "rotation": 0,
                "session": "job_42"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Assemble a proxy endpoint",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "proxyUser",
                    "host",
                    "port",
                    "protocol",
                    "username",
                    "connectionTemplate",
                    "secretPlaceholder",
                    "targeting"
                  ],
                  "properties": {
                    "proxyUser": {
                      "description": "Which credential this line was built on.",
                      "type": "object",
                      "required": [
                        "id",
                        "label"
                      ],
                      "properties": {
                        "id": {
                          "description": "The id from the path.",
                          "type": "integer"
                        },
                        "label": {
                          "description": "Its label, so a log line can name the job.",
                          "type": "string"
                        }
                      }
                    },
                    "host": {
                      "description": "Our gateway. Always ours — your traffic is metered as it passes through it.",
                      "type": "string"
                    },
                    "port": {
                      "description": "One port for both protocols: the gateway reads the first byte and works out which you are speaking.",
                      "type": "integer"
                    },
                    "protocol": {
                      "description": "What you asked for.",
                      "type": "string",
                      "enum": [
                        "http",
                        "socks5"
                      ]
                    },
                    "username": {
                      "description": "Credential and targeting, dash-delimited. Use it as-is; it is 255 bytes at most, which is what SOCKS5 can carry.",
                      "type": "string"
                    },
                    "connectionTemplate": {
                      "description": "The whole line, with secretPlaceholder standing in for the password. Substitute and connect.",
                      "type": "string"
                    },
                    "secretPlaceholder": {
                      "description": "The exact substring to replace. Returned rather than documented as a constant, so your code substitutes what we actually used instead of what a page said last year.",
                      "type": "string"
                    },
                    "targeting": {
                      "description": "What we understood, after defaults and normalisation: country upper-cased, proxyType and protocol defaulted. Only the fields that apply are present.",
                      "type": "object",
                      "required": [
                        "proxyType",
                        "country",
                        "protocol"
                      ],
                      "properties": {
                        "proxyType": {
                          "description": "Defaulted to residential when you did not send one.",
                          "type": "string",
                          "enum": [
                            "residential",
                            "mobile",
                            "datacenter"
                          ]
                        },
                        "country": {
                          "description": "Upper-cased.",
                          "type": "string"
                        },
                        "protocol": {
                          "description": "Defaulted to http.",
                          "type": "string",
                          "enum": [
                            "http",
                            "socks5"
                          ]
                        },
                        "state": {
                          "description": "Present only if you sent one.",
                          "type": "string"
                        },
                        "city": {
                          "description": "Present only if you sent one.",
                          "type": "string"
                        },
                        "asn": {
                          "description": "Present only if you sent one.",
                          "type": "string"
                        },
                        "rotation": {
                          "description": "Present only if you sent one. -1 appears here and never in the username.",
                          "type": "integer",
                          "enum": [
                            -1,
                            0,
                            5,
                            10,
                            15,
                            20,
                            60
                          ]
                        },
                        "session": {
                          "description": "Present only if you sent one.",
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "proxyUser": {
                    "id": 41,
                    "label": "scraper_prod"
                  },
                  "host": "gateway.proxylexus.com",
                  "port": 10800,
                  "protocol": "http",
                  "username": "8f3c9a1d4b7e2f6a5c0d9e81-type-residential-country-DE-rotation-0-session-job_42",
                  "connectionTemplate": "http://8f3c9a1d4b7e2f6a5c0d9e81-type-residential-country-DE-rotation-0-session-job_42:{secret}@gateway.proxylexus.com:10800",
                  "secretPlaceholder": "{secret}",
                  "targeting": {
                    "proxyType": "residential",
                    "country": "DE",
                    "protocol": "http",
                    "rotation": 0,
                    "session": "job_42"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — Country missing or not two letters; an unknown parameter; state and city together; a session without rotation 0; a name with a dash or an accent; a username over 255 bytes. Always carries field.\n\n`credential_in_url` — A key appeared in the query string. Refused whether or not it was valid — by the time we see it, it is already in an access log. Move it to the header and revoke the key you sent.\n\n`caller_supplied_identity` — The request tried to name an account, user or customer. Never honoured: your key decides whose data comes back, and refusing loudly beats ignoring it quietly.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "invalid_request",
                    "value": {
                      "error": "Country missing or not two letters; an unknown parameter; state and city together; a session without rotation 0; a name with a dash or an accent; a username over 255 bytes. Always carries field.",
                      "code": "invalid_request"
                    }
                  },
                  "credential_in_url": {
                    "summary": "credential_in_url",
                    "value": {
                      "error": "An API key must never appear in a URL — it is written to every access log it passes through. Send it as `Authorization: Bearer <key>` instead, and revoke the key you just sent: treat it as leaked.",
                      "code": "credential_in_url"
                    }
                  },
                  "caller_supplied_identity": {
                    "summary": "caller_supplied_identity",
                    "value": {
                      "error": "This API never accepts \"account_id\". Your key already identifies the account, and every response is scoped to it.",
                      "code": "caller_supplied_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "`not_found` — No live proxy user with that id on your account. An id belonging to somebody else answers identically — a 403 would confirm the row exists.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "not_found": {
                    "summary": "not_found",
                    "value": {
                      "error": "No such proxy user. List them at GET /api/v1/proxy-users.",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "description": "`unavailable` — We could not reach something we depend on. Back off and try again.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "unavailable",
                    "value": {
                      "error": "We could not reach something we depend on. Back off and try again.",
                      "code": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/locations": {
      "get": {
        "operationId": "listLocations",
        "summary": "List what is stocked",
        "description": "Countries, and the cities and states available inside them, for one proxy type. You need this to know that DE is stocked for mobile before you build a connection that targets it.\n\nEvery place comes back as a pair: name is for showing a human, value is the string to put in city or state. They differ because the proxy username is dash-delimited, so a place name is only usable if it survives that — spaces become underscores, and a name carrying a dash, an apostrophe or an accent cannot be carried at all. Send value and a connection always builds; retype name and it may not.\n\nA name that cannot be carried is left out and counted in omitted, rather than dropped silently. That count is there so a customer who cannot find Düsseldorf can see that we know it is missing, instead of concluding we do not stock Germany properly. Countries are never omitted — a country code is two ASCII letters and always survives.\n\nThe states array is a list of country-level subdivisions, not a mapping of cities to the state they sit in. Pick from one array or the other, never both.\n\nCache it. It changes on the order of weeks, every call reaches the network behind us, and the budget here is a tenth of the read budget for exactly that reason. Fetch it at start-up, keep it, refetch daily at most. The example below is abridged to one country; the live response carries every country stocked for that type.\n\nRate limit: 30 requests per 60 seconds, counted per API key.",
        "tags": [
          "Catalogue"
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Defaults to residential.",
            "schema": {
              "type": "string",
              "enum": [
                "residential",
                "mobile",
                "datacenter"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List what is stocked",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "type",
                    "items"
                  ],
                  "properties": {
                    "type": {
                      "description": "The type you asked for.",
                      "type": "string",
                      "enum": [
                        "residential",
                        "mobile",
                        "datacenter"
                      ]
                    },
                    "items": {
                      "description": "One entry per stocked country, ordered by name.",
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "countryCode",
                          "name",
                          "cities",
                          "states",
                          "omitted"
                        ],
                        "properties": {
                          "countryCode": {
                            "description": "What to send as country.",
                            "type": "string"
                          },
                          "name": {
                            "description": "Country name, for display.",
                            "type": "string"
                          },
                          "cities": {
                            "description": "Ordered by name. Send the value, show the name.",
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "name",
                                "value"
                              ],
                              "properties": {
                                "name": {
                                  "description": "The place, spelled the way a person writes it.",
                                  "type": "string"
                                },
                                "value": {
                                  "description": "The same place, encoded for the username. Send this as city.",
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "states": {
                            "description": "Same pairs, for state. Use a state instead of a city, never as well as one.",
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "name",
                                "value"
                              ],
                              "properties": {
                                "name": {
                                  "description": "The subdivision, spelled the way a person writes it.",
                                  "type": "string"
                                },
                                "value": {
                                  "description": "The same subdivision, encoded. Send this as state.",
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "omitted": {
                            "description": "How many names under this country the username grammar cannot express. Almost always accented ones.",
                            "type": "object",
                            "required": [
                              "cities",
                              "states"
                            ],
                            "properties": {
                              "cities": {
                                "description": "Cities left out of the list above.",
                                "type": "integer"
                              },
                              "states": {
                                "description": "States left out of the list above.",
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "type": "residential",
                  "items": [
                    {
                      "countryCode": "DE",
                      "name": "Germany",
                      "cities": [
                        {
                          "name": "Berlin",
                          "value": "Berlin"
                        },
                        {
                          "name": "Frankfurt am Main",
                          "value": "Frankfurt_am_Main"
                        },
                        {
                          "name": "Hamburg",
                          "value": "Hamburg"
                        }
                      ],
                      "states": [
                        {
                          "name": "Bavaria",
                          "value": "Bavaria"
                        },
                        {
                          "name": "Hesse",
                          "value": "Hesse"
                        }
                      ],
                      "omitted": {
                        "cities": 14,
                        "states": 2
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — type was not one of residential, mobile, datacenter. Carries field: \"type\".\n\n`credential_in_url` — A key appeared in the query string. Refused whether or not it was valid — by the time we see it, it is already in an access log. Move it to the header and revoke the key you sent.\n\n`caller_supplied_identity` — The request tried to name an account, user or customer. Never honoured: your key decides whose data comes back, and refusing loudly beats ignoring it quietly.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "invalid_request",
                    "value": {
                      "error": "type must be one of residential, mobile, datacenter.",
                      "code": "invalid_request"
                    }
                  },
                  "credential_in_url": {
                    "summary": "credential_in_url",
                    "value": {
                      "error": "An API key must never appear in a URL — it is written to every access log it passes through. Send it as `Authorization: Bearer <key>` instead, and revoke the key you just sent: treat it as leaked.",
                      "code": "credential_in_url"
                    }
                  },
                  "caller_supplied_identity": {
                    "summary": "caller_supplied_identity",
                    "value": {
                      "error": "This API never accepts \"account_id\". Your key already identifies the account, and every response is scoped to it.",
                      "code": "caller_supplied_identity"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "description": "`unavailable` — The catalogue could not be read. Retry, or keep using the copy you cached — it changes on the order of weeks, which is exactly why you should have one.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed in the window for this budget."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "unavailable",
                    "value": {
                      "error": "The location catalogue cannot be read right now. Retry shortly, or keep using the copy you cached — it changes on the order of weeks.",
                      "code": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from the dashboard, e.g. `plx_a3f91b7c4d_Kd8vQ2mR7wXpL4nZ1yB6tC0sE5uH9jF3aG7kM2oP1qS`. Header only."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A sentence for whoever is reading the logs. NOT stable — it may be reworded at any time, so never branch on it."
          },
          "code": {
            "type": "string",
            "description": "The machine half. Frozen for the life of v1. Branch on this.",
            "enum": [
              "invalid_request",
              "credential_in_url",
              "caller_supplied_identity",
              "unauthorized",
              "insufficient_balance",
              "not_found",
              "conflict",
              "not_provisioned",
              "account_suspended",
              "rate_limited",
              "unavailable",
              "internal"
            ]
          },
          "field": {
            "type": "string",
            "description": "Present on some invalid_request responses: which parameter is wrong."
          },
          "status": {
            "type": "string",
            "description": "Present on account_suspended: the subscription status."
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "`unauthorized` — No key, a malformed key, a wrong secret, or a key that has been revoked. Deliberately one code: telling a caller which of the four it was tells an attacker which half to work on.",
        "headers": {
          "RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests allowed in the window for this budget."
          },
          "RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests left in the current window."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "unauthorized": {
                "summary": "unauthorized",
                "value": {
                  "error": "Send a valid API key as `Authorization: Bearer <key>`. Keys are created and revoked in the dashboard, and a revoked key stops working immediately.",
                  "code": "unauthorized"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited` — Over a budget. Carries Retry-After and RateLimit-Reset, both in seconds from now.",
        "headers": {
          "RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests allowed in the window for this budget."
          },
          "RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests left in the current window."
          },
          "RateLimit-Reset": {
            "schema": {
              "type": "integer"
            },
            "description": "SECONDS FROM NOW until the window resets. Not an epoch timestamp."
          },
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait. Same value as RateLimit-Reset."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "rate_limited": {
                "summary": "rate_limited",
                "value": {
                  "error": "Rate limit reached for this key: 300 requests per 60 seconds on this endpoint.",
                  "code": "rate_limited"
                }
              }
            }
          }
        }
      },
      "Internal": {
        "description": "`internal` — Our fault, already logged. Back off and try again.",
        "headers": {
          "RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests allowed in the window for this budget."
          },
          "RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests left in the current window."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal": {
                "summary": "internal",
                "value": {
                  "error": "Our fault, already logged. Back off and try again.",
                  "code": "internal"
                }
              }
            }
          }
        }
      }
    }
  }
}
