> ## Documentation Index
> Fetch the complete documentation index at: https://docs.homeservicedata.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# GET /api/v1/search — Search Across All Datasets

> Unified search endpoint that queries equipment, finance, pricing, incentive, utility, permit, labor, and dataset records in a single request.

The Search endpoint is a single, unified entry point for finding anything in the Home Service Data platform. One request fans out across equipment records, finance programs, price observations, incentive programs, utility territories, permit assumptions, labor adders, source documents, and published dataset manifests — then merges, deduplicates, and ranks the results by relevance. It is the right starting point when you do not yet know which specific dataset a record lives in.

## Endpoint

```
GET https://homeservicedata.org/api/v1/search
```

## Authentication

Requires a valid `x-api-key` header. The search endpoint checks dataset-level access for each result type and only returns records from datasets your key is entitled to. If your key has no entitlement for a given type, results of that type are silently omitted rather than rejected with a 403.

## Query Parameters

<ParamField query="q" type="string" required>
  The search query string. Minimum 2 characters after trimming whitespace and control characters. Matched against titles, SKUs, manufacturer names, program names, and other text fields depending on the result type. Maximum 120 characters; longer values are truncated.
</ParamField>

<ParamField query="trade" type="string">
  Restrict results to a single trade vertical. One of: `solar`, `hvac`, `roofing`, `plumbing`, `electrical`, `context`. Omit to search all trades.
</ParamField>

<ParamField query="type" type="string">
  Restrict results to a single record type. One of:

  | Value                 | Alias(es)                        | Description                                     |
  | --------------------- | -------------------------------- | ----------------------------------------------- |
  | `equipment`           | `hardware`                       | Equipment and hardware catalog records          |
  | `price`               | —                                | Market price observations                       |
  | `finance`             | —                                | Finance fee / dealer fee programs               |
  | `finance_eligibility` | `eligibility`                    | Finance product eligibility rules               |
  | `incentive`           | —                                | Incentive programs (tax credits, rebates)       |
  | `utility`             | —                                | Utility providers and service territories       |
  | `permit`              | —                                | Permit assumptions and jurisdiction authorities |
  | `labor`               | —                                | Labor adder catalog entries                     |
  | `source_document`     | `source`, `sources`, `documents` | Ingested source files and data sheets           |
  | `dataset`             | `sync`                           | Published dataset manifests                     |

  Aliases like `hardware` and `eligibility` are normalized automatically.
</ParamField>

<ParamField query="quote_safe" type="boolean">
  Filter results by their `quote_safe` flag. Pass `true` to return only records approved for customer-facing quotes. Pass `false` to return only records that are not yet approved. Omit to return both. Must be the literal string `true` or `false`.
</ParamField>

<ParamField query="limit" type="integer">
  Maximum number of results to return. Minimum `1`, maximum `50`, default `24`. The API may return fewer results if deduplication or access filtering reduces the set.
</ParamField>

## Result Ranking

Results are scored before being returned. The ranking algorithm rewards:

* Exact title matches (highest weight)
* Title prefix matches
* Title substring matches
* Subtitle and metadata matches (lower weight)
* Number of matched fields
* `quote_safe: true` records
* Higher `confidence` values (`high` > `medium`)

Records with the same score are further ordered by type priority: `equipment` → `price` → `finance` → `finance_eligibility` → `incentive` → `utility` → `permit` → `labor` → `source_document` → `dataset`.

## Request Examples

<CodeGroup>
  ```http Basic search theme={null}
  GET /api/v1/search?q=Goodman+HVAC HTTP/1.1
  Host: homeservicedata.org
  x-api-key: YOUR_API_KEY
  ```

  ```http Filtered by trade and quote_safe theme={null}
  GET /api/v1/search?q=Goodman+HVAC&trade=hvac&quote_safe=true HTTP/1.1
  Host: homeservicedata.org
  x-api-key: YOUR_API_KEY
  ```

  ```http Scoped to a single result type theme={null}
  GET /api/v1/search?q=Sunpower&type=equipment&trade=solar&limit=10 HTTP/1.1
  Host: homeservicedata.org
  x-api-key: YOUR_API_KEY
  ```

  ```http Finance program search theme={null}
  GET /api/v1/search?q=GoodLeap&type=finance&quote_safe=true HTTP/1.1
  Host: homeservicedata.org
  x-api-key: YOUR_API_KEY
  ```
</CodeGroup>

```typescript TypeScript example theme={null}
const response = await fetch(
  "https://homeservicedata.org/api/v1/search?" +
    new URLSearchParams({
      q: "Goodman HVAC",
      trade: "hvac",
      quote_safe: "true",
      limit: "10",
    }),
  {
    headers: { "x-api-key": process.env.HSD_API_KEY! },
  }
);

const { data, error } = await response.json();
if (error) throw new Error(error);
// data is GlobalSearchResult[]
```

## Response

A successful response returns an array of result objects in `data`. The `meta` field is `null` for the search endpoint — pagination is not supported; use the `limit` parameter to control result count.

```json theme={null}
{
  "data": [
    {
      "type": "equipment",
      "id": "a1b2c3d4-...",
      "title": "Goodman GSX14024",
      "subtitle": "Goodman",
      "trade": "hvac",
      "category": "equipment",
      "href": "/dashboard?trade=hvac&category=equipment&highlight=a1b2c3d4-...",
      "quote_safe": true,
      "confidence": "high",
      "source_document_id": "f9e8d7c6-...",
      "last_verified_at": "2024-11-15T00:00:00.000Z",
      "unresolved_requirements": [],
      "meta": "$1,240",
      "matched_fields": ["sku", "manufacturer"]
    },
    {
      "type": "finance",
      "id": "b2c3d4e5-...",
      "title": "GoodLeap 25-Year 2.99%",
      "subtitle": "GoodLeap",
      "trade": "solar",
      "category": "financing",
      "href": "/dashboard?trade=solar&category=financing&highlight=b2c3d4e5-...",
      "quote_safe": true,
      "confidence": "high",
      "source_document_id": "e8d7c6b5-...",
      "last_verified_at": "2024-12-01T00:00:00.000Z",
      "unresolved_requirements": [],
      "meta": "2.99% / 300mo / 39% fee",
      "matched_fields": ["financier_name"]
    }
  ],
  "error": null,
  "meta": { "request_id": "req_01J...", "api_version": "2026-06-30" }
}
```

### Result Object Fields

<ResponseField name="type" type="string">
  The record type. One of `equipment`, `price`, `finance`, `finance_eligibility`, `incentive`, `utility`, `permit`, `labor`, `source_document`, `dataset`.
</ResponseField>

<ResponseField name="id" type="string">
  The unique identifier for the record within its type. Stable across requests.
</ResponseField>

<ResponseField name="title" type="string">
  The primary display label — SKU for equipment, program name for finance and incentive records, utility name for utility records, and so on.
</ResponseField>

<ResponseField name="subtitle" type="string | null">
  A secondary label providing additional context — manufacturer for equipment, financier name for finance, authority name for permits, etc. `null` if not applicable.
</ResponseField>

<ResponseField name="trade" type="string | null">
  The trade vertical the record belongs to (`solar`, `hvac`, `roofing`, `plumbing`, `electrical`, or `context`). `null` for records that are not trade-scoped.
</ResponseField>

<ResponseField name="category" type="string">
  The dashboard category this record belongs to (e.g. `equipment`, `financing`, `incentives`, `permits`, `labor`). Suitable for building navigation links.
</ResponseField>

<ResponseField name="href" type="string">
  A deep-link path into the Home Service Data dashboard for this record. Useful for linking users directly to the source.
</ResponseField>

<ResponseField name="quote_safe" type="boolean | null">
  Whether this record has passed all validation checks and is approved for use in customer-facing quotes. `null` for record types (such as source documents and datasets) that do not carry this signal.
</ResponseField>

<ResponseField name="confidence" type="string | null">
  Data confidence tier: `high`, `medium`, or `low`. `null` for record types that do not carry this signal.
</ResponseField>

<ResponseField name="source_document_id" type="string | null">
  The UUID of the source document this record was ingested from. `null` if the record was not derived from a tracked source file.
</ResponseField>

<ResponseField name="last_verified_at" type="string | null">
  ISO 8601 timestamp of the most recent source verification. `null` if no verification date is recorded.
</ResponseField>

<ResponseField name="unresolved_requirements" type="string[]">
  A list of outstanding issues that prevent this record from being fully `quote_safe`. An empty array means no issues. Examples: `"polygon-backed territory not loaded"`, `"source document needs review"`.
</ResponseField>

<ResponseField name="meta" type="string">
  A compact, human-readable summary string formatted for display in search results — for example `"$1,240"` for equipment, `"2.99% / 300mo / 39% fee"` for finance, or `"active"` for incentives.
</ResponseField>

<ResponseField name="matched_fields" type="string[]">
  The names of the database fields that the query string matched in this record. Useful for highlighting matched text in a search UI.
</ResponseField>

## Errors

| Status | Message                                                                              | Cause                                                    |
| ------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------- |
| `400`  | `Query parameter 'q' is required (min 2 characters).`                                | `q` parameter missing or shorter than 2 characters       |
| `400`  | `Unsupported trade "…". Use solar, hvac, roofing, plumbing, electrical, or context.` | `trade` value not in the allowed set                     |
| `400`  | `Unsupported type. Use equipment, price, finance, …`                                 | `type` value not in the allowed set                      |
| `400`  | `Invalid quote_safe. Use true or false.`                                             | `quote_safe` is not the literal string `true` or `false` |
| `401`  | `Missing x-api-key header.`                                                          | No API key provided                                      |
| `401`  | `Invalid API key.`                                                                   | Key not found or revoked                                 |

<Note>
  If the search index is unavailable, the API falls back to direct per-table queries. All result types are searched in parallel and results are merged client-side. This fallback is transparent — you will still receive results, though response time may be slightly higher.
</Note>
