{
  "openapi": "3.1.0",
  "info": {
    "title": "ShowFirst API",
    "version": "2.0.0",
    "summary": "Find local businesses, build preview sites for them in batches, and build full websites.",
    "description": "Every ShowFirst route in one file. Send requests to the same domain you use for your dashboard. Create keys on the API Keys page of your dashboard; account keys begin with lgen_ and work on Site Creation, Lead Gen and Prospecting. White-label agency keys begin with sf_agency_ and work on the White-label Agency routes only.\n\nError responses have \"ok\": false and an error string. Validation errors also have a details list with one line per problem.\n\nThe readable guides: /docs/api-site-creation, /docs/api-lead-gen and /docs/api-prospecting. AI agents can use the MCP server at /docs/mcp and the prospecting skill at /docs/skills-prospecting.",
    "termsOfService": "https://showfirst.app/terms",
    "contact": { "name": "ShowFirst", "url": "https://showfirst.app/docs/" }
  },
  "servers": [
    { "url": "https://showfirst.app", "description": "Production. If your dashboard is on your own domain, use that domain instead." }
  ],
  "tags": [
    { "name": "Site Creation", "description": "Queue a full website build for one business and check on it until it finishes. 1 site credit per build." },
    { "name": "Lead Gen", "description": "Send a list of businesses as one batch and get a preview site for each. 1 credit per row, minimum 5." },
    { "name": "White-label Agency", "description": "For agencies that take site requests from their own clients. Agency keys (sf_agency_) only." },
    { "name": "Prospecting", "description": "Find local businesses by kind and place, run one search across many places, page through results and export CSVs. Uses lead credits." },
    { "name": "Resources", "description": "Files for agents and tools." }
  ],
  "security": [ { "BearerAuth": [] }, { "ApiKeyHeader": [] } ],
  "paths": {
    "/api/templates": {
      "get": {
        "tags": ["Site Creation"],
        "operationId": "listSiteTemplates",
        "summary": "List site templates",
        "description": "The templates a new site can use, as a JSON list. No key is needed; a key that is sent must be valid. Send a template's templateId (and its themeId, when it has one) to POST /api/provision.",
        "security": [ {}, { "BearerAuth": [] }, { "ApiKeyHeader": [] } ],
        "responses": {
          "200": {
            "description": "The template list.",
            "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/SiteTemplate" } },
              "example": [ { "id": "classic", "templateId": "template6", "themeId": "classic", "name": "Classic", "description": "Clean and modern, our standard look", "version": "2.0.0", "hasServices": true, "hasServiceAreas": true, "featureImage": "/img/look-picker/classic-2.jpg", "previewUrl": "/templates/ultimate?look=classic" } ] } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/provision": {
      "post": {
        "tags": ["Site Creation"],
        "operationId": "createSite",
        "summary": "Create a site",
        "description": "Queues a full website build for one business. A valid request uses 1 site credit and returns 202. You need at least 1 credit on your account. Keep the siteId: you need it to check the build.\n\nSometimes a valid request is not queued: you then get 200 with \"paused\": true and a message. No site is queued and no credit is used. Try again later.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SiteCreateRequest" },
          "example": { "businessName": "Example Plumbing", "industry": "Plumbing", "email": "owner@example.com", "phone": "(555) 010-0100", "address": "100 Main St, Springfield, IL 62701", "aboutBusiness": "Example Plumbing repairs and installs residential plumbing.", "services": ["Drain cleaning", "Water heater installation", "Leak repair"], "serviceAreas": ["Springfield", "Riverton", "Chatham"], "look": "classic", "primaryColor": "#2F6B55", "backgroundTheme": "light" } } } },
        "responses": {
          "202": {
            "description": "The build is queued and 1 site credit was used.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SiteCreateAccepted" },
              "example": { "slug": "example-plumbing", "siteId": "3f2a9c1e-5b7d-4e8a-9c0f-1a2b3c4d5e6f", "previewUrl": null, "statusUrl": "/api/provision/3f2a9c1e-5b7d-4e8a-9c0f-1a2b3c4d5e6f" } } }
          },
          "200": {
            "description": "Builds are paused. Nothing was queued and no credit was used.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PausedResponse" },
              "example": { "ok": true, "paused": true, "message": "Today's free builds are used up — try again tomorrow." } } }
          },
          "400": { "description": "The body is not valid JSON, or a field is missing or invalid. See details. No credit is used.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" },
              "example": { "ok": false, "error": "Invalid provision request", "details": ["phone: must contain at least 10 digits"] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Your account has no credits left.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "No site credits remain." } } } },
          "500": { "description": "Creating a site failed on our side. No job was created and no credit was used. Send the request again.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Provisioning hit a transient storage error. No job was created and no credit was used. Please retry." } } } },
          "503": { "description": "Site creation is paused for maintenance.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "New site generation is temporarily in maintenance mode." } } } }
        }
      }
    },
    "/api/provision/{siteId}": {
      "get": {
        "tags": ["Site Creation"],
        "operationId": "getSiteStatus",
        "summary": "Check a build",
        "description": "The build's status, progress and links. Only the key that created the build can read it. Any other key gets the same 404 as an unknown ID. There is no endpoint that lists your sites: keep the siteId of each build you create.",
        "parameters": [ { "$ref": "#/components/parameters/siteId" } ],
        "responses": {
          "200": { "description": "The build's status.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SiteStatus" },
              "example": { "status": "complete", "currentStep": "Complete", "progressPct": 100, "previewUrl": "https://example-plumbing.example.com/", "adminUrl": "https://example-plumbing.example.com/wp-admin/", "warnings": [], "error": null, "liveAt": "2026-10-01T15:04:10.000Z", "enhancingStartedAt": "2026-10-01T15:04:11.000Z", "completedAt": "2026-10-01T15:06:30.000Z", "enhancementError": null, "creditRefunded": false } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/SiteNotFound" }
        }
      }
    },
    "/api/integrations/site-status": {
      "post": {
        "tags": ["Site Creation"],
        "operationId": "postSiteStatus",
        "summary": "Check a build from a workflow",
        "description": "For workflow tools that can only send POST. Send {\"siteId\": \"...\"} with the same key. The answer is the same as GET /api/provision/{siteId}, plus ok, siteId and statusUrl. Same 401 and 404 rules.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorkflowStatusRequest" }, "example": { "siteId": "3f2a9c1e-5b7d-4e8a-9c0f-1a2b3c4d5e6f" } } } },
        "responses": {
          "200": { "description": "The build's status.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorkflowSiteStatus" } } } },
          "400": { "description": "The body is not valid JSON, siteId is missing or not a UUID, or the body has an extra field.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Invalid request", "details": ["siteId: required, a site ID (uuid)"] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/SiteNotFound" }
        }
      }
    },
    "/api/lead-gen/templates": {
      "get": {
        "tags": ["Lead Gen"],
        "operationId": "listLeadGenTemplates",
        "summary": "List batch templates",
        "description": "The designs a batch can use. Send an entry's key as templateKey, and its look as look, to POST /api/lead-gen/batches. Waterline (key river) has no look. The same set the MCP tool list_lead_gen_templates returns.",
        "responses": {
          "200": { "description": "The template list.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "templates"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "templates": { "type": "array", "items": { "$ref": "#/components/schemas/LeadGenTemplate" } } } },
              "example": { "ok": true, "templates": [
                { "id": "classic", "key": "ultimate", "look": "classic", "label": "Classic", "description": "Clean and modern, our standard look", "version": "2.0.0", "previewUrl": "https://dd104057-cedar-stone-remodeling.sites.showfirst.app/", "thumbnailUrl": "/img/look-picker/classic-2.jpg" },
                { "id": "waterline", "key": "river", "look": null, "label": "Waterline", "description": "Split-photo hero, navy top bar, cream bands, teal service cards", "version": "2.0.0", "previewUrl": "https://fa5768f0-larkspur-painting.sites.showfirst.app/", "thumbnailUrl": "/img/look-picker/waterline.jpg" } ] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/lead-gen/batches": {
      "post": {
        "tags": ["Lead Gen"],
        "operationId": "createLeadGenBatch",
        "summary": "Create a batch",
        "description": "Takes a list of businesses and builds a preview site for each one. A batch costs 1 credit per row, with a minimum of 5, taken when the batch is created. If your balance is lower than the cost, the request fails with 403 and nothing is created. A request that fails validation uses no credits. If every row fails and none completes, the full cost is returned to your account.\n\nMost fields accept a camelCase name or a snake_case name, such as primaryColor or primary_color. The batch is queued for building as soon as it is created; you do not need to start it.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadGenBatchCreateRequest" },
          "example": { "industry": "Plumbing", "templateKey": "ultimate", "look": "classic", "primaryColor": "#0B1F3A", "backgroundTheme": "light", "rows": [
            { "businessName": "Example Plumbing", "phone": "(555) 010-0100", "address": "100 Main St, Springfield, IL 62701", "websiteUrl": "https://example.com" },
            { "businessName": "Sample Drain Co", "email": "hello@example.com", "address": "Riverton" },
            { "google_cid": "12345678901234567890" } ] } } } },
        "responses": {
          "200": { "description": "The batch was created and queued.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadGenBatchCreated" },
              "example": { "ok": true, "batchId": "8d1e6f2a-4c3b-4a5e-9f70-2b1c0d9e8a7f", "batch": { "id": "8d1e6f2a-4c3b-4a5e-9f70-2b1c0d9e8a7f", "status": "pending", "industry": "Plumbing", "template_key": "ultimate", "look": "classic", "primary_color": "#0B1F3A", "background_theme": "light", "logo_size_px": 88, "contact_mode": "form", "total_count": 3, "credits_reserved": 5, "price_tool": 1, "error": null, "created_at": "2026-10-01T15:00:00.000Z", "updated_at": "2026-10-01T15:00:00.000Z" }, "creditsReserved": 5, "creditsRemaining": 95 } } } },
          "400": { "description": "The body is not valid JSON, or a field or row is invalid. See details. No credits are used.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" },
              "example": { "ok": false, "error": "Invalid lead-gen batch rows", "details": ["Row 2 must include business_name.", "Row 3 must not set industry; industry is batch-level only."] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Your balance is lower than the batch cost.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Not enough Lead Gen credits." } } } }
        }
      },
      "get": {
        "tags": ["Lead Gen"],
        "operationId": "listLeadGenBatches",
        "summary": "List batches",
        "description": "Up to 50 of your most recent batches, newest first. Rows are not included; use Get a batch for those. Only batches made with the key you send are listed.",
        "responses": {
          "200": { "description": "Your recent batches.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "batches"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "batches": { "type": "array", "maxItems": 50, "items": { "$ref": "#/components/schemas/LeadGenBatch" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/lead-gen/batches/{batchId}": {
      "get": {
        "tags": ["Lead Gen"],
        "operationId": "getLeadGenBatch",
        "summary": "Get a batch",
        "description": "The batch and its rows in items, in the order they were created. Each item also returns the row values you sent. A completed item has its link in site_url or, if that is empty, public_url.",
        "parameters": [ { "$ref": "#/components/parameters/batchId" } ],
        "responses": {
          "200": { "description": "The batch and its rows.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "batch", "items"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "batch": { "$ref": "#/components/schemas/LeadGenBatch" },
              "items": { "type": "array", "items": { "$ref": "#/components/schemas/LeadGenItem" } } } },
              "example": { "ok": true, "batch": { "id": "8d1e6f2a-4c3b-4a5e-9f70-2b1c0d9e8a7f", "status": "completed", "industry": "Plumbing", "total_count": 2, "credits_reserved": 5 }, "items": [ { "id": "c2b7e4d1-9a0f-4e3c-8b5d-6f1a2e3d4c5b", "status": "completed", "business_name": "Example Plumbing", "slug": "example-plumbing", "site_url": "https://example-plumbing.example.com/", "public_url": null, "error": null } ] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/BatchNotFound" }
        }
      },
      "patch": {
        "tags": ["Lead Gen"],
        "operationId": "updateLeadGenBatch",
        "summary": "Change batch design defaults",
        "description": "Send one or more of primaryColor, backgroundTheme, logoSizePx and language (camelCase or snake_case). The same rules as on create apply. A new color or background only reaches rows that did not set their own; the logo size reaches every row. language can change only while the batch is still pending or queued; after that the request returns 409.",
        "parameters": [ { "$ref": "#/components/parameters/batchId" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadGenBatchUpdateRequest" }, "example": { "primaryColor": "#173B57", "backgroundTheme": "dark" } } } },
        "responses": {
          "200": { "description": "The new defaults and how many rows took them.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadGenBatchUpdated" },
              "example": { "ok": true, "batchId": "8d1e6f2a-4c3b-4a5e-9f70-2b1c0d9e8a7f", "primaryColor": "#173B57", "backgroundTheme": "dark", "logoSizePx": 88, "language": "en", "updatedCount": 2, "logoUpdatedCount": 0, "logoSkippedCount": 0, "colorUpdatedCount": 2, "colorSkippedCount": 0, "backgroundUpdatedCount": 2, "backgroundSkippedCount": 0 } } } },
          "400": { "description": "The body is not valid JSON (\"Request body must be valid JSON.\"), none of the supported fields was sent (\"Send at least one supported setting.\"), or a value is invalid (\"Invalid batch settings\", see details).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" },
              "example": { "ok": false, "error": "Invalid batch settings", "details": ["backgroundTheme must be light or dark."] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/BatchNotFound" },
          "409": { "description": "language was sent, but the batch is already being built or is done.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" },
              "example": { "ok": false, "error": "language can only change before the batch is generated.", "details": ["This batch is processing; its pages are already written in en."] } } } }
        }
      }
    },
    "/api/lead-gen/batches/{batchId}/process": {
      "post": {
        "tags": ["Lead Gen"],
        "operationId": "processLeadGenBatch",
        "summary": "Check a batch is queued",
        "description": "You do not need to call this. A batch is queued when it is created. This route reports the batch's state and never starts a build a second time. A pending batch is marked queued; any other status is returned unchanged.",
        "parameters": [ { "$ref": "#/components/parameters/batchId" } ],
        "responses": {
          "200": { "description": "The batch's state.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "batchId", "queued", "status", "message"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "batchId": { "type": "string", "format": "uuid" },
              "queued": { "type": "boolean", "description": "true when the status is queued." },
              "status": { "$ref": "#/components/schemas/LeadGenBatchStatus" },
              "message": { "type": "string", "const": "The dedicated Lead Gen worker owns batch processing." } } },
              "example": { "ok": true, "batchId": "8d1e6f2a-4c3b-4a5e-9f70-2b1c0d9e8a7f", "queued": true, "status": "queued", "message": "The dedicated Lead Gen worker owns batch processing." } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/BatchNotFound" }
        }
      }
    },
    "/api/agency/v1/templates": {
      "get": {
        "tags": ["White-label Agency"],
        "operationId": "getAgencyTemplates",
        "summary": "Agency templates",
        "description": "templates (the same list as GET /api/templates) and themes (the themeId values you can send). Agency key only.",
        "responses": {
          "200": { "description": "Templates and themes.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "templates", "themes"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "templates": { "type": "array", "items": { "$ref": "#/components/schemas/SiteTemplate" } },
              "themes": { "type": "array", "items": { "type": "object", "required": ["themeId", "templateId", "name"], "properties": {
                "themeId": { "type": "string", "example": "classic" },
                "templateId": { "type": "string", "example": "template6" },
                "name": { "type": "string", "example": "Classic" } } } } } } } } },
          "401": { "$ref": "#/components/responses/AgencyUnauthorized" }
        }
      }
    },
    "/api/agency/v1/site-requests": {
      "post": {
        "tags": ["White-label Agency"],
        "operationId": "createAgencySiteRequest",
        "summary": "Create a site request",
        "description": "Takes every field of POST /api/provision, plus the client's email (required), autoApprove and themeId. With autoApprove true the build starts now and uses 1 site credit, with the same 403 and paused answers as POST /api/provision. Otherwise the request waits under Agency site requests on the API Keys page, and the credit is used when you click Approve. The client email is saved as the business email; we do not send it anything.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgencySiteRequest" },
          "example": { "businessName": "Example Plumbing", "email": "client@example.com", "industry": "Plumbing", "phone": "(555) 010-0100", "address": "100 Main St, Springfield, IL 62701", "themeId": "bold", "autoApprove": false } } } },
        "responses": {
          "200": { "description": "The request, with its approval and build status. When builds are paused and autoApprove is true, the paused body is returned instead and nothing is stored.",
            "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/AgencySiteRequestStatus" }, { "$ref": "#/components/schemas/PausedResponse" } ] } } } },
          "400": { "description": "The body is not valid JSON, email is missing or not an email address, autoApprove is not true or false, or a site field is invalid. See details.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Invalid site request", "details": ["email: required"] } } } },
          "401": { "$ref": "#/components/responses/AgencyUnauthorized" },
          "403": { "description": "autoApprove is true and your account has no credits left.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "No site credits remain." } } } },
          "500": { "description": "Creating the request failed on our side. Nothing was created and no credit was used. Send the request again.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "The request hit a transient storage error. Nothing was created and no credit was used. Please retry." } } } },
          "503": { "description": "Site creation is paused for maintenance.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "New site generation is temporarily in maintenance mode." } } } }
        }
      }
    },
    "/api/agency/v1/site-requests/{requestId}": {
      "get": {
        "tags": ["White-label Agency"],
        "operationId": "getAgencySiteRequest",
        "summary": "Check a site request",
        "description": "Approval status, build status, the site's public URL and any error. Any agency key on your account can read your requests; anything else gets the same 404 as an unknown id.",
        "parameters": [ { "name": "requestId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The requestId from Create a site request." } ],
        "responses": {
          "200": { "description": "The request's status.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgencySiteRequestStatus" } } } },
          "401": { "$ref": "#/components/responses/AgencyUnauthorized" },
          "404": { "description": "The id is unknown, or the request belongs to another account.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Request not found." } } } }
        }
      }
    },
    "/api/prospecting/credits": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "getProspectingCredits",
        "summary": "Lead credits left",
        "description": "The account's lead credits, the same ones the Search tab uses.",
        "responses": {
          "200": { "description": "The balance.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "lead_credits"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "lead_credits": { "type": "integer", "minimum": 0 } } }, "example": { "ok": true, "lead_credits": 1400 } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" }
        }
      }
    },
    "/api/prospecting/locations": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "findLocation",
        "summary": "Find a location",
        "description": "Place name to location_code. The best matches come first, up to 8.",
        "parameters": [ { "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "description": "A place name, such as Sarasota or Tampa, Florida.", "example": "Sarasota" } ],
        "responses": {
          "200": { "description": "Matching places, best first.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "locations"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "locations": { "type": "array", "maxItems": 8, "items": { "$ref": "#/components/schemas/Location" } } } },
              "example": { "ok": true, "locations": [ { "location_code": 1015192, "location_name": "Sarasota,Florida,United States", "location_type": "City" } ] } } } },
          "400": { "description": "q is missing.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "query is required, e.g. \"Tampa, Florida\"." } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/categories": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "listCategories",
        "summary": "List business categories",
        "description": "The business kinds a search can be narrowed to with exact_category_key. Public: no key is needed. Send q to filter by name.",
        "security": [ {} ],
        "parameters": [ { "name": "q", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only categories whose name contains this text.", "example": "roof" } ],
        "responses": {
          "200": { "description": "The categories.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "categories"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "categories": { "type": "array", "items": { "type": "object", "required": ["key", "name"], "properties": {
                "key": { "type": "string", "description": "The value to send as exact_category_key.", "example": "roofing_contractor" },
                "name": { "type": "string", "example": "Roofing contractor" } } } } } },
              "example": { "ok": true, "categories": [ { "key": "roofing_contractor", "name": "Roofing contractor" } ] } } } }
        }
      }
    },
    "/api/prospecting/searches": {
      "post": {
        "tags": ["Prospecting"],
        "operationId": "createSearch",
        "summary": "Plan or start a search",
        "description": "One kind of business in one place. Two phases: without confirm the reply is the plan (what the search would hold, and your balance) and nothing starts. Send the same request with \"confirm\": true to start it.\n\nA search holds 1 lead credit for each business it may return: its depth, or twice that with verify_emails on (the default). When it finishes you pay 1 per business found with a phone or website, plus 1 per verified email address when verify_emails is on, and the rest come back. With 1 to 99 lead credits left, a depth 100 search holds your balance and returns at most that many businesses (max_leads); with verify_emails on, a balance below 200 does the same and returns at most half your balance.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchRequest" },
          "examples": {
            "plan": { "summary": "Get the plan", "value": { "keyword": "roofing contractor", "location_code": 1015192, "depth": 300 } },
            "start": { "summary": "Start the search", "value": { "keyword": "roofing contractor", "location_code": 1015192, "depth": 300, "confirm": true } } } } } },
        "responses": {
          "200": { "description": "Without confirm: the plan. With confirm true: the started search and its job_id.",
            "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/SearchPlanResponse" }, { "$ref": "#/components/schemas/SearchStarted" } ] },
              "examples": {
                "plan": { "summary": "The plan", "value": { "ok": true, "requires_confirmation": true, "plan": { "keyword": "roofing contractor", "location_code": 1015192, "location_name": null, "depth": 300, "verify_emails": true, "lead_credits_held": 600, "lead_credits_balance": 1400 }, "next": "Nothing has started. Send the same request with confirm: true to run it." } },
                "started": { "summary": "Started", "value": { "ok": true, "job_id": "6f1c2a9e-1b0d-4c55-9a51-0d6c1e1f7a20", "status": "enriching", "keyword": "roofing contractor", "location_code": 1015192, "depth": 300, "lead_credits_held": 600 } } } } } },
          "400": { "description": "A field is missing or invalid: keyword missing, a depth that is not 100, 300, 500 or 700, a radius_miles that is not 10, 25, 50 or 100, a website that is not any or none, or a verify_emails that is not true or false.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "depth must be one of 100, 300, 500, 700." } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/OutOfLeadCredits" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "description": "No place matched location.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "No location matched \"Nowhere, ZZ\". Try \"City, State\" or a larger nearby city." } } } },
          "429": { "$ref": "#/components/responses/Busy" },
          "502": { "$ref": "#/components/responses/SearchServiceProblem" },
          "503": { "$ref": "#/components/responses/SearchServiceProblem" }
        }
      }
    },
    "/api/prospecting/searches/{jobId}": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "getSearch",
        "summary": "A search's progress",
        "description": "Poll every 10 to 20 seconds. A search usually takes a few minutes. Credits settle the first time a status check sees a final status, so keep polling until the search is done.",
        "parameters": [ { "$ref": "#/components/parameters/jobId" } ],
        "responses": {
          "200": { "description": "The search's status.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchJob" },
              "example": { "job_id": "6f1c2a9e-1b0d-4c55-9a51-0d6c1e1f7a20", "status": "completed", "keyword": "roofing contractor", "total_businesses": 212, "enriched_count": 212, "failed_count": 0, "progress": 100 } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "$ref": "#/components/responses/SearchNotFound" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/searches/{jobId}/results": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "getSearchResults",
        "summary": "A page of businesses, or a CSV",
        "description": "A page of businesses with the same fields as the results table in the Search tab, or a CSV file when format is a CSV format. The filters work the same way for both. Each business has 28 fields; see Business.",
        "parameters": [
          { "$ref": "#/components/parameters/jobId" },
          { "$ref": "#/components/parameters/offset" },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/page" },
          { "$ref": "#/components/parameters/page_size" },
          { "$ref": "#/components/parameters/has_website" },
          { "$ref": "#/components/parameters/has_email" },
          { "$ref": "#/components/parameters/has_phone" },
          { "$ref": "#/components/parameters/has_mobile_phone" },
          { "$ref": "#/components/parameters/has_verified_email" },
          { "$ref": "#/components/parameters/format" }
        ],
        "responses": {
          "200": { "description": "A page of businesses (format json), or a CSV file.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ResultsPage" },
                "example": { "ok": true, "job_id": "6f1c2a9e-1b0d-4c55-9a51-0d6c1e1f7a20", "total": 38, "offset": 0, "limit": 2, "next_offset": 2, "businesses": [ { "name": "Gulf Coast Roof Repair", "category": "Roofing contractor", "website": null, "email": "office@example.com", "phone": "(941) 555-0142", "owner": "Dana Ruiz", "contact": { "address": "1200 Main St", "city": "Sarasota", "state_code": "FL" }, "reputation": { "rating": 4.8, "reviews": 61, "is_claimed": true }, "site_report": null } ] } },
              "text/csv": { "schema": { "type": "string" }, "example": "name,phone,email,website\nGulf Coast Roof Repair,(941) 555-0142,office@example.com,\n" } } },
          "400": { "description": "A filter or format value is invalid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "format must be one of simple, advanced, ghl, instantly, batch." } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "$ref": "#/components/responses/SearchNotFound" },
          "409": { "description": "The search has not finished yet. Keep polling.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "This search has not finished yet. Check search_status until it is completed." } } } },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaigns": {
      "post": {
        "tags": ["Prospecting"],
        "operationId": "createCampaign",
        "summary": "Plan or start a campaign",
        "description": "One keyword across many places. Without confirm the reply is the plan: the lead estimate, about how long it takes, and lead_credits_held. With \"confirm\": true the campaign starts.\n\nA campaign holds places × depth_per_location, or max_leads if that is lower, twice that with verify_emails on. It settles the same way as a search when it finishes or is stopped. A campaign moves on to its next places each time you check it, so poll its results about once a minute.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CampaignCreateRequest" },
          "example": { "keyword": "plumber", "locations": ["Tampa, Florida", "Sarasota, Florida", "Bradenton, Florida"], "depth_per_location": 100, "max_leads": 250, "confirm": true } } } },
        "responses": {
          "200": { "description": "Without confirm: the plan. With confirm true: the started campaign.",
            "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/CampaignPlanResponse" }, { "$ref": "#/components/schemas/CampaignStarted" } ] },
              "examples": {
                "started": { "summary": "Started", "value": { "ok": true, "campaign_id": "b2a7f0de-55c1-4f0e-8d0b-3c0f7c1a9e44", "lead_credits_held": 500 } } } } } },
          "400": { "description": "A field is missing or invalid: keyword missing, neither location_codes nor locations sent, more than 200 locations, a depth_per_location that is not 100, 300, 500 or 700, a max_leads that is not a positive whole number, a website that is not any or none, or an idempotency_key that is not a UUID.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "depth_per_location must be one of 100, 300, 500, 700." } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/OutOfLeadCredits" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "description": "No place matched one of the locations.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "No location matched \"Nowhere, ZZ\". Try \"City, State\" or a larger nearby city." } } } },
          "429": { "$ref": "#/components/responses/Busy" },
          "502": { "$ref": "#/components/responses/SearchServiceProblem" },
          "503": { "$ref": "#/components/responses/SearchServiceProblem" }
        }
      },
      "get": {
        "tags": ["Prospecting"],
        "operationId": "listCampaigns",
        "summary": "List campaigns",
        "description": "Your campaigns, newest first, with the same fields as a campaign's progress.",
        "responses": {
          "200": { "description": "Your campaigns.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "campaigns"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "campaigns": { "type": "array", "items": { "$ref": "#/components/schemas/Campaign" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaigns/preview": {
      "post": {
        "tags": ["Prospecting"],
        "operationId": "previewCampaign",
        "summary": "Plan a campaign",
        "description": "The plan for a campaign, without starting it: the same answer as POST /api/prospecting/campaigns without confirm. Nothing is held.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CampaignCreateRequest" },
          "example": { "keyword": "plumber", "locations": ["Tampa, Florida", "Sarasota, Florida"], "depth_per_location": 100 } } } },
        "responses": {
          "200": { "description": "The plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CampaignPlanResponse" } } } },
          "400": { "description": "A field is missing or invalid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "description": "No place matched one of the locations.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaign-locations": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "listCampaignLocations",
        "summary": "Browse campaign locations",
        "description": "Places a campaign can cover, by country and region, as location codes. Use it to build the location_codes list for a campaign.",
        "parameters": [
          { "name": "country", "in": "query", "required": false, "schema": { "type": "string" }, "description": "A two-letter country code, such as US.", "example": "US" },
          { "name": "region", "in": "query", "required": false, "schema": { "type": "string" }, "description": "A state or region code within the country, such as FL.", "example": "FL" },
          { "name": "q", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Only places whose name contains this text.", "example": "Tampa" },
          { "$ref": "#/components/parameters/offset" },
          { "$ref": "#/components/parameters/limit" }
        ],
        "responses": {
          "200": { "description": "A page of places.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "locations"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "total": { "type": "integer" },
              "offset": { "type": "integer" },
              "limit": { "type": "integer" },
              "next_offset": { "type": ["integer", "null"] },
              "locations": { "type": "array", "items": { "$ref": "#/components/schemas/Location" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaigns/{campaignId}": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "getCampaign",
        "summary": "A campaign's progress",
        "description": "Each check also moves the campaign on to its next places, and the first check that sees a final status settles its lead credits. needs_attention means the campaign paused because your lead credits ran low: add credits, then resume it.",
        "parameters": [ { "$ref": "#/components/parameters/campaignId" } ],
        "responses": {
          "200": { "description": "The campaign.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Campaign" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "$ref": "#/components/responses/CampaignNotFound" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaigns/{campaignId}/results": {
      "get": {
        "tags": ["Prospecting"],
        "operationId": "getCampaignResults",
        "summary": "Progress and a page of businesses, or a CSV",
        "description": "The campaign's progress plus a page of the businesses found so far (same offset, limit, filters and format as a search), or a CSV file when format is a CSV format. Poll about once a minute; stop when status is completed, stopped or failed. A campaign that has found nothing yet answers with an empty page.",
        "parameters": [
          { "$ref": "#/components/parameters/campaignId" },
          { "$ref": "#/components/parameters/offset" },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/page" },
          { "$ref": "#/components/parameters/page_size" },
          { "$ref": "#/components/parameters/has_website" },
          { "$ref": "#/components/parameters/has_email" },
          { "$ref": "#/components/parameters/has_phone" },
          { "$ref": "#/components/parameters/has_mobile_phone" },
          { "$ref": "#/components/parameters/has_verified_email" },
          { "$ref": "#/components/parameters/format" }
        ],
        "responses": {
          "200": { "description": "Progress and a page of businesses (format json), or a CSV file.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/CampaignResultsPage" } },
              "text/csv": { "schema": { "type": "string" } } } },
          "400": { "description": "A filter or format value is invalid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "$ref": "#/components/responses/CampaignNotFound" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaigns/{campaignId}/pause": {
      "post": {
        "tags": ["Prospecting"],
        "operationId": "pauseCampaign",
        "summary": "Pause a campaign",
        "description": "Pauses a running campaign. Resume it with POST .../resume.",
        "parameters": [ { "$ref": "#/components/parameters/campaignId" } ],
        "responses": {
          "200": { "$ref": "#/components/responses/CampaignActionDone" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "$ref": "#/components/responses/CampaignNotFound" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaigns/{campaignId}/resume": {
      "post": {
        "tags": ["Prospecting"],
        "operationId": "resumeCampaign",
        "summary": "Resume a paused campaign",
        "description": "Resumes a paused campaign, including one that paused with needs_attention after you added credits.",
        "parameters": [ { "$ref": "#/components/parameters/campaignId" } ],
        "responses": {
          "200": { "$ref": "#/components/responses/CampaignActionDone" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "$ref": "#/components/responses/CampaignNotFound" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/campaigns/{campaignId}/stop": {
      "post": {
        "tags": ["Prospecting"],
        "operationId": "stopCampaign",
        "summary": "Stop a campaign for good",
        "description": "Stops the campaign. It cannot be resumed. Its lead credits settle on the next status check: you pay for what it found and the rest come back.",
        "parameters": [ { "$ref": "#/components/parameters/campaignId" } ],
        "responses": {
          "200": { "$ref": "#/components/responses/CampaignActionDone" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "404": { "$ref": "#/components/responses/CampaignNotFound" },
          "429": { "$ref": "#/components/responses/Busy" }
        }
      }
    },
    "/api/prospecting/enrich": {
      "post": {
        "tags": ["Prospecting"],
        "operationId": "enrichWebsites",
        "summary": "Look up websites",
        "description": "Finds emails, phones, social links and runs the website check for 1 to 10 websites you already have. 1 lead credit per website, held when the lookup starts and given back if it cannot start. Returns a job_id: poll GET /api/prospecting/searches/{jobId} and read GET .../results like a search. Send domains, or businesses when you know the names, or leads.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnrichRequest" },
          "examples": {
            "domains": { "summary": "Websites only", "value": { "domains": ["gulfcoastroofing.com", "https://www.example-plumbing.com"] } },
            "businesses": { "summary": "With names", "value": { "businesses": [ { "business_name": "Gulf Coast Roofing", "website": "gulfcoastroofing.com", "phone": "(941) 555-0142" } ] } },
            "leads": { "summary": "With place", "value": { "leads": [ { "domain": "gulfcoastroofing.com", "business_name": "Gulf Coast Roofing", "city": "Sarasota", "state": "FL" } ] } } } } } },
        "responses": {
          "200": { "description": "The lookup started. job_id is null and status completed when every website given is on your do-not-include list, in which case nothing is held.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "job_id", "status", "lead_credits_held"], "properties": {
              "ok": { "type": "boolean", "const": true },
              "job_id": { "type": ["string", "null"], "format": "uuid" },
              "status": { "type": "string", "enum": ["enriching", "completed"] },
              "lead_credits_held": { "type": "integer", "minimum": 0 },
              "next": { "type": "string" } }, "additionalProperties": true },
              "example": { "ok": true, "job_id": "0d4e9b61-7a2c-4c1e-9f35-5b8a2c6e1d07", "status": "enriching", "lead_credits_held": 2 } } } },
          "400": { "description": "No websites were sent, more than 10 were sent, or an entry has no website.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "At most 10 websites per call." } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/OutOfLeadCredits" },
          "403": { "$ref": "#/components/responses/NoAccount" },
          "429": { "$ref": "#/components/responses/Busy" },
          "502": { "$ref": "#/components/responses/SearchServiceProblem" },
          "503": { "$ref": "#/components/responses/SearchServiceProblem" }
        }
      }
    },
    "/docs/skills/prospecting/SKILL.md": {
      "get": {
        "tags": ["Resources"],
        "operationId": "getProspectingSkill",
        "summary": "The prospecting skill",
        "description": "A Markdown skill file that teaches a coding agent to use the Prospecting API and MCP tools: the confirm-first rule, the routes, and the credit arithmetic. No key is needed.",
        "security": [ {} ],
        "responses": {
          "200": { "description": "The skill file.", "content": { "text/markdown": { "schema": { "type": "string" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer YOUR_API_KEY. Account keys begin with lgen_; white-label agency keys begin with sf_agency_. Create keys on the API Keys page of your dashboard. The full key is shown once; treat it like a password."
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "The same key, sent as an x-api-key header instead of a bearer token."
      }
    },
    "parameters": {
      "siteId": { "name": "siteId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The siteId returned by Create a site." },
      "batchId": { "name": "batchId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The batchId returned by Create a batch." },
      "jobId": { "name": "jobId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The job_id returned when a search or a website lookup started." },
      "campaignId": { "name": "campaignId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The campaign_id returned when the campaign started." },
      "offset": { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "Skip this many businesses. Use next_offset from the last page." },
      "limit": { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 }, "description": "Businesses per page, 1 to 100." },
      "page": { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1 }, "description": "Page number, starting at 1. Another way to say offset: page 2 with page_size 25 is offset 25." },
      "page_size": { "name": "page_size", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100 }, "description": "Businesses per page when you send page. Same range as limit." },
      "has_website": { "name": "has_website", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "true keeps only businesses whose Google listing shows a website, false only those without." },
      "has_email": { "name": "has_email", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "true keeps only businesses with an email address, false only those without." },
      "has_phone": { "name": "has_phone", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "true keeps only businesses with a phone number, false only those without." },
      "has_mobile_phone": { "name": "has_mobile_phone", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "true keeps only businesses with a mobile number, false only those without." },
      "has_verified_email": { "name": "has_verified_email", "in": "query", "required": false, "schema": { "type": "boolean" }, "description": "true keeps only businesses whose email passed the mailbox check (verified). Catch-all, unknown and unchecked emails are left out. false keeps the rest." },
      "format": { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": ["json", "simple", "advanced", "ghl", "instantly", "batch"], "default": "json" }, "description": "json (default) returns a page of businesses. The others return a CSV file: simple (the main columns), advanced (every column), ghl (ready to import into GoHighLevel), instantly (ready to import into Instantly), batch (ready to upload as a Lead Gen batch)." }
    },
    "responses": {
      "Unauthorized": { "description": "The key is missing, unknown or revoked.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Missing or invalid bearer API key." } } } },
      "AgencyUnauthorized": { "description": "The key is missing, not an agency key (sf_agency_), unknown or revoked.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Missing or invalid agency API key." } } } },
      "NoAccount": { "description": "The key is not linked to a ShowFirst account. Make a new key on the API Keys page.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "This API key is not linked to a ShowFirst account. Make a key on the API Keys page of the dashboard." } } } },
      "SiteNotFound": { "description": "The ID is unknown, or another key created the build.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Site not found." } } } },
      "BatchNotFound": { "description": "The ID is unknown, or another key created the batch.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Batch not found." } } } },
      "SearchNotFound": { "description": "The search is unknown or belongs to another account.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Search not found." } } } },
      "CampaignNotFound": { "description": "The campaign is unknown or belongs to another account.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "Campaign not found." } } } },
      "OutOfLeadCredits": { "description": "Not enough lead credits. Nothing started and nothing was held.",
        "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": {
          "balance": { "type": "integer", "description": "Your lead credits." },
          "required": { "type": "integer", "description": "What this request needs." } } } ] },
          "example": { "ok": false, "error": "You're out of lead credits.", "details": ["This search needs 300 lead credits; you have 120."], "balance": 120, "required": 300 } } } },
      "Busy": { "description": "The search service is busy. Wait a few seconds and send the request again. Keep to about one request a second.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "SearchServiceProblem": { "description": "The search service had a problem. Try again in a moment. Any credits held for a request that did not start came back.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "CampaignActionDone": { "description": "The action was applied. campaign is the campaign's status right after.",
        "content": { "application/json": { "schema": { "type": "object", "required": ["ok"], "properties": {
          "ok": { "type": "boolean", "const": true },
          "campaign": { "$ref": "#/components/schemas/Campaign" } }, "additionalProperties": true } } } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error body. details, when present, has one line per problem.",
        "required": ["ok", "error"],
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string", "description": "What went wrong, in one sentence." },
          "details": { "type": "array", "items": { "type": "string" }, "description": "One line per problem, on validation errors." }
        },
        "additionalProperties": true
      },
      "PausedResponse": {
        "type": "object",
        "description": "Builds are paused. No site was queued and no credit was used. Try again later.",
        "required": ["ok", "paused", "message"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "paused": { "type": "boolean", "const": true },
          "message": { "type": "string", "enum": ["Today's free builds are used up — try again tomorrow.", "Free tests are paused right now — check back soon."], "description": "The first applies to accounts that have only ever had signup credits." }
        }
      },
      "SiteTemplate": {
        "type": "object",
        "description": "A template a new site can use.",
        "required": ["id", "templateId", "themeId", "name", "description", "version", "hasServices", "hasServiceAreas", "featureImage", "previewUrl"],
        "properties": {
          "id": { "type": "string", "example": "classic" },
          "templateId": { "type": "string", "description": "Send as templateId to POST /api/provision.", "example": "template6" },
          "themeId": { "type": ["string", "null"], "description": "Send as themeId (the same as look). null when the template has no looks.", "example": "classic" },
          "name": { "type": "string", "example": "Classic" },
          "description": { "type": "string", "example": "Clean and modern, our standard look" },
          "version": { "type": ["string", "null"], "example": "2.0.0" },
          "hasServices": { "type": "boolean" },
          "hasServiceAreas": { "type": "boolean" },
          "featureImage": { "type": ["string", "null"], "description": "A picture of the template." },
          "previewUrl": { "type": ["string", "null"], "description": "A live example." }
        }
      },
      "HexColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "Six-digit hex color.", "example": "#2F6B55" },
      "Language": { "type": "string", "enum": ["en", "es", "fr", "de", "it", "ro", "tr"], "default": "en", "description": "The language the site is written in." },
      "PriceToolCalcId": { "type": "string", "enum": ["decks", "fencing", "gutters", "home_construction", "house_cleaning", "hvac", "landscaping", "painting", "pest_control", "plumbing", "windows_doors"] },
      "SiteCreateRequest": {
        "type": "object",
        "description": "The fields of POST /api/provision.",
        "required": ["businessName"],
        "properties": {
          "businessName": { "type": "string", "minLength": 1, "description": "Must not be blank." },
          "phone": { "type": "string", "description": "If sent: at least 10 digits for a US business. For a business in Mexico, Guatemala, Peru, Colombia, Ecuador, Costa Rica or Argentina, 8 to 12 digits. For any other country, 7 to 15 digits." },
          "address": { "type": "string", "description": "Free text. A full street address such as 100 Main St, Springfield, IL 62701, or just a city or service area such as Springfield. Built from the address fields when you leave it out." },
          "addressLine1": { "type": "string", "maxLength": 200, "description": "Street address." },
          "addressLine2": { "type": "string", "maxLength": 200, "description": "Suite, unit or floor." },
          "addressCity": { "type": "string" },
          "addressRegion": { "type": "string", "description": "State or region." },
          "addressPostalCode": { "type": "string" },
          "countryCode": { "type": "string", "minLength": 2, "maxLength": 2, "description": "Two-letter country code such as US or GB. Any country." },
          "hasPublicBusinessAddress": { "type": "boolean", "default": true, "description": "When false, the street lines and postal code are dropped and never appear on the site. City, region and country stay. Same as publicAddress." },
          "publicAddress": { "type": "boolean", "description": "Same as hasPublicBusinessAddress." },
          "googlePlaceId": { "type": "string", "description": "The business's Google Place ID. Kept with the site's business details." },
          "look": { "type": "string", "enum": ["classic", "bold", "night", "angle"], "default": "classic", "description": "An unknown value is built as classic." },
          "themeId": { "type": "string", "description": "Same as look. Send the themeId from GET /api/templates." },
          "templateId": { "type": "string", "description": "The templateId from GET /api/templates." },
          "industry": { "type": "string", "description": "The type of business, such as Plumbing." },
          "email": { "type": "string", "format": "email", "description": "The business email." },
          "businessWebsite": { "type": "string", "format": "uri", "description": "Must start with http:// or https://." },
          "aboutBusiness": { "type": "string", "description": "A description of the business." },
          "services": { "type": "array", "items": { "type": "string" }, "description": "Services the business offers. Up to 9, one page each; services past the ninth are not built." },
          "serviceAreas": { "type": "array", "items": { "type": "string" }, "description": "Towns or areas the business serves. Up to 9, one page each; areas past the ninth are not built." },
          "businessHours": { "type": "string", "description": "Opening hours as free text, or as a JSON-encoded string keyed by day, such as {\"monday\":{\"open\":\"09:00\",\"close\":\"17:00\"},\"sunday\":\"closed\"}." },
          "businessHoursMode": { "type": "string", "enum": ["weekly", "open_24_hours", "do_not_display"], "description": "Same as hoursMode (weekly, always_open, hidden)." },
          "hoursMode": { "type": "string", "enum": ["weekly", "always_open", "hidden"], "description": "Same as businessHoursMode." },
          "businessHoursByDay": { "type": "array", "maxItems": 7, "description": "Up to 7 entries.", "items": { "type": "object", "required": ["day"], "properties": {
            "day": { "type": "string", "enum": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] },
            "open": { "type": "string", "description": "Such as 9:00 AM or 09:00." },
            "close": { "type": "string", "description": "Such as 5:00 PM or 17:00." },
            "closed": { "type": "boolean", "description": "true for a day the business is closed, instead of open and close." } } } },
          "logoUrl": { "type": "string", "format": "uri", "description": "Must start with http:// or https://." },
          "primaryColor": { "allOf": [ { "$ref": "#/components/schemas/HexColor" } ], "description": "If you leave it out, a default color is used and a warning is added to the build." },
          "secondaryColor": { "$ref": "#/components/schemas/HexColor" },
          "backgroundTheme": { "type": "string", "enum": ["light", "dark"] },
          "primaryLanguage": { "allOf": [ { "$ref": "#/components/schemas/Language" } ], "description": "The site's content, page addresses, navigation and forms are written in this language. Any other value is rejected with a 400." },
          "country": { "type": "string", "maxLength": 60, "description": "Filled from countryCode when you leave it out." },
          "customerModel": { "type": "string", "enum": ["visit_business", "travel_to_customers", "hybrid", "remote_online", "visit", "travel", "remote"], "default": "travel_to_customers", "description": "visit_business and hybrid need a public address." },
          "reviews": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["author", "text", "rating"], "properties": {
            "author": { "type": "string" },
            "text": { "type": "string" },
            "rating": { "type": "integer", "minimum": 1, "maximum": 5, "description": "A whole number from 1 to 5." },
            "source": { "type": "string" },
            "date": { "type": "string" },
            "url": { "type": "string", "format": "uri" } } } },
          "skipImages": { "type": "boolean", "default": false, "description": "When true, image generation is skipped and the build lists a warning." },
          "priceTool": { "type": "boolean", "description": "Adds a price tool to the site. On by default when priceToolCalcId is set or the industry matches a trade that has a price tool. Send false to leave it off." },
          "priceToolCalcId": { "$ref": "#/components/schemas/PriceToolCalcId" },
          "createServicePages": { "type": "boolean", "default": true, "description": "false: no page for each service; the services are listed on the home page and the services page." },
          "createServiceAreaPages": { "type": "boolean", "default": true, "description": "Service-area pages are built for travel_to_customers and hybrid; false leaves them out." },
          "createLocationPages": { "type": "boolean", "default": true, "description": "Location pages are built for visit_business and hybrid; false leaves them out." },
          "locations": { "type": "array", "maxItems": 12, "description": "Up to 12, one page each. With none sent, the business address is the one location.", "items": { "type": "object", "required": ["name"], "properties": {
            "name": { "type": "string" },
            "address": { "type": "string" },
            "phone": { "type": "string" },
            "email": { "type": "string" },
            "hours": { "type": "string" },
            "hoursMode": { "type": "string", "enum": ["weekly", "always_open", "hidden"] },
            "mapUrl": { "type": "string", "format": "uri" },
            "description": { "type": "string" },
            "content": { "type": "string" } } } }
        }
      },
      "SiteCreateAccepted": {
        "type": "object",
        "description": "The 202 answer: the build is queued.",
        "required": ["slug", "siteId", "previewUrl", "statusUrl"],
        "properties": {
          "slug": { "type": "string", "description": "Made from the business name. If that slug is already taken, a number is added, such as example-plumbing-2." },
          "siteId": { "type": "string", "format": "uuid", "description": "Keep it: you need it to check the build." },
          "previewUrl": { "type": ["string", "null"], "description": "null until the build reports it." },
          "statusUrl": { "type": "string", "description": "The path of GET /api/provision/{siteId}." }
        }
      },
      "SiteStatus": {
        "type": "object",
        "description": "A build's status.",
        "required": ["status", "currentStep", "progressPct", "previewUrl", "adminUrl", "warnings", "error", "liveAt", "enhancingStartedAt", "completedAt", "enhancementError", "creditRefunded"],
        "properties": {
          "status": { "type": "string", "enum": ["queued", "provisioning", "live", "enhancing", "complete", "failed"], "description": "complete and failed are final. The preview link works from live: the main pages are published first, and the other pages and the photos are added during enhancing." },
          "currentStep": { "type": ["string", "null"], "description": "A short description of the step in progress." },
          "progressPct": { "type": "integer", "minimum": 0, "maximum": 100 },
          "previewUrl": { "type": ["string", "null"], "description": "The site's address. null until the build reports it." },
          "adminUrl": { "type": ["string", "null"], "description": "The site's WordPress admin address." },
          "warnings": { "type": "array", "items": { "type": "string" }, "description": "Each one names a value that was defaulted, ignored or skipped." },
          "error": { "type": ["string", "null"], "description": "Why the build failed, or null." },
          "liveAt": { "type": ["string", "null"], "format": "date-time", "description": "When the build first reached live." },
          "enhancingStartedAt": { "type": ["string", "null"], "format": "date-time", "description": "When the build first reached enhancing." },
          "completedAt": { "type": ["string", "null"], "format": "date-time", "description": "When the build reached complete or failed." },
          "enhancementError": { "type": ["string", "null"], "description": "An error from the enhancing step, or null. The site stays live and the build still ends complete; no credit is returned." },
          "creditRefunded": { "type": "boolean", "description": "true when a failed build's credit has been returned to your account. A failed build returns its credit once, on its own." }
        }
      },
      "WorkflowStatusRequest": {
        "type": "object",
        "required": ["siteId"],
        "properties": { "siteId": { "type": "string", "format": "uuid" } },
        "additionalProperties": false
      },
      "WorkflowSiteStatus": {
        "allOf": [
          { "type": "object", "required": ["ok", "siteId", "statusUrl"], "properties": {
            "ok": { "type": "boolean", "const": true },
            "siteId": { "type": "string", "format": "uuid" },
            "statusUrl": { "type": "string", "description": "The path of GET /api/provision/{siteId}." } } },
          { "$ref": "#/components/schemas/SiteStatus" }
        ]
      },
      "AgencySiteRequest": {
        "allOf": [
          { "$ref": "#/components/schemas/SiteCreateRequest" },
          { "type": "object", "required": ["email"], "properties": {
            "email": { "type": "string", "format": "email", "description": "Required. Your client's email. It is saved as the business email. We do not send it anything." },
            "autoApprove": { "type": "boolean", "default": false, "description": "When true, the build starts now and uses 1 site credit. Otherwise the request waits for your approval on the API Keys page." },
            "themeId": { "type": "string", "description": "Same as look." } } }
        ]
      },
      "AgencySiteRequestStatus": {
        "type": "object",
        "description": "An agency site request: approval status, build status, the site's public URL and any error.",
        "required": ["ok", "requestId", "businessName", "email", "approvalStatus", "status", "siteId", "statusUrl", "siteUrl", "previewUrl", "adminUrl", "progressPct", "currentStep", "warnings", "error", "createdAt", "decidedAt"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "requestId": { "type": "string", "format": "uuid" },
          "businessName": { "type": "string" },
          "email": { "type": "string", "description": "The client email you sent." },
          "approvalStatus": { "type": "string", "enum": ["pending", "approved", "rejected"] },
          "status": { "type": "string", "enum": ["awaiting_approval", "rejected", "queued", "provisioning", "live", "enhancing", "complete", "failed"], "description": "The build status, or awaiting_approval / rejected before a build exists." },
          "siteId": { "type": ["string", "null"], "format": "uuid" },
          "statusUrl": { "type": "string", "description": "The path of GET /api/agency/v1/site-requests/{requestId}." },
          "siteUrl": { "type": ["string", "null"], "description": "The site's public address: its own domain once it has one, otherwise the preview address." },
          "previewUrl": { "type": ["string", "null"] },
          "adminUrl": { "type": ["string", "null"] },
          "progressPct": { "type": "integer", "minimum": 0, "maximum": 100 },
          "currentStep": { "type": ["string", "null"] },
          "warnings": { "type": "array", "items": { "type": "string" } },
          "error": { "type": ["string", "null"], "description": "Why the build failed, or why the last approval could not queue it, or null." },
          "createdAt": { "type": "string", "format": "date-time" },
          "decidedAt": { "type": ["string", "null"], "format": "date-time", "description": "When the request was approved or rejected." }
        }
      },
      "LeadGenTemplate": {
        "type": "object",
        "description": "A design a batch can use.",
        "required": ["id", "key", "look", "label", "description", "version", "previewUrl", "thumbnailUrl"],
        "properties": {
          "id": { "type": "string", "example": "classic" },
          "key": { "type": "string", "enum": ["ultimate", "river"], "description": "Send as templateKey." },
          "look": { "type": ["string", "null"], "description": "Send as look. null for Waterline (key river), which has no looks.", "example": "classic" },
          "label": { "type": "string", "example": "Classic" },
          "description": { "type": "string", "example": "Clean and modern, our standard look" },
          "version": { "type": "string", "example": "2.0.0" },
          "previewUrl": { "type": ["string", "null"], "description": "A live example." },
          "thumbnailUrl": { "type": ["string", "null"], "description": "A picture of the design." }
        }
      },
      "ContactMode": { "type": "string", "enum": ["form", "scheduler", "schedule", "booking", "survey"], "default": "form", "description": "form (default) or scheduler. schedule, booking and survey also mean scheduler. Any other value is treated as form." },
      "LeadGenRow": {
        "type": "object",
        "description": "One business in a batch. Most fields accept a camelCase or a snake_case name. A row must not contain industry, a template field (templateKey, template_key, templateName, template_name) or a price tool field (priceTool, price_tool, priceToolCalcId, price_tool_calc_id): these are set once per batch, and a row that has one is rejected with 400.",
        "properties": {
          "businessName": { "type": "string", "minLength": 1, "description": "Required unless the row has a Google ID. Must not be blank when sent. Also business_name." },
          "business_name": { "type": "string", "minLength": 1, "description": "Same as businessName." },
          "googlePlaceId": { "type": "string", "description": "A Google Place ID. When the batch runs, the business name, address, phone, website and opening hours are filled in from Google. Fields you send yourself are kept. Send only one Google ID per row. Also google_place_id." },
          "google_place_id": { "type": "string", "description": "Same as googlePlaceId." },
          "googleCid": { "type": "string", "description": "A Google CID (the number in a Google Maps ?cid= link). Works like googlePlaceId. Send only one Google ID per row. Also google_cid." },
          "google_cid": { "type": "string", "description": "Same as googleCid." },
          "phone": { "type": "string" },
          "email": { "type": "string" },
          "address": { "type": "string", "description": "A street address or a town." },
          "logoUrl": { "type": "string", "description": "A link to the logo image. Also logo_url." },
          "logo_url": { "type": "string", "description": "Same as logoUrl." },
          "websiteUrl": { "type": "string", "description": "The business's current website. When logoUrl is empty, the build looks for a logo there. Also website_url." },
          "website_url": { "type": "string", "description": "Same as websiteUrl." },
          "businessHours": { "type": "string", "description": "Replaces the batch value for this row. Also business_hours." },
          "business_hours": { "type": "string", "description": "Same as businessHours." },
          "primaryColor": { "allOf": [ { "$ref": "#/components/schemas/HexColor" } ], "description": "Replaces the batch value for this row. Also primary_color." },
          "primary_color": { "allOf": [ { "$ref": "#/components/schemas/HexColor" } ], "description": "Same as primaryColor." },
          "backgroundTheme": { "type": "string", "enum": ["light", "dark"], "description": "Replaces the batch value for this row. Also background_theme." },
          "background_theme": { "type": "string", "enum": ["light", "dark"], "description": "Same as backgroundTheme." },
          "contactMode": { "allOf": [ { "$ref": "#/components/schemas/ContactMode" } ], "description": "Same values as the batch field. Replaces the batch value for this row. Also contact_mode." },
          "contact_mode": { "allOf": [ { "$ref": "#/components/schemas/ContactMode" } ], "description": "Same as contactMode." }
        },
        "additionalProperties": true
      },
      "LeadGenBatchCreateRequest": {
        "type": "object",
        "description": "The batch fields apply to every row. Most fields accept a camelCase or a snake_case name.",
        "required": ["rows"],
        "properties": {
          "rows": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "$ref": "#/components/schemas/LeadGenRow" }, "description": "1 to 500 rows. At most 100 rows with a Google ID. A row with a Google ID and no business name costs the same credit as any other row; a row whose Google listing cannot be found, and that has no business name of its own, is marked failed." },
          "industry": { "type": "string", "default": "Home Services" },
          "templateKey": { "type": "string", "default": "ultimate", "description": "A key from GET /api/lead-gen/templates: ultimate (default) or river (Waterline). template7 means river. Any other value is built as ultimate. Also template_key." },
          "template_key": { "type": "string", "description": "Same as templateKey." },
          "look": { "type": "string", "enum": ["classic", "bold", "night", "angle"], "default": "classic", "description": "A look from GET /api/lead-gen/templates. Applies to ultimate. A river batch is always classic." },
          "primaryColor": { "allOf": [ { "$ref": "#/components/schemas/HexColor" } ], "default": "#0B1F3A", "description": "Also primary_color." },
          "primary_color": { "allOf": [ { "$ref": "#/components/schemas/HexColor" } ], "description": "Same as primaryColor." },
          "backgroundTheme": { "type": "string", "enum": ["light", "dark"], "default": "light", "description": "Also background_theme." },
          "background_theme": { "type": "string", "enum": ["light", "dark"], "description": "Same as backgroundTheme." },
          "logoSizePx": { "type": "integer", "minimum": 48, "maximum": 160, "default": 88, "description": "Also logo_size_px." },
          "logo_size_px": { "type": "integer", "minimum": 48, "maximum": 160, "description": "Same as logoSizePx." },
          "language": { "allOf": [ { "$ref": "#/components/schemas/Language" } ], "description": "The language every site in the batch is written in. Also primaryLanguage." },
          "primaryLanguage": { "allOf": [ { "$ref": "#/components/schemas/Language" } ], "description": "Same as language." },
          "contactMode": { "allOf": [ { "$ref": "#/components/schemas/ContactMode" } ], "description": "Also contact_mode." },
          "contact_mode": { "allOf": [ { "$ref": "#/components/schemas/ContactMode" } ], "description": "Same as contactMode." },
          "businessHours": { "type": "string", "maxLength": 200, "description": "Free text, cut to 200 characters. Used for every row that does not set its own. Also business_hours." },
          "business_hours": { "type": "string", "maxLength": 200, "description": "Same as businessHours." },
          "priceTool": { "type": "boolean", "description": "Adds a price tool to the sites. On by default when priceToolCalcId is set or the industry matches a trade that has a price tool. Send false to leave it off. Also price_tool." },
          "price_tool": { "type": "boolean", "description": "Same as priceTool." },
          "priceToolCalcId": { "allOf": [ { "$ref": "#/components/schemas/PriceToolCalcId" } ], "description": "Also price_tool_calc_id." },
          "price_tool_calc_id": { "allOf": [ { "$ref": "#/components/schemas/PriceToolCalcId" } ], "description": "Same as priceToolCalcId." }
        }
      },
      "LeadGenBatchStatus": { "type": "string", "enum": ["pending", "queued", "processing", "completed", "failed"], "description": "completed and failed are final." },
      "LeadGenBatch": {
        "type": "object",
        "description": "A batch. It also carries a few internal fields not listed here.",
        "required": ["id", "status", "industry", "total_count", "credits_reserved", "created_at", "updated_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/LeadGenBatchStatus" },
          "industry": { "type": "string" },
          "template_key": { "type": "string", "enum": ["ultimate", "river"] },
          "look": { "type": "string" },
          "primary_color": { "$ref": "#/components/schemas/HexColor" },
          "background_theme": { "type": "string", "enum": ["light", "dark"] },
          "logo_size_px": { "type": "integer" },
          "language": { "$ref": "#/components/schemas/Language" },
          "contact_mode": { "type": "string" },
          "business_hours": { "type": ["string", "null"] },
          "total_count": { "type": "integer", "description": "How many rows the batch has." },
          "credits_reserved": { "type": "integer", "description": "The credits taken when the batch was created." },
          "price_tool": { "type": "integer", "enum": [0, 1], "description": "1 when the sites get a price tool." },
          "price_tool_calc_id": { "type": ["string", "null"] },
          "error": { "type": ["string", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "additionalProperties": true
      },
      "LeadGenBatchCreated": {
        "type": "object",
        "required": ["ok", "batchId", "batch", "creditsReserved", "creditsRemaining"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "batchId": { "type": "string", "format": "uuid" },
          "batch": { "$ref": "#/components/schemas/LeadGenBatch" },
          "creditsReserved": { "type": "integer", "description": "1 per row, minimum 5. 0 when your plan covers the batch." },
          "creditsRemaining": { "type": "integer" }
        }
      },
      "LeadGenItem": {
        "type": "object",
        "description": "One row of a batch, with its build result. It also returns the row values you sent.",
        "required": ["id", "status", "business_name", "slug", "site_url", "public_url", "error"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "batch_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["pending", "completed", "failed"], "description": "A failed item has the reason in error." },
          "business_name": { "type": ["string", "null"] },
          "slug": { "type": ["string", "null"] },
          "site_url": { "type": ["string", "null"], "description": "The preview site's link. A completed item has its link here or, if this is empty, in public_url." },
          "public_url": { "type": ["string", "null"] },
          "error": { "type": ["string", "null"] },
          "phone": { "type": ["string", "null"] },
          "email": { "type": ["string", "null"] },
          "address": { "type": ["string", "null"] },
          "logo_url": { "type": ["string", "null"] },
          "website_url": { "type": ["string", "null"] },
          "business_hours": { "type": ["string", "null"] },
          "primary_color": { "type": ["string", "null"] },
          "background_theme": { "type": ["string", "null"] },
          "contact_mode": { "type": ["string", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "additionalProperties": true
      },
      "LeadGenBatchUpdateRequest": {
        "type": "object",
        "description": "Send at least one of these. Each also accepts its snake_case name.",
        "minProperties": 1,
        "properties": {
          "primaryColor": { "$ref": "#/components/schemas/HexColor" },
          "primary_color": { "$ref": "#/components/schemas/HexColor" },
          "backgroundTheme": { "type": "string", "enum": ["light", "dark"] },
          "background_theme": { "type": "string", "enum": ["light", "dark"] },
          "logoSizePx": { "type": "integer", "minimum": 48, "maximum": 160 },
          "logo_size_px": { "type": "integer", "minimum": 48, "maximum": 160 },
          "language": { "allOf": [ { "$ref": "#/components/schemas/Language" } ], "description": "Only while the batch is pending or queued." }
        }
      },
      "LeadGenBatchUpdated": {
        "type": "object",
        "description": "The ...UpdatedCount values count rows that take the new value. The ...SkippedCount values count rows that keep their own. updatedCount is the number of rows that changed in any way.",
        "required": ["ok", "batchId", "primaryColor", "backgroundTheme", "logoSizePx", "language", "updatedCount", "logoUpdatedCount", "logoSkippedCount", "colorUpdatedCount", "colorSkippedCount", "backgroundUpdatedCount", "backgroundSkippedCount"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "batchId": { "type": "string", "format": "uuid" },
          "primaryColor": { "$ref": "#/components/schemas/HexColor" },
          "backgroundTheme": { "type": "string", "enum": ["light", "dark"] },
          "logoSizePx": { "type": "integer" },
          "language": { "$ref": "#/components/schemas/Language" },
          "updatedCount": { "type": "integer" },
          "logoUpdatedCount": { "type": "integer" },
          "logoSkippedCount": { "type": "integer" },
          "colorUpdatedCount": { "type": "integer" },
          "colorSkippedCount": { "type": "integer" },
          "backgroundUpdatedCount": { "type": "integer" },
          "backgroundSkippedCount": { "type": "integer" }
        }
      },
      "Location": {
        "type": "object",
        "description": "A place a search can cover.",
        "required": ["location_code", "location_name"],
        "properties": {
          "location_code": { "type": "integer", "description": "Send as location_code (or in location_codes).", "example": 1015192 },
          "location_name": { "type": "string", "example": "Sarasota,Florida,United States" },
          "location_type": { "type": "string", "example": "City" }
        },
        "additionalProperties": true
      },
      "Depth": { "type": "integer", "enum": [100, 300, 500, 700], "default": 100, "description": "How many businesses to look through." },
      "WebsiteFilter": { "type": "string", "enum": ["any", "none"], "default": "any", "description": "none keeps only businesses whose Google listing shows no website; businesses whose listing shows one are left out and not charged. any keeps every business." },
      "SearchRequest": {
        "type": "object",
        "description": "Send keyword or exact_category_key, and location_code or location.",
        "properties": {
          "keyword": { "type": "string", "description": "The kind of business, e.g. roofing contractor. Required unless exact_category_key is sent.", "example": "roofing contractor" },
          "location_code": { "type": "integer", "description": "From GET /api/prospecting/locations.", "example": 1015192 },
          "location": { "type": "string", "description": "A place as text, e.g. Sarasota, Florida. The best match is used.", "example": "Sarasota, Florida" },
          "depth": { "$ref": "#/components/schemas/Depth" },
          "radius_miles": { "type": "integer", "enum": [10, 25, 50, 100], "description": "Only keep businesses within this many miles." },
          "website": { "$ref": "#/components/schemas/WebsiteFilter" },
          "verify_emails": { "type": "boolean", "default": true, "description": "true checks each email address; each business whose address is verified costs 1 extra lead credit. false runs no email check: addresses come back as found, without a verified status, and only the business credit is charged." },
          "exact_category_key": { "type": "string", "description": "A category_key from GET /api/prospecting/categories. Keeps only businesses in exactly that category." },
          "confirm": { "type": "boolean", "default": false, "description": "Leave it out to get the plan without starting anything. true starts the search." }
        }
      },
      "SearchPlan": {
        "type": "object",
        "description": "What a search would hold, before it starts.",
        "required": ["keyword", "location_code", "location_name", "depth", "verify_emails", "lead_credits_held", "lead_credits_balance"],
        "properties": {
          "keyword": { "type": "string" },
          "location_code": { "type": "integer" },
          "location_name": { "type": ["string", "null"] },
          "depth": { "$ref": "#/components/schemas/Depth" },
          "radius_miles": { "type": "integer", "enum": [10, 25, 50, 100] },
          "website": { "type": "string", "enum": ["none"] },
          "verify_emails": { "type": "boolean" },
          "lead_credits_held": { "type": "integer", "description": "The hold confirm would place." },
          "max_leads": { "type": "integer", "description": "Present when your balance is below the full hold: the most businesses the search will return." },
          "lead_credits_balance": { "type": "integer" }
        }
      },
      "SearchPlanResponse": {
        "type": "object",
        "required": ["ok", "requires_confirmation", "plan"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "requires_confirmation": { "type": "boolean", "const": true },
          "plan": { "$ref": "#/components/schemas/SearchPlan" },
          "next": { "type": "string", "description": "What to do next, in plain words." }
        }
      },
      "SearchStarted": {
        "type": "object",
        "required": ["ok", "job_id", "status", "lead_credits_held"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "job_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["enriching"] },
          "keyword": { "type": "string" },
          "location_code": { "type": "integer" },
          "location_name": { "type": ["string", "null"] },
          "depth": { "$ref": "#/components/schemas/Depth" },
          "lead_credits_held": { "type": "integer" },
          "max_leads": { "type": "integer" },
          "next": { "type": "string" }
        },
        "additionalProperties": true
      },
      "SearchJob": {
        "type": "object",
        "description": "A search's progress.",
        "required": ["job_id", "status"],
        "properties": {
          "job_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["enriching", "completed", "no_results", "failed"], "description": "enriching: still running, keep polling. completed: done, read the results. no_results: nothing matched; a search that finds nothing costs 10 credits and the rest of the hold came back. failed: the search stopped and unused credits came back." },
          "keyword": { "type": "string" },
          "total_businesses": { "type": "integer" },
          "enriched_count": { "type": "integer" },
          "failed_count": { "type": "integer" },
          "progress": { "type": "integer", "minimum": 0, "maximum": 100 }
        },
        "additionalProperties": true
      },
      "Business": {
        "type": "object",
        "description": "One business, with the same 28 fields as the results table in the Search tab. The example on the docs page is shortened; a real row has every field, with null where nothing was found.",
        "properties": {
          "name": { "type": "string" },
          "category": { "type": ["string", "null"] },
          "website": { "type": ["string", "null"], "description": "null when the Google listing shows no website." },
          "email": { "type": ["string", "null"] },
          "phone": { "type": ["string", "null"] },
          "owner": { "type": ["string", "null"], "description": "The owner's name, where known." },
          "mobile_number": { "type": ["string", "null"] },
          "place_id": { "type": ["string", "null"], "description": "The Google Place ID." },
          "gbp_url": { "type": ["string", "null"], "description": "The Google Business Profile link." },
          "summary": { "type": ["string", "null"], "description": "A short description of the business." },
          "greeting_name": { "type": ["string", "null"], "description": "A first name to open an email with." },
          "services": { "type": ["array", "null"], "items": { "type": "string" } },
          "contact": { "type": ["object", "null"], "properties": {
            "address": { "type": ["string", "null"] },
            "city": { "type": ["string", "null"] },
            "state_code": { "type": ["string", "null"] },
            "postal_code": { "type": ["string", "null"] },
            "country_code": { "type": ["string", "null"] } }, "additionalProperties": true },
          "reputation": { "type": ["object", "null"], "properties": {
            "rating": { "type": ["number", "null"] },
            "reviews": { "type": ["integer", "null"] },
            "is_claimed": { "type": ["boolean", "null"] } }, "additionalProperties": true },
          "web": { "type": ["object", "null"], "description": "What the website check found.", "properties": {
            "tech": { "type": ["array", "null"], "items": { "type": "string" } },
            "socials": { "type": ["object", "null"], "additionalProperties": { "type": ["string", "null"] } },
            "pages": { "type": ["array", "null"], "items": { "type": "string" } } }, "additionalProperties": true },
          "site_report": { "type": ["object", "null"], "description": "The website check, or null when there is no website.", "additionalProperties": true }
        },
        "additionalProperties": true
      },
      "ResultsPage": {
        "type": "object",
        "description": "A page of businesses.",
        "required": ["ok", "job_id", "total", "offset", "limit", "next_offset", "businesses"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "job_id": { "type": "string", "format": "uuid" },
          "total": { "type": "integer", "description": "How many businesses match the filters." },
          "offset": { "type": "integer" },
          "limit": { "type": "integer" },
          "next_offset": { "type": ["integer", "null"], "description": "Send as offset for the next page. null on the last page." },
          "businesses": { "type": "array", "items": { "$ref": "#/components/schemas/Business" } }
        }
      },
      "CampaignCreateRequest": {
        "type": "object",
        "description": "Send keyword or exact_category_key, and location_codes or locations.",
        "properties": {
          "keyword": { "type": "string", "description": "The kind of business. Required unless exact_category_key is sent.", "example": "plumber" },
          "location_codes": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "type": "integer" }, "description": "A list of location codes, up to 200." },
          "locations": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "type": "string" }, "description": "A list of places as text; each uses its best match." },
          "depth_per_location": { "$ref": "#/components/schemas/Depth" },
          "max_leads": { "type": "integer", "minimum": 1, "description": "Stop after this many businesses in total." },
          "website": { "allOf": [ { "$ref": "#/components/schemas/WebsiteFilter" } ], "description": "none keeps only businesses whose Google listing shows no website in every place." },
          "verify_emails": { "type": "boolean", "default": true, "description": "As in a search: true adds 1 lead credit per verified email address; false runs no email check." },
          "exact_category_key": { "type": "string", "description": "A category_key from GET /api/prospecting/categories. Keeps only businesses in exactly that category." },
          "name": { "type": "string", "description": "Shown in the Campaigns tab." },
          "confirm": { "type": "boolean", "default": false, "description": "Leave it out for the plan. true starts the campaign." },
          "idempotency_key": { "type": "string", "format": "uuid", "description": "A UUID. Sending the same one again does not start a second campaign." }
        }
      },
      "CampaignPlanResponse": {
        "type": "object",
        "description": "The plan for a campaign: the lead estimate, about how long it takes, and what confirm would hold.",
        "required": ["ok", "requires_confirmation", "location_codes", "depth_per_location", "max_leads", "verify_emails", "lead_credits_held", "lead_credits_balance"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "requires_confirmation": { "type": "boolean", "const": true },
          "location_codes": { "type": "array", "items": { "type": "integer" } },
          "depth_per_location": { "$ref": "#/components/schemas/Depth" },
          "max_leads": { "type": ["integer", "null"] },
          "verify_emails": { "type": "boolean" },
          "estimated_leads_low": { "type": "integer" },
          "estimated_leads_high": { "type": "integer" },
          "estimated_run_time_minutes": { "type": "number" },
          "total_planned_depth": { "type": "integer" },
          "lead_credits_held": { "type": "integer", "description": "places × depth_per_location, or max_leads if lower, twice that with verify_emails on." },
          "lead_credits_balance": { "type": "integer" },
          "next": { "type": "string" }
        },
        "additionalProperties": true
      },
      "CampaignStarted": {
        "type": "object",
        "required": ["ok", "campaign_id", "lead_credits_held"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "campaign_id": { "type": "string", "format": "uuid" },
          "lead_credits_held": { "type": "integer" },
          "next": { "type": "string" }
        },
        "additionalProperties": true
      },
      "Campaign": {
        "type": "object",
        "description": "A campaign's progress.",
        "required": ["campaign_id", "status"],
        "properties": {
          "campaign_id": { "type": "string", "format": "uuid" },
          "name": { "type": ["string", "null"] },
          "keyword": { "type": "string" },
          "status": { "type": "string", "enum": ["draft", "queued", "running", "paused", "needs_attention", "completed", "stopped", "failed"], "description": "completed, stopped and failed are final. needs_attention: paused because your lead credits ran low; add credits, then resume." },
          "leads_found": { "type": "integer" },
          "location_count": { "type": "integer", "description": "How many places the campaign covers." },
          "locations_completed": { "type": "integer" },
          "depth_per_location": { "$ref": "#/components/schemas/Depth" },
          "max_leads": { "type": ["integer", "null"] },
          "lead_credits_held": { "type": "integer" },
          "error": { "type": ["string", "null"] },
          "created_at": { "type": "string", "format": "date-time" }
        },
        "additionalProperties": true
      },
      "CampaignResultsPage": {
        "type": "object",
        "description": "The campaign's progress plus a page of the businesses found so far.",
        "required": ["ok", "campaign_id", "campaign", "total", "offset", "limit", "next_offset", "businesses"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "campaign_id": { "type": "string", "format": "uuid" },
          "campaign": { "$ref": "#/components/schemas/Campaign" },
          "total": { "type": "integer" },
          "offset": { "type": "integer" },
          "limit": { "type": "integer" },
          "next_offset": { "type": ["integer", "null"] },
          "businesses": { "type": "array", "items": { "$ref": "#/components/schemas/Business" } }
        }
      },
      "EnrichRequest": {
        "type": "object",
        "description": "Send one of domains, businesses or leads, with 1 to 10 entries.",
        "properties": {
          "domains": { "type": "array", "minItems": 1, "maxItems": 10, "items": { "type": "string" }, "description": "Website addresses, with or without https:// and www." },
          "businesses": { "type": "array", "minItems": 1, "maxItems": 10, "items": { "type": "object", "required": ["website"], "properties": {
            "business_name": { "type": "string" },
            "website": { "type": "string" },
            "phone": { "type": "string" },
            "address": { "type": "string" } } }, "description": "When you know the names." },
          "leads": { "type": "array", "minItems": 1, "maxItems": 10, "items": { "type": "object", "required": ["domain"], "properties": {
            "domain": { "type": "string" },
            "business_name": { "type": "string" },
            "city": { "type": "string" },
            "state": { "type": "string" } } }, "description": "When you know the place as well." }
        }
      }
    }
  }
}
