> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.truvo.com/llms.txt.

# truvo.md

If you're a coding agent, this file is for you. It explains what the Truvo API does, how to authenticate, every operation, the sandbox, and the rules for errors, retries, polling, and request keys. Each section links to the guide with more detail. Every example uses the sandbox. Sandbox quotes are synthetic, and a sandbox key never reads a live record or causes a live effect.

## What Truvo does

The Truvo API gets insurance quotes from inside your product. You send one applicant's facts for one line, Truvo gets the quotes in the background, and you read the result when it's ready.

- **Lines:** `personal_auto`, `renters`, and `pet`. One quote request covers one line.
- **Pet** is quote plus a hand-off: the customer buys on the carrier's site, and Truvo doesn't learn the result.
- **Purchase:** the API doesn't have the purchase operations yet.

## Terms

| Term | Meaning |
| --- | --- |
| quote request | The asynchronous job that `POST /v1/quote-requests` starts. Its ID starts with `qreq_`. |
| quote | One priced result, read with `GET /v1/quotes/{id}`. Its ID starts with `qt_`, and each quote has numbered versions. |
| line | `personal_auto`, `renters`, or `pet`. |
| `market_outcomes` | The list on a quote request of each carrier that Truvo asked, and how it answered. Each entry names the carrier in `source`. |
| key | Your API key. It starts with `trv_sandbox_` or `trv_live_`. |
| request key | The value of the `Idempotency-Key` header. Over MCP, it's the `request_key` argument. |
| sandbox workspace | The sandbox state of one key: its active scenario, its clock, and its records. |
| scenario | A catalog entry that decides what a sandbox quote request returns. |
| final status | Any quote request status other than `queued` or `running`. |

## Keys and environments

Send your key as a bearer token on every request:

```http
Authorization: Bearer trv_sandbox_...
```

- Both environments use the base URL `https://api.truvo.com/v1/`, and the key prefix picks the environment: `trv_sandbox_` is `sandbox`, and `trv_live_` is `live`. After the prefix, a key has 32 letters and digits.
- Truvo reads the environment and the customer from the key. Don't send `environment`, `customer`, or `customer_id` in the query or the body, or an `X-Environment`, `X-Customer`, or `X-Customer-Id` header, because the API refuses that with `422` `invalid_input`.
- The sandbox works before your organization is approved. Live traffic needs Truvo's approval.
- A key proves which integration is calling. It doesn't prove authority over a particular customer, so Truvo checks the two separately. A resource that doesn't exist and one outside your authority get the same `404`.
- A missing or invalid key gets `401` `authentication_failed`. An outage never returns `401`, so treat a `401` as a problem with the key.
- Every answer, success or error, carries a `Request-Id` header. An error body repeats it as `request_id`.

More: [Keys and environments](/guides/authentication).

## Operations

Every path sits under `https://api.truvo.com`. The OpenAPI document for these operations is `https://api.truvo.com/v1/openapi.json`, and the [API reference](/api-reference) shows the same schemas.

| Operation | HTTP | Request key | MCP tool |
| --- | --- | --- | --- |
| Discover capabilities | `GET /v1/capabilities` | No | `get_insurance_capabilities` |
| Start a quote request | `POST /v1/quote-requests` | Required | `request_insurance_quotes` |
| Read a quote request | `GET /v1/quote-requests/{id}` | No | `get_quote_request` |
| List quote requests | `GET /v1/quote-requests` | No | `list_quote_requests` |
| Cancel a quote request | `POST /v1/quote-requests/{id}/cancel` | No | None |
| Read a quote | `GET /v1/quotes/{id}` | No | `get_insurance_quote` |
| Select a sandbox scenario | `POST /v1/sandbox/scenarios` | Required | None |
| List the sandbox scenarios | `GET /v1/sandbox/scenarios` | No | None |
| Reset the sandbox | `POST /v1/sandbox/reset` | Required | None |
| Run a sandbox control | `POST /v1/sandbox/controls` | Required | None |
| Read the sandbox workspace | `GET /v1/sandbox/workspace` | No | None |
| Send feedback | `POST /v1/feedback` | No | `send_feedback` |
| Read a feedback report | `GET /v1/feedback/{id}` | No | None |
| Create a webhook subscription | `POST /v1/event-subscriptions` | Required | None |
| List or read subscriptions | `GET /v1/event-subscriptions`, `GET /v1/event-subscriptions/{id}` | No | None |
| Change a subscription | `PATCH /v1/event-subscriptions/{id}` | No | None |
| Disable a subscription | `DELETE /v1/event-subscriptions/{id}` | No | None |
| Rotate a signing secret | `POST /v1/event-subscriptions/{id}/rotate` | Required | None |
| Send a test event | `POST /v1/event-subscriptions/{id}/test` | Required | `send_test_event` |
| List a subscription's deliveries | `GET /v1/event-subscriptions/{id}/deliveries` | No | `list_event_deliveries` |
| Read one delivery | `GET /v1/event-subscriptions/{id}/deliveries/{event_id}` | No | `get_event_delivery` |
| Replay a delivery | `POST /v1/event-subscriptions/{id}/replay` | Required | `replay_event_delivery` |
| Check delivery health | `GET /v1/event-subscriptions/{id}/health` | No | `get_event_subscription_health` |
| Read the event feed | `GET /v1/events` | No | None |
| List recent requests | `GET /v1/requests` | No | `list_recent_requests` |
| Read a recent request | `GET /v1/requests/{id}` | No | None |

A cancel answers `cancelled`, or `cannot_stop` when the work already went out and can't stop.

The feedback, webhook, event feed, and request log operations answer `404`, the same as an unknown path, while Truvo has them turned off. The webhook and event feed routes aren't in the OpenAPI document yet.

## Get a quote

These four steps price one personal auto quote in the sandbox. Set your key first:

```bash
export TRUVO_API_KEY="trv_sandbox_..."
```

### 1. Check what your key can do

```bash
curl https://api.truvo.com/v1/capabilities \
  -H "Authorization: Bearer $TRUVO_API_KEY"
```

The answer gives the line, the environment, the states, a JSON Schema of the facts the line requires, and the action types a quote can offer. Build the application from that schema, and ask the person for each fact it needs. Don't fill in a fact with a guess.

### 2. Start a quote request

Send the facts for one line, with a new request key. Every value in this body is synthetic:

```bash
curl -X POST https://api.truvo.com/v1/quote-requests \
  -H "Authorization: Bearer $TRUVO_API_KEY" \
  -H "Idempotency-Key: quickstart-quote-01" \
  -H "Content-Type: application/json" \
  -d '{
    "line": "personal_auto",
    "consent_receipt_id": "consent-auto-one-owned",
    "application": {
      "applicant": {
        "name": { "first_name": "Test", "last_name": "Applicant One" },
        "email": "alex@example.com",
        "phone": "5550100000"
      },
      "garaging_address": {
        "line1": "100 Example Way",
        "line2": null,
        "city": "Austin",
        "state": "TX",
        "postal_code": "78701"
      },
      "drivers": [
        {
          "name": { "first_name": "Test", "last_name": "Applicant One" },
          "date_of_birth": "1990-04-12",
          "gender": "female"
        }
      ],
      "vehicles": [
        {
          "vin": "SYNTHVIN000000001",
          "year": 2019,
          "make": "Toyota",
          "model": "Camry",
          "ownership": "owned",
          "use": "pleasure",
          "annual_mileage": 8000,
          "comprehensive_deductible_minor": 100000,
          "collision_deductible_minor": 100000
        }
      ],
      "protection": {
        "bodily_injury_per_person_minor": 10000000,
        "bodily_injury_per_accident_minor": 30000000,
        "property_damage_minor": 5000000
      }
    }
  }'
```

- `consent_receipt_id` is the ID of the applicant's consent for Truvo to get quotes for them. It covers quoting only. [Consent receipts](#consent-receipts) lists the other receipts.
- For auto, driver 1 is the named insured, and the API takes the applicant's date of birth from driver 1.
- Money values are integers in minor units, so `10000000` is $100,000.

The API answers `202` with a quote request in the `queued` status. Save its `id`.

This body names no scenario, so in a new sandbox workspace it runs `auto-one-driver-one-vehicle` and returns one quote. [Sandbox](#sandbox) shows how to pick another outcome.

### 3. Wait for the result

```bash
curl https://api.truvo.com/v1/quote-requests/qreq_... \
  -H "Authorization: Bearer $TRUVO_API_KEY" \
  -H "Prefer: wait=20"
```

Read it again while the status is `queued` or `running`. [Polling](#polling) has a loop that does this.

### 4. Read a quote

Once the status is `completed`, `quotes` gives the ID and the version of each quote. Read one:

```bash
curl https://api.truvo.com/v1/quotes/qt_... \
  -H "Authorization: Bearer $TRUVO_API_KEY"
```

Without `version`, you get the latest version. Add `?version=` to read a specific one.

## Consent receipts

Each receipt is the ID of the person's own record of one choice, and it covers only that choice. Never send one receipt ID in two fields, and never make a receipt up. Truvo reads IDs that differ only in case or separators, or that add segments to another ID on either side, as one receipt: `Consent_1`, `consent-1.contact`, and `x:consent-1:y` all match `consent-1`.

| Field | Covers | Lines |
| --- | --- | --- |
| `consent_receipt_id` | Quoting. Required. | All |
| `credit_disclosure_receipt_id` | A credit check. Without it, `credit_mode` is `declined`. | `personal_auto` |
| `contact_consent` | Contact by each channel in its `channels`: `email`, `sms`, or `phone`. | All |

`contact_consent` holds the receipt ID and the channels the person agreed to, each listed once. Set a channel in `contact_channels` to `true` only if the receipt lists it. A channel you leave out stays `false`.

```json
"contact_consent": { "receipt_id": "tcpa-7f3a91", "channels": ["email", "phone"] },
"contact_channels": { "email": true, "phone": true }
```

A mismatch gets `422` `invalid_input` with a field error:

| Request | Field error |
| --- | --- |
| A channel set to `true` without `contact_consent` | `contact_channels.phone`: Send a contact consent receipt, or set the channel to false. |
| A channel that the contact receipt doesn't list | `contact_channels.phone`: The contact consent receipt does not cover contact by phone. |
| The quote receipt as `contact_consent` | `contact_consent.receipt_id`: This receipt matches the quote consent receipt. It does not cover contact. |
| The quote receipt as `credit_disclosure_receipt_id` | `credit_disclosure_receipt_id`: This receipt matches the quote consent receipt. It does not cover a credit check. |
| `contact_consent` as a string | `contact_consent`: Send contact_consent as an object with receipt_id and channels. |

## The quote request

`queued` and `running` mean the work is still going. Every other status is final, so stop reading when you see one.

| Status | Final |
| --- | --- |
| `queued` | No |
| `running` | No |
| `completed` | Yes |
| `failed` | Yes |
| `cancelled` | Yes |

- **`completeness`** is `pending`, `partial`, or `complete`. A partial result can still be useful.
- **`quotes`** gives the ID and the version of each quote.
- **`market_outcomes`** gives one outcome for each carrier: `quote`, `decline`, `missing_information`, `failed`, `unknown`, or `no_answer`. A `quote` outcome names its `quote_id` and `quote_version`.
- **`requirements`** lists what's still needed. Each entry has a `kind` (`missing_fact` or `interaction`), a `path`, and a `code`.
- **`next_actions`** lists what you can do next.
- **`failure`** is `null` unless the request failed. Then it says why: `no_quotes_by_deadline` or `quote_run_not_started`. Its retry action is `use_new_request_key`, so send the request again with a new `Idempotency-Key`.

A `completed` request with zero quotes isn't a failure: a decline or an answer with no offers still completes. Check `market_outcomes` to see how each carrier answered.

Truvo stops collecting answers 60 seconds after it accepts a quote request. A carrier that hasn't answered by then reads `no_answer`, and an answer that arrives later never reopens the request.

More: [Get quote results](/guides/polling).

## The quote

| Field | What it holds |
| --- | --- |
| `source` | The `carrier` and the `product` that priced the quote. |
| `price` | `premium`, with an `amount_minor` and a `currency` of `USD`, or `null` when the price is unknown. Also `term_months`, `billing_cycle` (`pay_in_full` or `monthly`), `fees`, and `comparison`. `premium` is the price for the whole term, as the carrier states it. On a renters quote, `premium` already includes every fee in `fees`: show it as the total, and don't add the fees to it. On other lines, the quote doesn't say whether `premium` includes `fees`. |
| `coverage` | `conditions` and the covered `subjects`. |
| `availability` | `status` (`active`, `expired`, `withdrawn`, or `unknown`), `expires_at`, and `revalidation` (`none`, `required`, or `unknown`). When a renters quote states `expires_at`, it is 24 hours after Truvo rated the quote, or the start of the start date at the rented address, whichever comes first. |
| `next_actions` | The actions this quote supports. |
| `evidence` | `label` (`mock`, `test`, or `live`), `source_time`, and the `assumptions` the quote rests on, each with a `path`, a `value`, and a `source`. |

The next action types are `read_status`, `continue_on_carrier_site`, `prepare_purchase`, `open_hosted_checkout`, and `request_assistance`. A quote lists only the ones it supports. Two of them work like this:

- **`read_status`** names a resource to read again.
- **`continue_on_carrier_site`** gives a `url` to open in a new tab. The link goes through a Truvo redirect to the carrier's site and works until `availability.expires_at`. After that, it opens a Truvo page that says the quote has expired. In the sandbox, the link opens a Truvo sandbox page, never a carrier site.

An expired quote lists only `read_status`. Send a new quote request for a new price.

### Rules for showing quotes to a person

- Offer only the actions a quote lists. Never infer an action from the line or the carrier.
- A quote is a price offer, not coverage. A checkout link isn't proof of payment or coverage either.
- One carrier's quote isn't a market comparison.
- An unknown value stays unknown. Don't fill in a price, a fee, or a limit that the quote doesn't give.
- A sandbox quote has `evidence.label` `mock`, no comparison amount, and the coverage condition `Sandbox premium. Not a carrier price.`

## Polling

A quote request runs in the background, so read it until its status is final. A read never starts a new rating run.

- Add `Prefer: wait=N` to hold the read open for up to `N` seconds, where `N` is a whole number from 0 to 20. The API answers as soon as the status is final, or when the time runs out. Without the header, it answers at once.
- When the server held the read open, the answer carries `Preference-Applied: wait=N`.
- When the server can't hold the read, it answers at once without `Preference-Applied` and with `Retry-After`. Wait that many seconds before you read again.
- A `Prefer` value that isn't valid gets `422` `invalid_input`.
- Over MCP, `get_quote_request` takes `wait_seconds`, from 0 to 20, in place of the header.

This loop waits for the `Retry-After` seconds when the answer has that header, and for 1 second when the server didn't hold the read open:

```bash
while true; do
  body=$(curl -s -D headers.txt "https://api.truvo.com/v1/quote-requests/$QUOTE_REQUEST_ID" \
    -H "Authorization: Bearer $TRUVO_API_KEY" \
    -H "Prefer: wait=20")
  state=$(printf '%s' "$body" | jq -r .status)
  if [ "$state" != "queued" ] && [ "$state" != "running" ]; then
    break
  fi
  retry_after=$(awk -F': ' 'tolower($1) == "retry-after" { print $2 + 0 }' headers.txt)
  if [ -n "$retry_after" ]; then
    sleep "$retry_after"
  elif ! grep -qi '^preference-applied:' headers.txt; then
    sleep 1
  fi
done
printf '%s\n' "$body"
```

## Request keys

Networks drop responses. If that happens after you send a create, you can't tell whether Truvo accepted it. A request key fixes that: send the same request with the same key, and Truvo returns the original result instead of starting a second request.

- `POST /v1/quote-requests`, `POST /v1/event-subscriptions`, `POST /v1/event-subscriptions/{id}/rotate`, `POST /v1/event-subscriptions/{id}/test`, `POST /v1/event-subscriptions/{id}/replay`, and the three sandbox commands (`POST /v1/sandbox/scenarios`, `POST /v1/sandbox/reset`, and `POST /v1/sandbox/controls`) require the `Idempotency-Key` header. Reads don't take one.
- A key is 1 to 255 characters long, made of letters, digits, `.`, `_`, `:`, and `-`. Use a new key for each new request. A UUID works well.
- The same key with the same body returns the original result. Truvo checks your current authority first, and it doesn't start a second request.
- The same key with a different body gets `409` `conflict`, with the retry action `use_new_request_key`. In the sandbox, a changed `scenario` counts as a different body.
- A key's scope is one environment, one integration, one resource, and one operation.
- Over MCP, `request_key` carries the same key, with the same replay rules.

A key replays for at least 24 hours.

More: [Retry safely with request keys](/guides/request-keys).

## Errors

When a request fails, the body tells you what went wrong, which fields caused it, and what to do about it. Branch your code on `error.code` and `error.retry_action.type`. The `message` is written for a person.

```json
{
  "error": {
    "code": "needs_information",
    "message": "The quote request needs more information.",
    "field_errors": [
      {
        "path": "application.applicant.email",
        "message": "This fact is required."
      }
    ],
    "retry_action": {
      "type": "supply_facts",
      "summary": "Supply the missing facts and send the request again."
    },
    "doc_url": "https://docs.truvo.com/v1/errors/needs_information"
  },
  "request_id": "req_..."
}
```

`field_errors` is a list of fields, each with a `path` and a `message`, or `null`. `doc_url` has the form `https://docs.truvo.com/v1/errors/<code>`, and it opens the code table in the [Errors](/guides/errors) guide.

| Code | HTTP | What it means |
| --- | --- | --- |
| `invalid_json` | 400 | The request body isn't valid JSON. |
| `invalid_cursor` | 400 | The `cursor` isn't one that this list or feed returned. It applies to every paged read: `GET /v1/quote-requests`, `GET /v1/requests`, `GET /v1/event-subscriptions`, and the event feed, `GET /v1/events`. Send a `next_cursor` from an earlier page of the same read, or leave the cursor out to start again. |
| `authentication_failed` | 401 | The key is missing or isn't valid. |
| `forbidden` | 403 | The key doesn't have authority for this action. |
| `not_found` | 404 | The resource doesn't exist, or it's outside your authority. |
| `conflict` | 409 | A request key conflict or a version conflict. |
| `quote_expired` | 409 | The quote has expired. |
| `price_changed` | 409 | The quote has a newer version with a different price. |
| `purchase_not_supported` | 409 | The quote has no purchase through the API. |
| `event_cursor_expired` | 410 | The event feed, `GET /v1/events`, keeps events for 30 days. It can't resume from this cursor because retention deleted an event after it, or a sandbox reset voided it. Read each resource you track, then read the feed again without a cursor. |
| `request_too_large` | 413 | The request body is too large. |
| `invalid_input` | 422 | The input isn't valid. `field_errors` lists the fields. |
| `needs_information` | 422 | Required facts are missing. Truvo returns this before it asks any carrier. |
| `rate_limited` | 429 | The key or its organization reached a rate limit, per minute or, in the sandbox, per day. The message names the limit. |
| `internal_error` | 500 | Something went wrong inside Truvo. |
| `service_unavailable` | 503 | A service that Truvo needs is down. Try the request again later, after the seconds in the `Retry-After` header. |

Only the purchase operations return `quote_expired`, `price_changed`, and `purchase_not_supported`. The API doesn't have those operations yet.

| Retry action | What to do |
| --- | --- |
| `none` | Don't send the request again. |
| `retry` | Try the request again later. |
| `correct_input` | Fix the fields in `field_errors`, then send the request again. |
| `supply_facts` | Add the missing facts in `field_errors`, then send the request again. |
| `read_status` | Read the resource again. |
| `use_new_request_key` | Send the request with a new `Idempotency-Key`. |
| `wait` | Wait, then send the request again. |
| `contact_support` | Contact Truvo support with the `request_id`. |

### When to retry

- **A timeout or a network error.** Send the same body with the same request key.
- **`503` `service_unavailable`.** Wait for the time in `Retry-After`, then send the same body with the same key.
- **`500` `internal_error`.** Send the same body with the same key.
- **`429` `rate_limited`.** Wait for the time in `Retry-After`, then send the request again.
- **`409` `conflict`.** Your body is different from the first request with this key. To make a new request, use a new key.
- **A quote request that ends `failed`.** Send it again with a new key.

Every operation except capabilities reports its rate limit in the `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers. They describe the limit you're closest to reaching. `RateLimit-Reset` is the seconds until its count resets. Most windows are fixed at one minute, and a burst of up to twice the limit across a window edge is possible.

The sandbox also has daily limits, which reset at 00:00 UTC. Each organization can send 5,000 quote requests a day, and each call that counts toward its quote request limit counts toward this one too, even one that fails validation. When all organizations together have 25,000 sandbox quote requests accepted in a day, the sandbox is busy: until 00:00 UTC, it accepts new quote requests only from organizations with fewer than 500 accepted that day, up to 50,000 in all. A request that fails validation doesn't count toward the 25,000 and 50,000 totals. A `429` from a daily limit says `Try again after 00:00 UTC.`, and `Retry-After` gives the seconds until then.

Truvo can suspend an organization's sandbox, for example when its traffic looks like abuse. Then each of its sandbox keys gets `403` `forbidden` with the retry action `contact_support`, and it can't create sandbox keys, until Truvo restores it. Its live keys keep working.

More: [Errors](/guides/errors).

## Webhooks

Instead of polling, subscribe a URL to the event types you want, and Truvo sends a signed `POST` each time one happens. The [Webhooks](/guides/webhooks) guide has the details and the list of event types.

- **Subscribe.** Send `POST /v1/event-subscriptions` with an `Idempotency-Key`, an `https` `url` on a public host, and the exact `event_types` you want. There's no wildcard. The answer shows the `signing_secret` once, so store it right away. Only a server key can manage subscriptions, and each integration can hold 10 active subscriptions in each environment. `PATCH` and `DELETE` on `/v1/event-subscriptions/{id}` change or disable one, and `POST /v1/event-subscriptions/{id}/rotate` replaces the secret. The old secret keeps working for 24 hours.
- **Verify.** Each delivery carries the `svix-id`, `svix-timestamp`, and `svix-signature` headers. The signature is an HMAC-SHA256 over `{svix-id}.{svix-timestamp}.{body}`, keyed with the part of your signing secret after `whsec_`, decoded from base64. Verify the raw body, before any JSON parsing. The `svix` libraries accept these headers as they are, and their `verify` refuses a timestamp more than 5 minutes from your clock.
- **Retries.** Answer `2xx` within 15 seconds. Any other answer counts as a failure, and the message is sent again after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and 10 hours. If every delivery fails for 5 days, delivery turns off until you send `PATCH` with your URL. The same event can arrive more than once and out of order, so skip an `id` you've already processed.
- **Thin events.** An event names the resource that changed, in `resource.type`, `resource.id`, and `resource.version`, and it carries no customer data. Read the resource to see what changed, and never apply an older `resource.version` over a newer one. Ignore an event type or a field you don't know, and still answer `2xx`.
- **Resync through the feed.** `GET /v1/events` holds every event of the last 30 days, with the same `id` as its webhook. Page through it with `cursor` and `limit` (10 by default, 100 at most), store each `next_cursor` only after your work is saved, and stop at an empty page. After `event_cursor_expired`, read each resource you track, then read the feed again without a cursor.

To check your endpoint, send a test event with `POST /v1/event-subscriptions/{id}/test` and a new `Idempotency-Key`. To see a real delivery in the sandbox, start a quote request or run the `force_expiry` control.

## Sandbox

Each sandbox key has its own sandbox workspace, with an active scenario, its own clock, and your sandbox records. The paths under `/v1/sandbox/` work only with a sandbox key: a live key gets `404`, and a sandbox key without authority gets `403`.

A scenario decides what a sandbox quote request returns. Pick one in either of two ways:

- **For one request:** add `"scenario": "<id>"` to the body of `POST /v1/quote-requests`, or the `scenario` argument over MCP. The scenario must be for the request's line. A live request that names a scenario gets `422` `invalid_input`.
- **For the workspace:** `POST /v1/sandbox/scenarios` with `{"scenario": "<id>"}` and an `Idempotency-Key`. The scenario applies to each later quote request of its line, until you select another one or reset the workspace. Requests for other lines keep their default.

A new workspace has no scenario selected, so each line runs its default and returns one quote: auto runs `auto-one-driver-one-vehicle`, renters runs `renters-one-offer`, and pet runs `pet-one-dog`. The default sandbox scenario prices whatever pets, drivers, or vehicles you send. `GET /v1/sandbox/scenarios` lists the whole catalog, with each scenario's line, summary, status, and completeness.

| Line | Scenario | Result |
| --- | --- | --- |
| Auto | `auto-one-driver-one-vehicle` | One driver and one owned vehicle. |
| Auto | `auto-several-drivers-vehicles` | Three drivers and two vehicles. One vehicle is owned and one is financed. |
| Auto | `auto-financed-vehicle` | One driver and one financed vehicle. |
| Auto | `auto-leased-vehicle` | One driver and one leased vehicle for business use. |
| Auto | `auto-several-offers` | Two mock offers for one owned vehicle. |
| Auto | `auto-no-offers` | The request completes with zero quotes. |
| Auto | `auto-partial-result` | One mock offer and one failed carrier. |
| Auto | `auto-decline` | The carrier declines. The request completes with zero quotes. |
| Auto | `auto-expiry` | The mock offer is already expired. |
| Auto | `auto-delay` | The mock offer arrives after a fixed delay. |
| Auto | `auto-no-answer` | One mock offer arrives after the collection deadline. |
| Auto | `auto-unknown-result` | The carrier's answer is unknown. The API doesn't invent a premium. |
| Auto | `auto-duplicate-delivery` | The same mock offer arrives twice. |
| Renters | `renters-one-offer` | One mock renters offer. |
| Renters | `renters-no-active-product` | A Florida address, where not every renters carrier quotes. The request still completes with one mock offer. |
| Renters | `renters-several-offers` | Two mock renters offers, each with its own quote ID. |
| Renters | `renters-no-offers` | The request completes with zero quotes. |
| Renters | `renters-partial-result` | One mock offer and one failed carrier. |
| Renters | `renters-decline` | The carrier declines. The request completes with zero quotes. |
| Renters | `renters-expiry` | The mock offer is already expired. |
| Renters | `renters-delay` | The mock offer arrives after a fixed delay. |
| Renters | `renters-no-answer` | One mock offer arrives after the collection deadline. |
| Renters | `renters-unknown-result` | The carrier's answer is unknown. The API doesn't invent a premium. |
| Renters | `renters-duplicate-delivery` | The same mock offer arrives twice, with the same quote ID. |
| Pet | `pet-one-dog` | One mock pet offer for one dog. |
| Pet | `pet-several-carriers` | Three mock carriers quote one dog, each with its own quote ID. |
| Pet | `pet-two-pets` | One mock offer for a dog and a cat, with one subject for each pet. |
| Pet | `pet-mixed-breed` | No carrier breed matches the requested breed, so the carrier rates each pet as mixed and the quote labels it with `breed_rated_as_mixed`. |
| Pet | `pet-coverage-adjustment` | The carrier sells a $250 or a $1,000 deductible and moves any other requested deductible to the next lower one. The quote labels this with `carrier_coverage_adjustment`. |
| Pet | `pet-unsupported-state` | One carrier doesn't sell in the owner's state and declines. The other carrier quotes. |
| Pet | `pet-decline` | The carrier declines the household. The request completes with zero quotes. |
| Pet | `pet-no-offers` | No carrier returns an offer. The request completes with zero quotes. |
| Pet | `pet-no-answer` | One mock pet offer arrives after the collection deadline. |

The catalog also has purchase and payment scenarios for auto and renters, but the API doesn't have the purchase operations yet.

Sandbox controls go to `POST /v1/sandbox/controls`, one at a time, each with its own `Idempotency-Key`:

| Control | Body | Effect |
| --- | --- | --- |
| `advance_time` | `{"control": "advance_time", "by_seconds": 86400}` | Moves the workspace clock forward. Sandbox quotes expire 30 days after Truvo creates them. |
| `force_expiry` | `{"control": "force_expiry"}` | Quotes that Truvo created at or before this time read as expired. |
| `replay_events` | `{"control": "replay_events", "scenario": "..."}` | Needs an event scenario, and the catalog has none yet, so this control answers `422` `invalid_input` for every scenario. To see an event, make a real change, such as a quote request or `force_expiry`. |

`POST /v1/sandbox/reset` with the body `{}` and an `Idempotency-Key` clears the workspace's records, and puts the controls and every line's scenario back to their start values. `GET /v1/sandbox/workspace` returns `active_scenario`, `clock_offset_seconds`, `workspace_time`, `quotes_expired_at`, `reset_count`, and `scenario_catalog_version`.

More: [Sandbox scenarios](/guides/sandbox-scenarios).

## MCP

The Truvo MCP server at `https://api.truvo.com/v1/mcp` serves the quote operations as tools, over Streamable HTTP, with the same key as a bearer token.

| Tool | What it does |
| --- | --- |
| `get_insurance_capabilities` | Shows what the key can quote for one `line`. |
| `request_insurance_quotes` | Starts one quote request. It requires a `request_key`, and in the sandbox it takes an optional `scenario`. |
| `get_quote_request` | Reads a quote request, with each quote inline. `wait_seconds` holds the answer for up to 20 seconds. |
| `list_quote_requests` | Lists the key's quote requests, newest first. It filters by `status`, `line`, `created_after`, `created_before`, or `updated_since`, and pages with `limit` and `cursor`. |
| `get_insurance_quote` | Reads one quote. Leave out `version` to read the latest version. |
| `send_feedback` | Sends a feedback report. The server lists it only while Truvo accepts feedback. |
| `list_recent_requests` | Lists the integration's recent requests, newest first, with each request's status, error code, and request ID. The server lists it only while the request log is on. |
| `send_test_event` | Sends a test event to one webhook subscription. It requires a `request_key`. |
| `list_event_deliveries` | Lists one subscription's deliveries from the last 30 days, newest first. |
| `get_event_delivery` | Reads one delivery, with its attempts. |
| `replay_event_delivery` | Sends an event from the delivery log again, with the same event ID. It requires a `request_key`. |
| `get_event_subscription_health` | Shows whether delivery to one subscription works. |

The server lists the five webhook tools only while webhooks are on. A refused call comes back as a tool error that carries the same error envelope as HTTP. Canceling a quote request and the sandbox controls have no tool, so use HTTP for those.

More: [Agent setup](/guides/agent-setup), with the config for Claude Code, Cursor, Codex, and VS Code.

## Feedback

If the API is missing something, an answer is wrong, or an error or a page is unclear, tell Truvo. `POST /v1/feedback` takes a JSON body:

- **`kind`:** `missing_feature`, `bug`, `unclear_error`, `unclear_docs`, or `other`. Required.
- **`summary`:** one line, up to 200 characters, that says what went wrong or what's missing. Required.
- **`expected`** and **`actual`:** what you expected, and what happened instead, up to 2,000 characters each. Required.
- **`tried`:** the `operation` or MCP tool you called, and the `request_ids` of up to 20 calls that show the problem.
- **`agent`:** your agent's `name`, such as Claude Code, and its `version`.

The API answers `201` with a `feedback_id` that starts with `fb_` and the status `received`. `GET /v1/feedback/{id}` returns the report's status (`received`, `planned`, `shipped`, or `declined`) and an optional note from Truvo. Leave out personal data: the API refuses text that looks like a Social Security, driver's license, or card number.

## Links

- [Quickstart](/guides/quickstart): the same first quote, step by step.
- [Agent setup](/guides/agent-setup): connect an MCP client, and an `AGENTS.md` block for your repo.
- [Webhooks](/guides/webhooks): subscriptions, signatures, retries, event types, and the event feed.
- [API reference](/api-reference): every operation and its schemas.
- `/llms.txt`: an index of every docs page. Add `.md` to any page URL to get its Markdown.