{
  "openapi": "3.0.3",
  "info": {
    "title": "LocaMapHQ Public API",
    "version": "1.0.0",
    "description": "LocaMapHQ finds local business leads by city and industry. Search a city plus a trade, get back real local businesses with the signals you need to prioritize outreach: whether the business has a website, how many reviews it has, its rating, and direct contact details.\n\nAuthentication: all endpoints except /health and /meta require a Bearer API key. Create one at /dashboard/settings/api-keys (the raw key is shown once on creation).\n\nCredits: search costs 1 credit per business returned. Health, meta, credits balance, saved leads, and export are free. A search that returns no results or fails upstream charges nothing.",
    "contact": {
      "url": "https://www.locamaphq.com"
    }
  },
  "servers": [
    {
      "url": "https://www.locamaphq.com",
      "description": "Production"
    }
  ],
  "tags": [
    { "name": "Discovery", "description": "Free, unauthenticated endpoints for discovery and onboarding." },
    { "name": "Account", "description": "Endpoints for managing your account and credits." },
    { "name": "Leads", "description": "Endpoints for finding, retrieving, and exporting local business leads." }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from /dashboard/settings/api-keys. Format: lmhq_live_<key>. The raw key is shown once on creation and is not recoverable."
      }
    },
    "schemas": {
      "Prospect": {
        "type": "object",
        "description": "A local business. Does not include internal-only fields.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier for this prospect record."
          },
          "lead_search_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID of the search that found this business."
          },
          "business_name": {
            "type": "string",
            "example": "Austin Plumbing Co"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "example": "Plumber"
          },
          "website_url": {
            "type": "string",
            "nullable": true,
            "example": "",
            "description": "Business website. Empty string or null means no website was found."
          },
          "phone": {
            "type": "string",
            "nullable": true,
            "example": "+1-512-555-0100"
          },
          "address": {
            "type": "string",
            "nullable": true,
            "example": "1234 Main St, Austin TX 78701"
          },
          "rating": {
            "type": "number",
            "nullable": true,
            "example": 4.2
          },
          "review_count": {
            "type": "integer",
            "nullable": true,
            "example": 23
          },
          "website_signal": {
            "type": "string",
            "nullable": true,
            "enum": ["modern_website", "no_website"],
            "description": "Whether the business has a website. no_website indicates a cold-outreach opportunity for web design or digital marketing."
          },
          "review_signal": {
            "type": "string",
            "nullable": true,
            "enum": ["many_reviews", "some_reviews", "few_reviews"],
            "description": "Derived from review_count. many = over 50, some = 11 to 50, few = 10 or fewer."
          },
          "status": {
            "type": "string",
            "example": "saved",
            "description": "CRM status for your outreach pipeline."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "invalid_api_key",
                  "revoked_api_key",
                  "invalid_request",
                  "insufficient_credits",
                  "rate_limited",
                  "provider_unavailable",
                  "internal_error"
                ]
              },
              "message": { "type": "string" }
            }
          }
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "total": { "type": "integer", "description": "Total matching records." },
          "limit": { "type": "integer" },
          "offset": { "type": "integer" },
          "has_more": { "type": "boolean" }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid, or revoked API key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "invalid_api_key",
                "message": "Invalid API key."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Check the Retry-After header.",
        "headers": {
          "Retry-After": {
            "schema": { "type": "integer" },
            "description": "Seconds until the rate limit window resets."
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Rate limit exceeded. Retry after the window resets."
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/public/v1/health": {
      "get": {
        "summary": "Health check",
        "description": "Liveness check. Free and unauthenticated. Use to confirm the API is reachable before onboarding a user.",
        "operationId": "getHealth",
        "security": [],
        "tags": ["Discovery"],
        "responses": {
          "200": {
            "description": "API is reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" },
                    "version": { "type": "string", "example": "v1" },
                    "timestamp": { "type": "string", "format": "date-time" }
                  }
                },
                "example": {
                  "status": "ok",
                  "version": "v1",
                  "timestamp": "2026-06-18T12:00:00.000Z"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/meta": {
      "get": {
        "summary": "Service metadata",
        "description": "Service metadata, pricing, and onboarding URLs. Free and unauthenticated. Read this first to set cost expectations before any paid call.",
        "operationId": "getMeta",
        "security": [],
        "tags": ["Discovery"],
        "responses": {
          "200": {
            "description": "Service metadata including credit pack pricing and rate limits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_version": { "type": "string" },
                    "onboarding": { "type": "object" },
                    "capabilities": {
                      "type": "array",
                      "items": { "type": "string" },
                      "description": "What the API can detect and return."
                    },
                    "credits": { "type": "object" },
                    "rate_limits": { "type": "object" },
                    "endpoints": { "type": "object" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/credits": {
      "get": {
        "summary": "Credit balance",
        "description": "Return the authenticated user's current credit balance. Free. Call before a search to confirm the user can afford it. The balance is a guide, not an exact quote, since search charges per business actually found.",
        "operationId": "getCredits",
        "security": [{ "bearerAuth": [] }],
        "tags": ["Account"],
        "responses": {
          "200": {
            "description": "Current credit balance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "credits": { "type": "integer", "example": 250 },
                    "unit": { "type": "string", "example": "prospect" }
                  }
                },
                "example": { "credits": 250, "unit": "prospect" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/public/v1/leads/saved": {
      "get": {
        "summary": "List saved leads",
        "description": "List the businesses already found and stored for this user, paginated. Free. Use to retrieve or page through results without spending credits.",
        "operationId": "getSavedLeads",
        "security": [{ "bearerAuth": [] }],
        "tags": ["Leads"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 20, "maximum": 100, "minimum": 1 },
            "description": "Results per page. Max 100."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": { "type": "integer", "default": 0, "minimum": 0 },
            "description": "Pagination offset."
          },
          {
            "name": "search_id",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Filter to a specific search run. Omit to list all stored leads."
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of stored leads.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "prospects": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Prospect" }
                    },
                    "pagination": { "$ref": "#/components/schemas/Pagination" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/public/v1/leads/search": {
      "post": {
        "summary": "Search for local business leads",
        "description": "Find local businesses by city and trade. Charges 1 credit per business returned. Returns business name, category, address, phone, rating, review-volume signal, and whether the business has a website. A search that finds nothing or fails upstream charges nothing. Confirm user intent and balance before calling.",
        "operationId": "searchLeads",
        "security": [{ "bearerAuth": [] }],
        "tags": ["Leads"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["city", "industry"],
                "properties": {
                  "city": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "City to search in.",
                    "example": "Austin"
                  },
                  "industry": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Trade or industry to search for.",
                    "example": "plumber"
                  },
                  "limit": {
                    "type": "integer",
                    "enum": [10, 25, 50, 100],
                    "default": 10,
                    "description": "Maximum results to return. You are charged per result actually found, up to this limit."
                  },
                  "country": {
                    "type": "string",
                    "maxLength": 2,
                    "default": "US",
                    "description": "ISO 3166-1 alpha-2 country code.",
                    "example": "US"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Optional label for this search run."
                  }
                }
              },
              "example": {
                "city": "Austin",
                "industry": "plumber",
                "limit": 25,
                "country": "US"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search completed. Use GET /api/public/v1/leads/saved?search_id=<id> to retrieve the results.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "search_id": { "type": "string", "format": "uuid" },
                    "prospects_found": { "type": "integer", "example": 23 },
                    "credits_used": { "type": "integer", "example": 23 },
                    "remaining_credits": { "type": "integer", "example": 227 }
                  }
                },
                "example": {
                  "search_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "prospects_found": 23,
                  "credits_used": 23,
                  "remaining_credits": 227
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": { "code": "invalid_request", "message": "city is required." }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": {
            "description": "Insufficient credits. No search was run.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string", "example": "insufficient_credits" },
                        "message": { "type": "string" },
                        "remaining_credits": { "type": "integer", "example": 5 }
                      }
                    }
                  }
                },
                "example": {
                  "error": {
                    "code": "insufficient_credits",
                    "message": "Your credit balance (5) is less than the requested limit (25). Add credits at /pricing or lower your limit.",
                    "remaining_credits": 5
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": {
            "description": "Search provider temporarily unavailable. No credits charged.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "provider_unavailable",
                    "message": "Search provider is temporarily unavailable. No credits were used."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/leads/export": {
      "get": {
        "summary": "Export leads as CSV",
        "description": "Export this user's stored businesses for a given search as a CSV file. Free. Use after a search to hand the user a clean lead list.",
        "operationId": "exportLeads",
        "security": [{ "bearerAuth": [] }],
        "tags": ["Leads"],
        "parameters": [
          {
            "name": "search_id",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "The search to export."
          }
        ],
        "responses": {
          "200": {
            "description": "CSV file with 12 columns. Columns: Business Name, Category, Website URL, Phone, Address, Rating, Review Count, Website Signal, Review Signal, Status, Next Action, Notes.",
            "content": {
              "text/csv": {
                "schema": { "type": "string" },
                "example": "Business Name,Category,Website URL,Phone,Address,Rating,Review Count,Website Signal,Review Signal,Status,Next Action,Notes\nAustin Plumbing Co,Plumber,,+1-512-555-0100,\"1234 Main St, Austin TX\",4.2,23,no_website,some_reviews,saved,,"
              }
            }
          },
          "400": {
            "description": "Missing or invalid search_id.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": { "code": "invalid_request", "message": "search_id query parameter is required." }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  }
}
