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

# For coding agents

Want to connect your own AI app to Truvo to get quotes for yourself? Go to [truvo.com/ai](https://www.truvo.com/ai).

This file is for coding agents. It covers what the Truvo API does, how to authenticate, every operation, the sandbox, and the rules for errors, retries, polling, and request keys, and each section links to the guide with more detail. Every example uses the sandbox, where quotes are synthetic and a 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:** the customer buys on the carrier's site.

## 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 the carriers that Truvo asked, and how each answered. Each entry names the carrier in `carrier.id` and `carrier.name`. |
| 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. |
| final status | `completed`, `incomplete` or `failed`. A quote request in a final status doesn't change again. |

## 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 approval. A server key can call `GET /v1/capabilities` before it sends a quote request. A platform key reads capabilities with MCP `get_capabilities`. While approval is pending, the server key gets `200` with no capabilities and `access.live` set to `not_approved`. A platform key reads `access.live` through MCP `get_capabilities`. Other server-key requests to key-protected routes that are on return `403` `live_access_not_approved`, with the reason `pending_review`, unless the address is rate limited. Other MCP tool calls return that answer as a tool error with `isError: true`. Public routes stay unchanged. To get live access, an owner or admin books a call from the [Go live](https://partners.truvo.com/dashboard/go-live) page of the portal. After the call, Truvo approves the organization and issues its live keys.
- A key proves which integration is calling, and Truvo checks the integration's authority over each customer separately. A resource that doesn't exist and one outside your authority get the same `404`. A server key whose live access is waiting for approval gets the shared `403` on a key-protected route that is on, unless the address is rate limited.
- 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`.
- Every request to an API operation also sends a `Truvo-Version` header with the revision ID that your code is built for, and the answer names the same revision. See [Versions](#versions).

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.

Each operation has one name, which the request log and the MCP tools use.

| Name | HTTP | MCP tool |
| --- | --- | --- |
| `get_capabilities` | `GET /v1/capabilities` | Yes |
| `start_quote_request` | `POST /v1/quote-requests` with an `Idempotency-Key` | Yes |
| `get_quote_request` | `GET /v1/quote-requests/{id}` | Yes |
| `list_quote_requests` | `GET /v1/quote-requests` | Yes |
| `get_quote` | `GET /v1/quotes/{id}` | Yes |
| `send_feedback` | `POST /v1/feedback` | Yes |
| `get_feedback` | `GET /v1/feedback/{id}` | No |
| `create_event_subscription` | `POST /v1/event-subscriptions` with an `Idempotency-Key` | No |
| `list_event_subscriptions` | `GET /v1/event-subscriptions` | No |
| `get_event_subscription` | `GET /v1/event-subscriptions/{id}` | No |
| `update_event_subscription` | `PATCH /v1/event-subscriptions/{id}` | No |
| `disable_event_subscription` | `DELETE /v1/event-subscriptions/{id}` | No |
| `rotate_event_subscription_secret` | `POST /v1/event-subscriptions/{id}/rotate` with an `Idempotency-Key` | No |
| `send_test_event` | `POST /v1/event-subscriptions/{id}/test` with an `Idempotency-Key` | Yes |
| `list_event_deliveries` | `GET /v1/event-subscriptions/{id}/deliveries` | Yes |
| `get_event_delivery` | `GET /v1/event-subscriptions/{id}/deliveries/{event_id}` | Yes |
| `replay_event_delivery` | `POST /v1/event-subscriptions/{id}/replay` with an `Idempotency-Key` | Yes |
| `get_event_subscription_health` | `GET /v1/event-subscriptions/{id}/health` | Yes |
| `recover_events` | `GET /v1/events` | No |
| `list_api_requests` | `GET /v1/requests` | Yes |
| `get_api_request` | `GET /v1/requests/{id}` | No |

The feedback, webhook, and event feed operations answer `404`, the same as an unknown path, while Truvo has them turned off.

## 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" \
  -H "Truvo-Version: 2026-09-26"
```

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.

The answer also gives Truvo's quote disclosure in `quote_disclosure`. Before you ask for quotes, show the person its `text` as written and ask whether they agree. See [The quote disclosure](#the-quote-disclosure).

`agent_guidance` lists the rules Truvo asks an agent to keep when it acts for the person, each with a `type` and its `text`: `facts_only`, `disclosure`, `results`, and, while Truvo accepts feedback from your key, `feedback`. The MCP server gives the same rules in its instructions. The `version` changes whenever the text of a rule changes.

### 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 "Truvo-Version: 2026-09-26" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "line": "personal_auto",
    "quote_disclosure_version": "2026-10-06.1",
    "quote_disclosure_accepted": true,
    "application": {
      "applicant": {
        "name": { "first_name": "Test", "last_name": "Applicant One" },
        "email": "alex@example.com",
        "phone": "512-555-0123"
      },
      "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
      }
    }
  }'
```

- `quote_disclosure_version` is the `version` from `quote_disclosure` in step 1, and `quote_disclosure_accepted` is `true` once the person agrees. [The quote disclosure](#the-quote-disclosure) has the rules.
- 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 whose `status` is `in_progress`. Save its `id`.

In the sandbox, this body returns one quote.

### 3. Wait for the result

Send the `version` from the `202` answer in `since_version`:

```bash
curl "https://api.truvo.com/v1/quote-requests/qreq_...?since_version=1" \
  -H "Authorization: Bearer $TRUVO_API_KEY" \
  -H "Truvo-Version: 2026-09-26" \
  -H "Prefer: wait=20"
```

The read answers when the request changes, and a request in progress already shows the quotes so far. While the status is `in_progress`, read it again with the new `version`. [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" \
  -H "Truvo-Version: 2026-09-26"
```

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

## The quote disclosure

Before you ask for quotes, the person agrees to Truvo's quote disclosure. Truvo records their consent, so you send no consent receipt.

1. Read `quote_disclosure` in the answer of `GET /v1/capabilities`. It holds the disclosure `text`, its `version`, its `sha256`, and the `privacy_notice_url`.
2. Show the person the `text` as written, and ask whether they agree.
3. Only if they agree, send the `version` in `quote_disclosure_version` and `true` in `quote_disclosure_accepted`. If they don't agree, don't send the request.

Their yes covers the quote, contact by phone, text, and email, and for auto a credit check. So every request needs the person's email and a US phone number, on every line.

A request that breaks a rule gets one of these answers:

| Request | Answer |
| --- | --- |
| No `quote_disclosure_version`, or a version that isn't the current one | `422` `invalid_input`, with the field error `quote_disclosure_version`: Send the version of Truvo's current quote disclosure. |
| A `quote_disclosure_accepted` that isn't `true` | `422` `invalid_input`, with the field error `quote_disclosure_accepted`: Send true only after the person agrees to the quote disclosure. |
| No email, or no phone number | `422` `needs_information`, with a field error for each missing fact. |
| A phone number that isn't a US number | `422` `invalid_input`, with the field error `application.applicant.phone`: Send a US phone number with its area code, such as 512-555-0123. A live request gets the same message without the example. On pet, the path is `application.owner.phone`. |
| On a live request, a phone number that nobody could answer, such as `512-555-0123` | `422` `invalid_input`, with the field error `application.applicant.phone`: This is not a phone number a person can answer. Send the person's own number. On pet, the path is `application.owner.phone`. The sandbox accepts such a number. |
| `consent_receipt_id`, `credit_disclosure_receipt_id`, `contact_consent`, or `contact_channels` | `422` `invalid_input`, with a field error for each one: This field is not part of the request. |

## The quote request

Switch on `status`. It has five values, and a new one comes only in a new revision.

| Status | Final? | Normal? | What to do |
| --- | --- | --- | --- |
| `in_progress` | No | Yes | Read again. Quotes can show before the request finishes. |
| `action_required` | No | Yes | Not used yet. When used, show `status_details.message`, then follow `next_actions`. |
| `completed` | Yes | Yes | Show the quotes. A completed request can hold no quotes. |
| `incomplete` | Yes | Yes | Show `status_details.message`. Follow its retry action if you try a new request. A retry may still return no quotes. |
| `failed` | Yes | No | Show `status_details.message`, then follow `status_details.retry_action`. |

- **`status_details`** says why: a `reason` and a `message` that you can show the person. The reasons today are `queued` and `collecting` (`in_progress`), `quoted`, `partly_quoted`, `no_quotes` and `offline_quotes_only` (`completed`), `carriers_unavailable` and `no_carrier_answered` (`incomplete`), and `quote_run_failed` and `quote_run_not_started` (`failed`). New reasons can arrive in any revision, so show `message`, and give any switch on `reason` a default branch.
- `failed` is an error on Truvo's side and always says what to do next. `incomplete` is a finished result where carriers gave no final answer. Neither `completed` nor `incomplete` is an error.
- **`quotes`** gives the ID and the version of each quote. A request returns only quotes that the person can buy, one for each product a carrier prices.
- **`market_outcomes`** gives one outcome for each carrier, with the carrier's ID and name in `carrier.id` and `carrier.name`: `quote`, `offline_quote`, `decline`, `missing_information`, `pending`, or `no_answer`. A `quote` outcome names its `quote_id` and `quote_version`, the carrier's first quote when it has several. `offline_quote` means the carrier offered a price that the person can't buy online through Truvo.
- **`assumptions`** lists the defaults that Truvo set before it asked carriers. Each has a `path`, a `value`, a `source`, and a `label` that says why in plain words.
- **`next_actions`** lists what you can do next. A personal auto request that completes with a quote can list `continue_on_truvo`: a `url` to Truvo's results page for the request, to open in a new tab, where the customer can see more options and go on to buy. A live request lists it only when the customer agreed to the quote disclosure that includes contact consent. The page shows the customer's details to anyone who opens the link, so give it only to the customer. In the sandbox, the link opens a Truvo sandbox page.
- **`status_details.retry_action`** is `null` unless the request is `incomplete` or `failed`. When a request failed, follow its retry action. When it is incomplete, follow it only if you try a new request. In either case, use a new `Idempotency-Key`. A retry may still return no quotes.

A request with zero quotes completes with `offline_quotes_only` when a carrier reads `offline_quote`, or with `no_quotes` when every carrier result is a final answer. With no quote or offline quote, a decline beside a carrier that gave no usable answer ends as `incomplete`. You can try a new request later, but it may still return no quotes. When a request failed, follow its retry action. When it is incomplete, follow it only if you try a new request.

Truvo currently stops collecting answers 60 seconds after it accepts a quote request. A carrier that hasn't answered by then reads `no_answer`. A finished request doesn't change.

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

## The quote

| Field | What it holds |
| --- | --- |
| `status` | `active` or `inactive`. Offer the quote's next actions only while it's `active`. A new value comes only in a new revision. |
| `status_details` | Why the quote has its status: a `reason` and a `message` that you can show the person. The reasons today are `available` (`active`), and `expired` (`inactive`). New reasons can arrive in any revision. |
| `source` | The `carrier` and the `product` that priced the quote, and `carrier_quote_id`: the carrier's own number for the quote, or `null`. A person can give that number to the carrier. `product` is the name of the carrier's product, or `null` when Truvo has none. A carrier can give a quote for each of several products. |
| `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`), and `fees`. `premium` is the price for the whole term of `term_months` months, as the carrier states it, even when `billing_cycle` is `monthly`. `term_months` is `null` when the term is unknown. 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` | `expires_at`, when the quote turns `inactive`, and the value to rely on. Truvo currently sets it to the carrier's expiry when the carrier gives one, and otherwise to 30 days after Truvo created the quote. Some renters quotes expire sooner, 24 hours after Truvo rated the quote or when the coverage start date begins at the rented address, whichever comes first. These defaults can change. |
| `next_actions` | The actions this quote supports. |
| `evidence` | `label`: `mock` for a made-up sandbox price, or `live` for a carrier's price. Also the `assumptions` specific to this quote, such as a value the carrier changed. The quote request's `assumptions` lists the ones every quote shares. |

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. New types can arrive in any revision, so skip an action of a type that you don't know. 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 `inactive` quote lists only `read_status`. Send a new quote request for a new price.

### Mobile apps and web views

In a mobile app, open a `continue_on_carrier_site` link in the system browser or an in-app browser tab: `SFSafariViewController` on iOS, or Custom Tabs on Android. Don't load it in an embedded web view such as `WKWebView` or Android `WebView`.

The link redirects to the carrier's own site, where the customer enters personal details and pays. A browser tab shows the carrier's address, and it gives the customer the browser's saved passwords, autofill, and payment methods. An embedded web view shows no address, doesn't share the browser's saved data, and lets the host app read the page, which is the carrier's checkout.

### 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.
- To give a monthly figure, divide `premium.amount_minor` by `term_months` and round up to the dollar. Say that it's the premium spread over the term, not a payment plan. A sandbox quote with a `premium.amount_minor` of 186000 and a `term_months` of 6 is $1,860 for 6 months, or $310 a month spread over the term. When `term_months` is `null`, show only the premium.
- A sandbox quote has `evidence.label` `mock` and the coverage condition `Sandbox premium. Not a carrier price.`

## Polling

A quote request runs in the background, and its quotes arrive one at a time. Each change raises the request's `version`: the start of its run, each new quote, and the final status. A read never starts a new rating run.

- **Waiting reads** are the loop we recommend. Add `Prefer: wait=N` to hold the read open for up to `N` seconds, where `N` is a whole number from 0 to 20, and `since_version` with the `version` from your last answer, starting with the `202` answer.
- The read answers as soon as the request has a version newer than `since_version` or a status other than `in_progress`, so a quote that arrives between two reads answers at once. A `since_version` higher than the current version counts as the current one. Without `since_version`, the wait counts from the version that the read finds first.
- Without `Prefer: wait`, a read answers at once. For quick polling, read every few seconds until the status is no longer `in_progress`.
- 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` or `since_version` value that isn't valid gets `422` `invalid_input`.
- [Webhooks](#webhooks) push each change to your server instead.
- Over MCP, `get_quote_request` takes `wait_seconds` in place of the header, and waits 20 seconds when you leave it out. Read `status` in each result. While it is `in_progress`, call `get_quote_request` again with the same `id`, `wait_seconds: 20`, and `since_version` set to the `version` you just read. If you read by handle, send the same `read_handle` too.

This loop starts from the `version` in the `202` answer, waits out `Retry-After` when an answer has it, and stops when the status is no longer `in_progress`:

```bash
version=1  # the version in the 202 answer
while true; do
  body=$(curl -s -D headers.txt "https://api.truvo.com/v1/quote-requests/$QUOTE_REQUEST_ID?since_version=$version" \
    -H "Authorization: Bearer $TRUVO_API_KEY" \
    -H "Truvo-Version: 2026-09-26" \
    -H "Prefer: wait=20")
  state=$(printf '%s' "$body" | jq -r .status)
  printf '%s\n' "$body" | jq -c '{status, quotes}'
  if [ "$state" != "in_progress" ]; then
    break
  fi
  version=$(printf '%s' "$body" | jq -r .version)
  retry_after=$(awk -F': ' 'tolower($1) == "retry-after" { print $2 + 0 }' headers.txt)
  if [ -n "$retry_after" ]; then
    sleep "$retry_after"
  fi
done
```

More: [Get quote results](/guides/polling), which compares webhooks, quick polling, waiting reads, and MCP on one example request.

## Request keys

If a response is lost after you send a create, you can't tell whether Truvo accepted it. Send the same request again with the same request key. If Truvo accepted the first one, it returns that request instead of starting a second.

- `POST /v1/quote-requests`, `POST /v1/event-subscriptions`, `POST /v1/event-subscriptions/{id}/rotate`, `POST /v1/event-subscriptions/{id}/test`, and `POST /v1/event-subscriptions/{id}/replay` 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, such as a UUID, for each new request.
- The same key with the same body starts no second request, and Truvo checks your current authority first. A quote request comes back as it is now, with the same `id` and its current `status`, `version`, and `quotes`, so the answer can differ from the first one.
- The same key with a different body gets `409` `request_key_reused`, with the retry action `use_new_request_key`.
- 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. With a platform key or without a key, only a random UUID replays; see [Retry safely with request keys](/guides/request-keys).

A key replays for at least 24 hours.

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

## Errors

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.",
        "code": "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`, `message` and `code`, or `null`. A field may also include `format`, `allowed_values` or `did_you_mean`. `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. |
| `invalid_version` | 400 | The `Truvo-Version` header is missing, names no revision that Truvo serves, or names a revision that Truvo retired. The message says which revision to send, and `field_errors[0].allowed_values` lists each revision that Truvo serves, newest first. Send one of them exactly. See [Versions and changes](/guides/versioning). On `/v1/mcp`, the code means that the `MCP-Protocol-Version` header names an MCP protocol version that Truvo doesn't serve. The answer is a JSON-RPC error, so only the request log, `GET /v1/requests` or `list_api_requests`, shows the code. An MCP client that falls back to an older protocol version and connects, as Claude Code does, needs no action. |
| `authentication_failed` | 401 | The key is missing or isn't valid. |
| [`forbidden`](/v1/errors/forbidden) | 403 | The key doesn't have authority for this action. |
| [`organization_suspended`](/v1/errors/organization_suspended) | 403 | Truvo suspended your organization's sandbox or live access. Contact Truvo support. |
| [`live_access_not_approved`](/v1/errors/live_access_not_approved) | 403 | This live key, or the organization of a signed-in member, does not have access to live operations. The reason is `pending_review`. To get live access, book a call from the Go live page of the portal. |
| [`wrong_key_kind`](/v1/errors/wrong_key_kind) | 403 | This key can't access this endpoint. |
| `not_found` | 404 | The resource doesn't exist, or it's outside your authority. |
| [`conflict`](/v1/errors/conflict) | 409 | The resource's current state doesn't allow this request. For example, another call with this request key is still running, a subscription create didn't finish in time, another rotation of the subscription is in progress, the integration holds the most active subscriptions it may hold, the subscription is disabled, or delivery to it is off. |
| [`request_key_reused`](/v1/errors/request_key_reused) | 409 | This request key was used with different input. Send a new key. |
| [`version_conflict`](/v1/errors/version_conflict) | 409 | A newer version exists. Read it before sending the request again. |
| `quote_expired` | 409 | Not returned yet. The quote has expired. |
| `price_changed` | 409 | Not returned yet. The quote has a newer version with a different price. |
| `purchase_not_supported` | 409 | Not returned yet. 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. 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. When a quote request body matches the schema but Truvo refuses a field, `field_errors` also lists each missing fact after the refused fields, with the message `This fact is required.` A quote request in a state where Truvo can't quote the line gets this code too, in the sandbox and live. `field_errors` names the state field. `GET /v1/capabilities` lists the states. |
| `needs_information` | 422 | Required facts are missing. Truvo returns this before it asks any carrier. |
| `rate_limited` | 429 | A rate limit stopped the request. The message names the limit, and the `Retry-After` header gives the seconds to wait. |
| `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. |

`quote_expired`, `price_changed`, and `purchase_not_supported` belong to purchase operations, which the API doesn't serve yet, so no answer returns them today.

| 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 at [partners@truvo.com](mailto:partners@truvo.com) 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` `request_key_reused`.** 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.

Over HTTP, most answers carry 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. An answer can come without these headers, and `GET /v1/capabilities` always does, because it counts toward no limit. An MCP tool result has no rate limit headers, and when a limit stops a tool call, its text says when to try again. Over MCP, `get_capabilities` counts as one read.

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. A webhook subscription change counts even when it fails validation. A new quote request counts only once its input passes validation and has every required fact. Until then, it counts as a read. When all organizations together have 25,000 sandbox quote requests accepted in a day, the sandbox is busy. Until 00:00 UTC, it then 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` `organization_suspended` with the retry action `contact_support`, and it can't create sandbox keys, until Truvo restores it. Its live keys keep working.

Truvo can also suspend an organization's live access. Then each of its live keys gets `403` `organization_suspended` with the retry action `contact_support` until Truvo restores it. Its sandbox keys keep working. A `401` `authentication_failed` still means the key itself is missing, revoked, expired, or not valid.

A `403` `live_access_not_approved` has the reason `pending_review`. A suspension uses `organization_suspended` and has no public reason. In both cases, follow the `contact_support` retry action.

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

## Versions

The path stays `/v1`, and the contract has revisions. Until the REST API opens to outside developers, Truvo serves one revision, with the pre-release ID `2026-09-26`. When it opens, the first public revision gets its own ID, and Truvo stops serving `2026-09-26`. The notice period and the 6-month promise start with the first public revision, so the pre-release ID is replaced with no alias.

- A revision ID is a date in the form `YYYY-MM-DD`. The ID format may change before the first public revision. Revisions are in the order that they ship.
- Send `Truvo-Version` on every request to an API operation, with the revision ID that your code is built for, exactly as the changelog lists it. The answer names the same revision, and its fields follow it. `/v1/mcp` and the discovery documents (`/v1/openapi.json`, `/v1/openapi/<revision>.json`, `/v1/revisions`, `/llms.txt` and `/.well-known/api-catalog`) don't need the header.
- `GET /v1/revisions` needs no key. It lists the `current` revision and every revision with its `status` (`current`, `supported` or `retired`), newest first. `/v1/openapi/<revision>.json` is the OpenAPI document of one served revision.
- A missing header, or one that names no revision that Truvo serves, gets `400` `invalid_version`. Its message says which revision to send, and `field_errors[0].allowed_values` lists each revision that Truvo serves, newest first.
- Truvo stores no revision for your integration. The revision lives in your code, so an upgrade or a rollback is a code change. Each request names its own revision, so you can move one request at a time.
- `/v1/mcp` always answers in the current revision and ignores the header. Code that needs a stable contract calls the REST API. After a breaking change to a tool's input, the tool keeps accepting the old input for the 6-month notice period, then answers with a field error that names the new field.
- Webhook events have no revision, and an event's fields are the same in every revision. Read the resource over REST with your own `Truvo-Version`. `GET /v1/events` is an API operation, so it sends the header too.
- A breaking change ships only in a new revision, and a revision keeps working for at least 6 months after notice. The changelog and an email announce each new revision and the end date of the revision before it, and before a revision retires, its answers carry `Deprecation` and `Sunset` headers. After the end date, a request that names the revision gets `400` `invalid_version`. A new status value is a breaking change.
- Any field in an answer can get new values in any revision, except each resource's `status` and `retry_action.type`. New fields can arrive too. Ignore a field you don't know, and give each switch on a value a default branch.
- A `404` uses the current revision. So does a refusal that Truvo sends before it knows your revision: `401`, `413`, a `429` for failed authentication, or `400` `invalid_version`. Their top-level error fields stay compatible from one revision to the next.
- "Revision" is a version of the API contract. "Version", as in `resource.version` or a quote's `version`, is a step in the history of one object.

More: [Versions and changes](/guides/versioning).

## 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 `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. The signature is an HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the part of your signing secret after `whsec_`, decoded from base64. Verify the raw body, before any JSON parsing. The signature follows the [Standard Webhooks](https://www.standardwebhooks.com) scheme, so any Standard Webhooks library can verify it. The `standardwebhooks` package on npm also 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. Each page has `events`, `has_more`, and `next_cursor`. Page through it with `cursor` and `limit` (10 by default, 100 at most), and store each `next_cursor` only after your work is saved. While `has_more` is `true`, send `next_cursor` as `cursor` to read the next page. When `has_more` is `false`, you've caught up; keep `next_cursor` and read again later. 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.

## 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_capabilities` | Shows what the key can quote for one `line`. Its `required_facts_schema` is the JSON Schema of the `application` for that line. |
| `start_quote_request` | Starts one quote request. Its input lists `application` as an object with no fields, so build the application from the `required_facts_schema` that `get_capabilities` returns. Truvo checks the application as it does over HTTP. It requires a `request_key`. |
| `get_quote_request` | Reads a quote request, with each quote inline. It waits up to `wait_seconds`, 20 by default, for the next change, counted from `since_version` when you send it. Read `status`; while it is `in_progress`, call again with the same `id`, `wait_seconds: 20`, and `since_version` set to the `version` you just read. If you read by handle, send the same `read_handle` too. |
| `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_quote` | Reads one quote. Leave out `version` to read the latest version. |
| `send_feedback` | Sends a feedback report, once the person agrees. The server always lists it. |
| `list_api_requests` | Lists the integration's recent requests, newest first, with each request's status, error code, and request ID. |
| `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 based on the caller's live-access and quoting status. A refused call comes back as a tool error that carries the same error envelope as HTTP. The server always answers in the current revision, and it ignores a `Truvo-Version` that the request sends.

More: [MCP tools](/api-reference/mcp-tools), with a page for each tool's inputs, result, and callers, and [Agent setup](/guides/agent-setup), with the config for Claude Code, Cursor, Codex, VS Code, OpenClaw, and Hermes Agent.

## Feedback

If the API is missing something, an answer is wrong, or an error or a page is unclear, tell Truvo.

An agent that acts for a person asks before it sends a report, shows the person the report, and sends it only if they agree. It offers after a quote request finishes, when the person seems frustrated, or when the API gives it trouble it can't fix, and at most once per conversation unless the person asks. The MCP server instructions say the same. For a report on how a quote went, use the `kind` `other`, and describe what happened in your own words rather than pasting the conversation.

`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 an `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.
- [Versions and changes](/guides/versioning): revisions, the `Truvo-Version` header, and what counts as a breaking change. The [changelog](/changelog) lists each revision.
- [API reference](/api-reference): every operation and its schemas.
- [MCP tools](/api-reference/mcp-tools): each MCP tool, with a page for its inputs, result, and callers.
- `/llms.txt`: an index of every docs page. Add `.md` to any page URL to get its Markdown.

## Common questions

- [How long until quotes finish?](/guides/how-long-until-quotes-finish): auto times measured on truvo.com, and the current 60-second deadline.
- [Why did a request finish with no quotes?](/guides/why-did-a-request-finish-with-no-quotes): each carrier outcome, and what to do about it.
- [Why are there fewer quotes than carriers asked?](/guides/why-are-there-fewer-quotes-than-carriers): partial results and the deadline.
- [When does a quote expire, and what happens then?](/guides/when-does-a-quote-expire): `expires_at`, the `quote.expired` event, and what to show.
- Request a quote for [auto](/guides/request-a-quote/auto), [renters](/guides/request-a-quote/renters), or [pet](/guides/request-a-quote/pet): the disclosure step, a minimal request, each request field, and the `needs_information` error.
- [Which states and carriers are covered?](/guides/which-states-and-carriers-are-covered): states in capabilities, and carriers in each quote request.