{
  "openapi": "3.1.0",
  "info": {
    "title": "Leadmaster API",
    "version": "1.0.0",
    "description": "Find businesses to contact. Estimate a list, then confirm it to start. An estimate is free for an account that never paid while it has free leads; otherwise a new search costs $0.05, given back when its list starts, and the same search within 24 hours costs nothing. Money is in microdollars (1 USD = 1,000,000). Every error is `{ \"error\": { \"code\", \"message\" } }`. Limits: 60 requests a minute per account, 100 estimates a day per account. An account can have up to 10 active API keys."
  },
  "servers": [
    {
      "url": "https://leadmaster.tech/v1",
      "description": "Replace with your Leadmaster address."
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "paths": {
    "/balance": {
      "get": {
        "operationId": "getBalance",
        "summary": "Balance",
        "description": "The balance, what running lists are holding, and the newest customer-visible ledger rows: 20 by default, `historyLimit` for 0 to 200.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                },
                "example": {
                  "micros": 12500000,
                  "freeLeads": 25,
                  "heldMicros": 0,
                  "heldFreeLeads": 0,
                  "history": [
                    {
                      "kind": "free_grant",
                      "amountMicros": 0,
                      "leads": 25,
                      "jobId": null,
                      "label": null,
                      "createdAt": 1792400000000
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_history_limit`: historyLimit must be an integer 0-200.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "429": {
            "description": "More than 60 requests in a minute. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "historyLimit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 200,
              "default": 20
            },
            "description": "How many of the newest history rows to return."
          }
        ]
      }
    },
    "/places": {
      "get": {
        "operationId": "findPlaces",
        "summary": "Find a place",
        "description": "Turn a place into the region an estimate needs: a country name, \"City, Country\", or a city name alone. A city name alone gives the cities with that whole name in the country the request comes from (for an AI connector, usually the connector's own servers, not the person's country) and the most searched countries (US, GB, CA, AU, IN, ID, DE, FR), the largest first; add the country for anywhere else. An unknown place gives an empty list and a `hint`.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 80
            },
            "example": "Austin, United States"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "places"
                  ],
                  "properties": {
                    "places": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Place"
                      }
                    },
                    "hint": {
                      "type": "string",
                      "description": "Only with an empty list: what to try instead."
                    }
                  }
                },
                "examples": {
                  "city": {
                    "value": {
                      "places": [
                        {
                          "label": "Austin, Texas, United States",
                          "countryCode": "US",
                          "locationCoordinate": "30.2672,-97.7431,25",
                          "kind": "city"
                        }
                      ]
                    }
                  },
                  "bare": {
                    "value": {
                      "places": [
                        {
                          "label": "Bogor, West Java, Indonesia",
                          "countryCode": "ID",
                          "locationCoordinate": "-6.59444,106.78917,25",
                          "kind": "city"
                        }
                      ]
                    }
                  },
                  "none": {
                    "value": {
                      "places": [],
                      "hint": "Try the country too, for example \"Bogor, Indonesia\"."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_q`: q must be 1-80 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "429": {
            "description": "More than 60 requests in a minute. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/categories": {
      "get": {
        "operationId": "searchCategories",
        "summary": "Search categories",
        "description": "Business categories whose name contains q. Estimates accept only these names.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 60
            },
            "example": "dent"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "categories"
                  ],
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "categories": [
                    "dentist",
                    "dental_clinic",
                    "dental_lab"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_q`: q is longer than 60 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "429": {
            "description": "More than 60 requests in a minute. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/categories/suggest": {
      "post": {
        "operationId": "suggestCategories",
        "summary": "Suggest categories",
        "description": "Describe what you sell and get the business categories most likely to buy it. Limited to 10 a minute per account, the same as on the website. Waits at most 15 seconds for the model, then answers 503 `suggest_unavailable`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500
                  }
                }
              },
              "example": {
                "text": "Appointment reminder software for small dental practices"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Suggestion"
                },
                "example": {
                  "categories": [
                    "dentist",
                    "dental_clinic"
                  ],
                  "idealCustomer": "Small independent dental practices that book by phone.",
                  "placeHint": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_text`: text is required, 1-500 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "429": {
            "description": "`rate_limited`: more than 60 requests, or more than 10 category suggestions, in a minute (the message says which). Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "502": {
            "description": "`suggest_failed`: try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`suggest_unavailable`: suggestions did not answer within 15 seconds. Try again, or search categories by name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/estimates": {
      "post": {
        "operationId": "createEstimate",
        "summary": "Estimate a list",
        "description": "An estimate: how many businesses were found, examples, and the most the list can cost. Free for an account that never bought credit while it has free leads; an account that bought or was given credit pays $0.05 per new search (`searchFeeMicros`), given back when the list starts. The same search again within 24 hours (any maxLeads or rule) costs nothing. Refused with 402 `search_needs_credit` when the account has no free leads and less balance than one search. Limits: 10 estimates a minute, and 100 a day per account across all keys and assistants.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EstimateRequest"
              },
              "example": {
                "categories": [
                  "dentist"
                ],
                "countryCode": "US",
                "locationCoordinate": "30.2672,-97.7431,25",
                "placeLabel": "Austin, United States",
                "rule": "phone",
                "maxLeads": 200
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "jobId": "7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13",
                  "totalFound": 412,
                  "sample": [
                    {
                      "title": "Maple & Main Dental Studio",
                      "category": "dentist",
                      "city": "Austin",
                      "rating": 4.8
                    }
                  ],
                  "maxLeads": 200,
                  "priceMicros": 5000,
                  "maxCostMicros": 1000000,
                  "freeLeadsApplied": 0,
                  "balanceMicros": 12450000,
                  "searchFeeMicros": 50000
                }
              }
            }
          },
          "400": {
            "description": "`invalid_body` (a field we do not know, named with the one you probably meant, for example \"unknown field max_leads; did you mean maxLeads?\"), `invalid_max_leads` (missing, or not 10 to 10,000 or \"all\"), `invalid_categories` (no categories and no keyword, or one not in the catalog, named with the nearest one), `invalid_keyword`, `invalid_location_coordinate`, `invalid_country_code`, `invalid_rule`, `invalid_place_label`, `invalid_description` and similar: the code names the field. Nothing is searched or charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`search_needs_credit`: no free leads left and less balance than one search costs (top up, then estimate again), or `spend_limit`: this search's fee would take the key's monthly limit over.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "409": {
            "description": "`fee_changed`: the search would cost more than `acceptedFeeMicros`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (more than 60 requests, or more than 10 estimates, in a minute; the message says which; wait 60 seconds) or `estimate_limit` (the 100 a day cap; `Retry-After` is the seconds until midnight UTC).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "502": {
            "description": "`provider_error`: try again soon.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`screening_unavailable` or `provider_budget`: try again soon.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/estimates/{id}/confirm": {
      "post": {
        "operationId": "confirmEstimate",
        "summary": "Start a list",
        "description": "Starts the list from one of your own estimates and holds its cost from the balance. You are charged only for ready leads, never more than the estimate. Only the key that made the estimate can start it, so its monthly spending limit holds; with any other key (or a web estimate) the answer is `not_found`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id (the estimate id of a started list)."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`insufficient_balance` (top up) or `spend_limit` (this key's monthly limit would be exceeded).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "description": "`not_found`: no such estimate on this account, or it was made by a different key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`estimate_changed` (the price moved, make a new estimate) or `not_awaiting_confirmation` (already used), `already_continued` (a follow-up of the same list started first), or `results_expired` (a follow-up whose earlier list's results were deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (more than 60 requests in a minute; wait 60 seconds) or `daily_limit` (the account's daily limit for new lists; `Retry-After` is the seconds until midnight UTC).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/lists": {
      "get": {
        "operationId": "listLists",
        "summary": "Your lists",
        "description": "The newest 50 lists. An estimate nobody confirmed is not a list yet. Every key and assistant on the account sees all its lists.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "lists"
                  ],
                  "properties": {
                    "lists": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ListSummary"
                      }
                    }
                  }
                },
                "example": {
                  "lists": [
                    {
                      "id": "7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13",
                      "status": "done",
                      "label": "Dentist in Austin, United States",
                      "categories": [
                        "dentist"
                      ],
                      "rule": "phone",
                      "maxLeads": 200,
                      "readyLeads": 187,
                      "createdAt": 1792500000000,
                      "finishedAt": 1792500420000,
                      "resultsDeleted": false
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "429": {
            "description": "More than 60 requests in a minute. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/lists/{id}": {
      "get": {
        "operationId": "getList",
        "summary": "A list",
        "description": "Progress while running; once done, what it found, what it cost, and when its results are deleted (30 days after it finishes).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id (the estimate id of a started list)."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListDetail"
                },
                "example": {
                  "id": "7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13",
                  "status": "done",
                  "label": "Dentist in Austin, United States",
                  "categories": [
                    "dentist"
                  ],
                  "rule": "phone",
                  "maxLeads": 200,
                  "readyLeads": 187,
                  "createdAt": 1792500000000,
                  "finishedAt": 1792500420000,
                  "resultsDeleted": false,
                  "stopReason": "completed",
                  "totalFound": 412,
                  "pulled": 412,
                  "counts": {
                    "ready": 187,
                    "not_ready": 61
                  },
                  "maxCostMicros": 800000,
                  "charged": {
                    "micros": 748000,
                    "freeLeads": 0
                  },
                  "keepUntil": 1795092420000
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in a minute. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/lists/{id}/leads": {
      "get": {
        "operationId": "getLeads",
        "summary": "Leads of a finished list",
        "description": "A page of leads. `view=ready` (default) carries the contacts; `view=other` is the businesses that did not meet the rule, with no contacts.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id (the estimate id of a started list)."
          },
          {
            "name": "view",
            "in": "query",
            "schema": {
              "enum": [
                "ready",
                "other"
              ],
              "default": "ready"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "leads",
                    "total"
                  ],
                  "properties": {
                    "leads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Lead"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "leads": [
                    {
                      "name": "Maple & Main Dental Studio",
                      "category": "dentist",
                      "address": "412 Maple Street",
                      "city": "Austin",
                      "country": "US",
                      "phone": "+15125550123",
                      "phone_type": "mobile",
                      "whatsapp_url": "https://wa.me/15125550123",
                      "email": "hello@mapleandmain.example.com",
                      "email_status": "valid",
                      "website": "https://mapleandmain.example.com",
                      "instagram": "https://instagram.com/mapleandmaindental.example",
                      "tiktok": "",
                      "facebook": "",
                      "linkedin": "",
                      "rating": "4.8",
                      "reviews": "127",
                      "fit": "0.92",
                      "chain": "0.00",
                      "google_maps_url": "https://maps.google.com/?cid=1234567890123456701",
                      "status": "ready"
                    }
                  ],
                  "total": 187
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_offset` or `invalid_view`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`not_finished`: the list is still running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`expired`: the results were deleted after 30 days.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in a minute. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/lists/{id}/download-link": {
      "post": {
        "operationId": "createDownloadLink",
        "summary": "CSV download link",
        "description": "A fresh signed link to the finished list as a CSV. It works for 24 hours and needs no key, so treat it like a password.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id (the estimate id of a started list)."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "expiresAt"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expiresAt": {
                      "type": "integer",
                      "description": "Unix milliseconds."
                    }
                  }
                },
                "example": {
                  "url": "https://leadmaster.tech/v1/files/eyJ...signed",
                  "expiresAt": 1792586820000
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`not_finished`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests in a minute. Wait `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`unavailable`: downloads are off; read the leads instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/lists/{id}/unlock": {
      "get": {
        "operationId": "getUnlockQuote",
        "summary": "Unlock quote",
        "description": "What unlocking the checked extra leads of a finished list costs now (at most as many as the list's own size): free leads first, then the normal price.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id (the estimate id of a started list)."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockQuote"
                },
                "example": {
                  "extras": 6,
                  "freeLeadsApplied": 0,
                  "micros": 30000,
                  "priceMicros": 5000
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "description": "`not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "unlockExtras",
        "summary": "Unlock checked extras",
        "description": "Charges the quote and adds every checked extra to the list's ready leads and CSV. Send the quoted `micros`: a different price now answers `quote_changed`. A repeat charges nothing and answers the same. A key can unlock only lists that key made.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id (the estimate id of a started list)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "micros"
                ],
                "properties": {
                  "micros": {
                    "type": "integer",
                    "minimum": 0
                  }
                }
              },
              "example": {
                "micros": 30000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "unlocked",
                    "micros",
                    "freeLeads"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "unlocked": {
                      "type": "integer"
                    },
                    "micros": {
                      "type": "integer"
                    },
                    "freeLeads": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "unlocked": 6,
                  "micros": 30000,
                  "freeLeads": 0
                }
              }
            }
          },
          "400": {
            "description": "`invalid_micros`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`insufficient_balance` or `spend_limit`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "description": "`not_found`: no such list on this account, or it was started by a different key. Every key on the account can read the list; only the key that started it can unlock it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`quote_changed` (get the quote again) or `nothing_to_unlock`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/lists/{id}/continue": {
      "post": {
        "operationId": "continueList",
        "summary": "Estimate the rest",
        "description": "An estimate for a follow-up list that checks the rest of what a capped list found. Every lead is at the normal price: the lower of the first list's price and the account's price today. Free leads go first. Businesses already in the list are never included or charged again. Start it with `POST /estimates/{id}/confirm`. A list continues once.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The list id (the estimate id of a started list)."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "maxLeads": {
                    "oneOf": [
                      {
                        "type": "integer",
                        "minimum": 10,
                        "maximum": 10000
                      },
                      {
                        "type": "string",
                        "const": "all"
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "description": "10 to 10,000, or \"all\" (any letter case). Left out or null: all that are left. The body can also be left out."
                  }
                },
                "additionalProperties": false
              },
              "example": {
                "maxLeads": 200
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_body` (a field other than maxLeads) or `invalid_max_leads`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "description": "`not_found`: no such list on this account, or it was started by a different key. Every key on the account can read the list; only the key that started it can continue it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`nothing_left` or `already_continued`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/files/{token}": {
      "get": {
        "operationId": "downloadCsv",
        "summary": "Download a CSV",
        "description": "Streams the CSV for a signed link from the download-link route. This is the one route that takes no key: the link itself is the authorization.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The CSV file.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`invalid_link`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: the list or its file is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`link_expired`: ask for a new link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`unavailable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from the dashboard (Developers, API keys), sent as `Authorization: Bearer lm_live_...`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, for code to branch on."
              },
              "message": {
                "type": "string",
                "description": "Plain language, for people."
              }
            }
          }
        },
        "example": {
          "error": {
            "code": "rate_limited",
            "message": "Too many requests. Try again in a minute."
          }
        }
      },
      "Balance": {
        "type": "object",
        "required": [
          "micros",
          "freeLeads",
          "heldMicros",
          "heldFreeLeads",
          "history"
        ],
        "properties": {
          "micros": {
            "type": "integer",
            "description": "Microdollars (1 USD = 1,000,000)."
          },
          "freeLeads": {
            "type": "integer"
          },
          "heldMicros": {
            "type": "integer"
          },
          "heldFreeLeads": {
            "type": "integer"
          },
          "history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "enum": [
                    "free_grant",
                    "admin_grant",
                    "topup",
                    "charge",
                    "refund",
                    "repay",
                    "search_fee",
                    "search_fee_credit"
                  ]
                },
                "amountMicros": {
                  "type": "integer"
                },
                "leads": {
                  "type": "integer"
                },
                "jobId": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "label": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "createdAt": {
                  "type": "integer"
                }
              }
            },
            "description": "The newest balance changes, newest first: 20 unless historyLimit says otherwise."
          }
        }
      },
      "Place": {
        "type": "object",
        "required": [
          "label",
          "countryCode",
          "locationCoordinate",
          "kind"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "locationCoordinate": {
            "type": "string",
            "description": "lat,lng,radiusKm"
          },
          "kind": {
            "enum": [
              "country",
              "city"
            ]
          }
        }
      },
      "Suggestion": {
        "type": "object",
        "properties": {
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "idealCustomer": {
            "type": [
              "string",
              "null"
            ]
          },
          "placeHint": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "EstimateRequest": {
        "type": "object",
        "required": [
          "countryCode",
          "locationCoordinate",
          "placeLabel",
          "rule",
          "maxLeads"
        ],
        "properties": {
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 10,
            "description": "Names from the categories route. Up to 10; may be empty or left out when keyword is set. Send categories, a keyword, or both. A name that is not in the catalog is refused with `invalid_categories`, naming it and the nearest catalog name."
          },
          "keyword": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 2,
            "maxLength": 60,
            "pattern": "^[\\p{L}\\p{N} &'.-]+$",
            "description": "Also find businesses whose name contains this (2 to 60 letters, digits, spaces or & ' - . after trimming), for example \"photobooth\" when no category fits exactly. Both searches are combined and each business counts once; totalFound adds the two counts, so it is an upper bound."
          },
          "countryCode": {
            "type": "string",
            "pattern": "^[A-Za-z]{2}$"
          },
          "locationCoordinate": {
            "type": "string",
            "description": "lat,lng,radiusKm (radius 1-100000), from the places route."
          },
          "placeLabel": {
            "type": "string",
            "maxLength": 120,
            "description": "Names the list."
          },
          "rule": {
            "enum": [
              "any",
              "phone",
              "social",
              "email",
              "instagram",
              "tiktok",
              "raw"
            ],
            "description": "Which contact a lead must have to count and be charged: social is any Instagram, TikTok, Facebook or LinkedIn profile; instagram and tiktok count only that platform's profile, at the social price; raw is the raw list: every business found, with its phone and website exactly as Google Maps lists them (not checked, no WhatsApp link), at $3 per 1,000 ($2 per 1,000 over 100,000 ready leads in 30 days); a raw list starts at 50 leads (maxLeads 50 or more, and a search that finds fewer is refused)."
          },
          "maxLeads": {
            "oneOf": [
              {
                "type": "integer",
                "minimum": 10,
                "maximum": 10000
              },
              {
                "type": "string",
                "const": "all"
              }
            ],
            "description": "Required: the most ready leads to deliver, 10 to 10,000, or \"all\" for every business found."
          },
          "idealCustomer": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who the best customers are; used to rank by fit."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500
          },
          "onePerBrand": {
            "type": "boolean",
            "default": true
          },
          "acceptedFeeMicros": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional: the most search fee (micros) the person agreed to. A search that would cost more is refused with 409 `fee_changed` and costs nothing. Left out: no limit."
          }
        },
        "additionalProperties": false
      },
      "Estimate": {
        "type": "object",
        "required": [
          "jobId",
          "totalFound",
          "sample",
          "maxLeads",
          "priceMicros",
          "maxCostMicros",
          "freeLeadsApplied",
          "balanceMicros"
        ],
        "properties": {
          "jobId": {
            "type": "string",
            "description": "The estimate id; also the list id once confirmed."
          },
          "totalFound": {
            "type": "integer"
          },
          "sample": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "category": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "city": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "rating": {
                  "type": [
                    "number",
                    "null"
                  ]
                }
              }
            }
          },
          "maxLeads": {
            "type": "integer"
          },
          "priceMicros": {
            "type": "integer",
            "description": "Per ready lead."
          },
          "maxCostMicros": {
            "type": "integer",
            "description": "The most this list can cost (after free leads)."
          },
          "freeLeadsApplied": {
            "type": "integer"
          },
          "balanceMicros": {
            "type": "integer"
          },
          "searchFeeMicros": {
            "type": "integer",
            "description": "The search fee starting this list gives back (charged on this estimate, or on an earlier estimate of the same search that no list has returned yet). 0 when none; balanceMicros is after it."
          }
        }
      },
      "ListSummary": {
        "type": "object",
        "required": [
          "id",
          "status",
          "label",
          "categories",
          "keyword",
          "rule",
          "maxLeads",
          "readyLeads",
          "createdAt",
          "finishedAt",
          "resultsDeleted"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "enum": [
              "running",
              "finalizing",
              "done"
            ]
          },
          "label": {
            "type": "string"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "keyword": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name keyword the list searched, or null."
          },
          "rule": {
            "enum": [
              "any",
              "phone",
              "social",
              "email",
              "instagram",
              "tiktok",
              "raw"
            ]
          },
          "maxLeads": {
            "type": "integer"
          },
          "readyLeads": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null while the list runs."
          },
          "createdAt": {
            "type": "integer",
            "description": "Unix milliseconds."
          },
          "finishedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "resultsDeleted": {
            "type": "boolean"
          }
        }
      },
      "ListDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ListSummary"
          },
          {
            "type": "object",
            "required": [
              "stopReason",
              "totalFound",
              "pulled",
              "counts",
              "maxCostMicros",
              "charged",
              "keepUntil"
            ],
            "properties": {
              "stopReason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Why the list stopped, for example completed or reached_max."
              },
              "totalFound": {
                "type": "integer"
              },
              "pulled": {
                "type": "integer"
              },
              "counts": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "maxCostMicros": {
                "type": "integer"
              },
              "charged": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "micros": {
                        "type": "integer"
                      },
                      "freeLeads": {
                        "type": "integer"
                      }
                    }
                  }
                ]
              },
              "keepUntil": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "When the results are deleted."
              },
              "countryCode": {
                "type": "string"
              },
              "parentJobId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The list this follow-up continues."
              },
              "followUpJobId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The follow-up list that continues this one."
              },
              "unlock": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/UnlockQuote"
                  }
                ],
                "description": "Checked extras to unlock, or null."
              },
              "rest": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "remaining": {
                        "type": "integer",
                        "description": "About how many businesses are left to check (an upper bound)."
                      }
                    }
                  }
                ],
                "description": "The rest a follow-up list can check, or null."
              }
            }
          }
        ]
      },
      "Lead": {
        "type": "object",
        "description": "Every value is a string. Contact fields are present only on ready leads.",
        "additionalProperties": {
          "type": "string"
        },
        "properties": {
          "name": {
            "type": "string"
          },
          "status": {
            "enum": [
              "ready",
              "not_ready",
              "over_limit"
            ]
          },
          "city": {
            "type": "string"
          },
          "reviews": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "fit": {
            "type": "string"
          }
        }
      },
      "UnlockQuote": {
        "type": "object",
        "required": [
          "extras",
          "freeLeadsApplied",
          "micros",
          "priceMicros"
        ],
        "properties": {
          "extras": {
            "type": "integer",
            "description": "Checked ready leads past the list's cap that can be unlocked: at most the list's own size, the first ones checked. 0 = nothing to unlock (none, or the list was unlocked already: once per list)."
          },
          "freeLeadsApplied": {
            "type": "integer",
            "description": "Free leads used first, one per lead."
          },
          "micros": {
            "type": "integer",
            "description": "The price of the rest, rounded up once. Send it back to unlock."
          },
          "priceMicros": {
            "type": "integer",
            "description": "Per lead: the normal price (for a follow-up, the first list's, or its own when that was lower)."
          }
        }
      }
    },
    "responses": {
      "AccountRefused": {
        "description": "`account_blocked` (the account is blocked) or `account_review` (it is under review and cannot estimate or start lists yet).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
