---
title: "Plan Search Reference"
description: "Vehicle identification, request options, filters, response shapes, and making searches faster"
---

# Plan Search Reference

Search and Hand Off uses this call, and Send a Lead runs the same search on our side. This page covers it in full: how to identify a
vehicle, what you can put in a request, what comes back, and how to make it faster.

If you have not picked a path yet, start with
[Choose Your Integration](/getting-started/choose-your-integration).

## Which endpoint to call

There are more search endpoints than you need. In practice:

| Endpoint | Use it when |
|---|---|
| `POST /api/v2/Partners/PlansSearch` | **Default choice.** Accepts any vehicle identifier and works out which one you sent. One integration handles all three. |
| `POST /api/v2/Partners/PlansSearch/Vin` | You only ever search by VIN and want the request validated strictly against that. |
| `POST /api/v2/Partners/PlansSearch/MakeModelYear` | You only ever search by make, model, and year. |
| `POST /api/v2/Partners/PlansSearch/LicensePlate` | You only ever search by plate and state. |
| `POST /api/v2/Partners/PlansSearchWithUserInfo/Vin` | You have the customer's details and want a pre-filled checkout. VIN only. See [Search and Hand Off](/guides/search-and-hand-off#hand-off-with-details-pre-filled). |
| `POST /api/v2/Partners/PlansSearchAsync/*` | You need non-blocking searches or instant cached results, and Chaiz has enabled it for you. See [make searches faster](#make-searches-faster). |

The generic and the specific endpoints return the same shape. The specific ones just reject a request
that carries the wrong identifier, which can be useful as a guardrail.

<scalar-callout type="info">
The synchronous route is `/PlansSearch/Vin` and the asynchronous one is `/PlansSearchAsync/VIN`. Note
the different capitalization. It is inconsistent, and we are keeping it that way rather than breaking
existing integrations.
</scalar-callout>

## Identify the vehicle

Three ways, in descending order of accuracy.

```mermaid
flowchart TD
    START["What do you know about the vehicle?"]

    START ==>|"the VIN"| VIN["vin"]
    START ==>|"plate and state"| LP["licensePlate plus stateShortName"]
    START ==>|"make, model, year"| MMY["make plus model plus year"]

    VIN ==> VR["Most accurate. Customer never types a VIN."]
    LP ==> LR["Easiest for the customer. Some plates fail."]
    MMY ==> MR["Indicative only. VIN needed at checkout."]
```

Prefer VIN wherever you can get one. It prices the exact trim, and it removes a step from checkout
later. Make/model/year works, but the customer will be asked for a VIN before the sale completes, and
that is where you lose people.

**Always required, whichever you use:** the customer's `stateShortName` (two-letter US code) and the
current `mileage`. Pricing depends on both, so neither is optional in practice.

If you send more than one identifier, precedence is **VIN, then license plate plus state, then
make/model/year**. Send exactly one to avoid surprises.

### Resolving loose make and model text

`make` and `model` must match our catalog. Free text a customer typed usually will not. Resolve it
first:

```bash
curl "https://uat-api.chaiz.com/api/v2/Partners/Vehicles/Lookup?make=merc&model=f150&year=2019" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Returns up to five `matches` ordered by confidence, plus `suggestions` when the input is ambiguous.
Responses are cached and come back in under 50 ms.

| Confidence | Meaning | Example |
|---|---|---|
| `HIGH` | Exact match, ignoring case | `ford` resolves to `FORD` |
| `MEDIUM` | Matched after normalizing punctuation | `f150` resolves to `F-150` |
| `LOW` | Partial or contains match | `merc` could be `MERCURY` or `MERCEDES-BENZ` |

Treat `LOW` as "ask the customer to confirm" rather than as an answer. Check `yearsAvailable` on the
match before searching a year we do not carry.

To build your own dropdowns instead, `GET /api/v2/Partners/Vehicles/Makes` and
`GET /api/v2/Partners/Vehicles/Makes/{makeId}/Models` return the full catalog.

## Request anatomy

```json
{
  "searchCriteria": { "vin": "5XXG14J25MG015777", "stateShortName": "NY", "mileage": 42000 },
  "filters": { "duration": { "from": 24, "to": 60 }, "returnResults": 10 },
  "tracking": { "queryParams": "utm_source=partner&utm_campaign=your-campaign" },
  "responseDetailLevel": "Full",
  "resultOrder": "Recommended",
  "planVariant": "Recommended",
  "showProviderInfo": true
}
```

<scalar-callout type="warning">
`filters`, `tracking`, `responseDetailLevel`, `resultOrder`, `planVariant`, and `showProviderInfo` are **root-level**
fields, siblings of `searchCriteria`, not properties inside it. Nesting them inside `searchCriteria` is
the single most common request mistake, and it fails silently: the fields are ignored and you get an
unfiltered search.
</scalar-callout>

| Field | Required | Notes |
|---|---|---|
| `searchCriteria` | Yes | The vehicle, state, and mileage |
| `filters` | No | Narrow the results. See below. |
| `tracking` | No | `queryParams` and `referrer`, for attribution. See [Search and Hand Off](/guides/search-and-hand-off#getting-credit-for-the-sale). |
| `responseDetailLevel` | No | `Full` (default) or `Essential`. See [response shapes](#response-shapes). |
| `resultOrder` | No | `Recommended` (default), `Rating`, or `None` |
| `planVariant` | No | `Recommended` (default), `Highest`, `Longest`, or `All`. See [one row per plan](#one-row-per-plan). |
| `showProviderInfo` | No | Include provider detail in the results |
| `partner` | No | Leave it out. Resolved from your token, and a mismatch is rejected. |
| `searchId`, `userId` | No | For continuing an existing search session. Omit unless you have been told to use them. |

## Filters

All optional, all under `filters`.

| Filter | Type | Notes |
|---|---|---|
| `providers` | array of strings | Only these providers, by name: `"NAAC"`, `"Omega"`, `"CAPS"`, `"ServiceContract"` |
| `excludeProviders` | array of strings | Everything except these |
| `numberOfProviders` | integer | Cap on distinct providers in the results |
| `coverageTypes` | array of strings | See the vocabulary note below |
| `coverageType` | enum | Single value. **Different vocabulary from `coverageTypes`.** See below. |
| `categoryTypes` | array of strings | Only plans covering these components: `Engine`, `TurboSuper`, `TransferCase`, `Steering`, `Transmission`, `DriveAxle`, `Suspension`, `AirCon`, `Brakes`, `Electricals`, `Cooling`, `Fuel`, `Hybrid`, `Body`, `Consumables`, `Exhaust`, `General`, `Interior`, `Services`, `Assist` |
| `planChaizRating` | range | Chaiz quality rating, 0 to 10. `{ "from": 7, "to": 10 }` |
| `duration` | range, months | `{ "from": 24, "to": 60 }` |
| `miles` | range, thousands | `{ "from": 50, "to": 150 }` is 50,000 to 150,000 miles. `999` means unlimited. |
| `monthlyPrice` | range, USD | `{ "from": 50, "to": 150 }` |
| `totalPrice` | range, USD | `{ "from": 1000, "to": 5000 }` |
| `returnResults` | integer | Maximum rows returned. It counts rows, so with `planVariant: "All"` one plan can use several of them. Default depends on your account. |

Provider names are real catalog values, and which of them you can see depends on your account. Filtering
to a provider you are not configured for returns nothing rather than an error.

### The two coverage filters do not speak the same language

<scalar-callout type="danger">
`coverageTypes` (the array) accepts either the friendly aliases `"basic"`, `"basicplus"`, `"best"` or
the canonical names `"Powertrain"`, `"PowertrainPlus"`, `"MostComprehensive"`.

`coverageType` (the singular) accepts **only** the canonical names. The aliases do not work there.

An unrecognized value in `coverageTypes` is **silently dropped**, not rejected, so a typo looks like a
filter that had no effect. Prefer the canonical names everywhere and you avoid the whole problem.
</scalar-callout>

| Alias | Canonical name | Meaning |
|---|---|---|
| `basic` | `Powertrain` | Core powertrain components |
| `basicplus` | `PowertrainPlus` | Powertrain plus additional systems |
| `best` | `MostComprehensive` | Exclusionary, the broadest coverage |

Results report coverage as the number: `1` Powertrain, `2` PowertrainPlus, `3` MostComprehensive. Both
filters also accept those numbers.

## Response shapes

`responseDetailLevel` controls how much comes back.

| | `Full` (default) | `Essential` |
|---|---|---|
| Coverage breakdown, component scores, add-ons, provider detail, highlights | Yes | No |
| `planContractUrl` | Yes | No |
| Maintenance program | Full detail, as `maintenanceProgram` | Yes/no only, as `hasMaintenanceProgram` |
| Core fields: price, duration, deductible, rating, handoff URLs | Yes | Yes |
| Payload size | Large | Small |

Use `Full` when you are rendering a comparison UI where customers want the detail. Use `Essential` when
you only need price, name, rating, and a link, which keeps responses small and fast to parse.

### One row per plan

A plan can be sold with several terms: different durations and mileage tiers. `planVariant` decides which
of these variants you get back. The default is `Recommended`: one row for each `planId`.

| `planVariant` | What you get |
|---|---|
| `Recommended` (default) | One row per plan. The shortest duration wins. If durations tie, the mileage closest to the baseline for that duration wins. If that ties, the lowest monthly price wins. If that ties, the higher mileage wins. |
| `Highest` | One row per plan: the variant with the highest mileage. |
| `Longest` | One row per plan: the variant with the longest duration. |
| `All` | Every variant, as before. |

The baseline mileage for each duration:

| Duration | Baseline mileage |
|---|---|
| 12 months | 15,000 |
| 24 months | 30,000 |
| 36 months | 60,000 |
| 48 months | 75,000 |
| 60 months | 100,000 |
| 72 months | 125,000 |

Rules to know:

- The rows are picked first. The limits apply after that, each with its own unit:
  - `numberOfProviders` (or your account's provider limit when you do not send it) counts providers. It keeps
    the providers whose best plan scores highest.
  - Your account's result limit counts distinct plans, on every surface. With `All` it still counts plans:
    one plan with 3 variants is one plan and returns 3 rows.
  - `filters.returnResults` is applied last and counts rows. With `All` one plan can use several of them.
- If your account groups results to one row per provider (for example `bestValuePerProvider`), that grouping
  picks the row. `planVariant` then has no visible effect.
- The order of the results does not change. `resultOrder` works as before.
- Your account can set a default for `planVariant`. A value in the request overrides it.
- Send `"planVariant": "All"` to get the same rows you got before this option existed.

### Fields you will use most

| Field | Notes |
|---|---|
| `searchResultPlanId` | This plan at this price, embedded in the handoff URLs. Search again when it is older than 30 days. |
| `planId` | **`Essential` only.** Shared across the duration and mileage variants of the same plan, so it is what you dedupe on. Not returned on the default `Full` shape; see below. |
| `totalPrice`, `downpayment`, `monthlyPrice` | Already reflect vehicle, mileage, state, and your account. Do not recompute. Plans of 36 months or less can be paid monthly. Longer plans are paid in full and return `0` for `monthlyPrice` and `downpayment`, so show `totalPrice` for those. |
| `duration`, `miles`, `deductible` | Contract terms |
| `planChaizRating`, `planRatingVerdict` | Chaiz quality score out of 10, and its label |
| `displayProviderName`, `planName` | What to show the customer |
| `providerLogoUrl` | **`Full` only.** The provider's logo |
| `resultLabels` | Badges such as `BEST_VALUE` and `LOWEST_PRICE` |
| `maintenanceProgram` | **`Full` only.** The bundled maintenance program, or `null` when the plan has none. See below. |
| `hasMaintenanceProgram` | **`Essential` only.** Whether the plan bundles a maintenance program. |
| `planSummaryUrl`, `planCheckoutUrl` | Handoff destinations. See [Search and Hand Off](/guides/search-and-hand-off#step-3-hand-off). |

By default you get one row per `planId`, so a plan appears once. With `planVariant: "All"`, the same plan
appears several times at different durations and mileage tiers. That is not duplication in the results; it
is the same product on different terms. See [one row per plan](#one-row-per-plan).

### Maintenance programs

Some plans bundle a prepaid maintenance program at no extra cost, covering routine servicing such as
oil changes, brake pads, battery replacement and cooling system service. The two response shapes
answer this differently, so check the right field for the shape you requested:

- On **`Full`**, read `maintenanceProgram`. It is `null` when the plan does not include one; when it
  does, it carries a `description`, an `items` array of `{ label, info }` describing each covered
  service and its reimbursement limit, and a `documentUrl`. There is no boolean on this shape.
- On **`Essential`**, read the `hasMaintenanceProgram` boolean. The program's contents are not
  included; request `Full` for those.

```json
"maintenanceProgram": {
  "description": "Covers essential repairs to keep your car running smoothly",
  "items": [
    { "label": "3 oil changes", "info": "$40 towards a standard oil change." },
    { "label": "1 battery or light replacement", "info": null }
  ],
  "documentUrl": "https://assets.chaiz.com/files/maintenance-program.pdf"
}
```

<scalar-callout type="info">
`documentUrl` is `null` when the administrator has not published a program document. That does not
mean the plan lacks a maintenance program: the program's presence is signaled by
`maintenanceProgram` being non-null, never by `documentUrl`. Likewise, `info` is `null` on items
that have no limit or condition to state.
</scalar-callout>

<scalar-callout type="warning">
**Deduping variants:** you no longer need to dedupe. The default `planVariant` returns one row per plan on
both response shapes. If you send `planVariant: "All"` and want to group variants yourself, use `planId`.
It is returned only on the `Essential` shape. On the default `Full` shape, group on
`displayProviderName` plus `planName` plus `coverageType` instead.
</scalar-callout>

## Make searches faster

A cold search takes **2 to 8 seconds** because Chaiz is collecting live rates from multiple warranty
administrators. That is unavoidable on the synchronous path.

If you know which vehicles a customer might ask about before they ask (a listing page, a saved
vehicle, an inventory feed), you can move that wait off the customer's request entirely.

```mermaid
sequenceDiagram
    autonumber
    participant You as Your backend
    participant API as Chaiz API

    rect rgb(253, 240, 230)
    Note over You,API: Synchronous, the customer waits
    You->>API: POST /Partners/PlansSearch
    Note right of API: 2 to 8 seconds
    API-->>You: 200 with plans
    end

    rect rgb(232, 245, 240)
    Note over You,API: Cached, warm it up ahead of time
    You->>API: POST /Partners/VehicleRegistration
    API-->>You: 200 started, 201 cached, or 202 in progress
    Note over API: quoting happens in the background
    You->>API: POST /Partners/PlansSearchAsync/VIN
    API-->>You: 200 with plans, near instant
    end
```

<scalar-callout type="warning">
Cached and asynchronous search is **opt-in per account**. If it is not enabled for you, these endpoints
return an error regardless of a valid request. Email dev-support@chaiz.com to have it turned on, and do
not build against it until it is.
</scalar-callout>

### Pre-registering a vehicle

`POST /api/v2/Partners/VehicleRegistration` with a VIN and mileage starts the quoting work in the
background so a later search returns immediately.

| Status | Meaning |
|---|---|
| `200` | Registration accepted, quoting started |
| `201` | Already cached and ready to search. The body echoes `vin` and `mileage`. |
| `202` | Already in progress, nothing more to do |
| `400` | The request did not validate, or quoting failed. `response` is `null`. |

The cache is keyed on **VIN plus mileage**, so a meaningful mileage change means the entry no longer
matches; re-register when the odometer moves by roughly 5,000 miles. Entries last 30 days.
`POST /api/v2/Partners/VehicleRegistration/CleanVehicleCache` clears one explicitly if you know the
underlying data changed.

### Searching asynchronously

`POST /api/v2/Partners/PlansSearchAsync/VIN` (or `/MakeModelYear`, `/LicensePlate`, or the bare route)
never blocks for the full quoting round trip.

| Status | Meaning | What to do |
|---|---|---|
| `200` | **Done.** The body holds your plans. | Use the results. This is the terminal success state. |
| `202` | Still quoting | Poll the same endpoint with the same body every 1 to 2 seconds |
| `400` | Validation failed, or quoting errored. `response` is `null`. | Read the errors; do not poll |

<scalar-callout type="info">
Asynchronous search completes with **`200`**, not `201`. Only `VehicleRegistration` returns `201`, and
it means something different there (already cached). If you are polling for a `201` from a search
endpoint you will poll forever.

There is no `Retry-After` header on a `202`. Use your own 1 to 2 second interval, and set a sane
overall ceiling so a stuck search does not hang your request.
</scalar-callout>

Responses carry an `X-Cache` header of `HIT` or `MISS`, which is useful for confirming your
pre-registration strategy is actually working.

### Just the starting price

`POST /api/v2/Partners/PlansSearchAsync/GetStartPrice` returns only the starting price for a
vehicle, for a "protection from $X" teaser without pulling a full result set. It follows the same
`200` / `202` / `400` contract.

The starting price is the lowest first payment among plans of 36 months or less. When a vehicle only
has longer plans, which are paid in full, there is no starting price.

<scalar-callout type="warning">
When no price is available, including when a vehicle only has plans longer than 36 months, this returns
an **empty string** rather than a number or `null`. Handle the empty case explicitly.
</scalar-callout>

## Next steps

- **[Search and Hand Off](/guides/search-and-hand-off)** for what to do with the results.
- **[Restrictions and limitations](/reference/restrictions)** for why a search can come back empty.
- **[UI Requirements](/reference/ui-requirements)** for what your results page must show.
- **[Errors & Rate Limits](/reference/errors)** for validation codes and retry behavior.
- **[API Reference](/reference/api-reference)** for complete schemas.
