# Test with test keys

Test keys let you build and test your whole integration for free: requests, refusals, retries, limits and result handling. This page explains exactly what test mode simulates, what it does not, and how to write tests you can rely on.

## What a test key is

A test key starts with `au_test_` and is created under **API keys** with **Mode: Test · simulated, free**. It uses the same endpoints, scopes, validation, catalogue and error contract as a live key. The difference is the valuation itself:

| | Test key | Live key |
| --- | --- | --- |
| Price | Simulated by a deterministic formula | Real market valuation |
| Credits | Never | One per `valued` result |
| `precision.grade` | Always `indicative` | `high`, `medium`, `low` or `indicative` |
| `model.model_id` / `model.synthetic` | `sandbox-v1` / `true` | The production model / `false` |
| `valuation.intervals[].method` | `synthetic` | The model's method |
| AI interpretation of text and photos | Never runs | Runs when enabled and needed |
| Notes | Includes `synthetic_result` | No `synthetic_result` |
| Concurrency | Its own limit of 4 per workspace | 4 per workspace, plus shared market capacity |
| Fair-use allowance | Valuations, selling-time forecasts, normalization and VIN, HSN/TSN and plate lookups share 10 per minute, 100 per hour and 300 per day per workspace | None beyond the rate limits and credits |
| Idempotency keys and read-back | Separate from live | Separate from test |
| Request logs | Recorded with mode `test` | Recorded with mode `live` |

Catalogue and account calls behave identically for both modes and are free either way.

## Test keys are for testing

Test keys exist to build and test an integration. Normalization and the vehicle identification in a test valuation use the real catalogue, so a test key could look like a free way to identify real vehicles. It is not allowed: the [service terms](/legal/terms#test-keys) forbid using test keys to check, identify or value real vehicles for your business or your users, and AutoUncle monitors test-key usage per workspace and key.

To keep that fair, test-key requests that identify a vehicle share a **fair-use allowance per workspace**: 10 per minute, 100 per hour and 300 per day. Catalogue and account calls do not count. When a window is used up, the request is refused with `429` [`test_limit_reached`](/docs/errors#test_limit_reached) and `retry_after`, before anything is recorded; a refused request does not count against the other windows. Keep test suites small, stub the API in unit tests, and [contact us](/contact-sales) if your CI needs a higher allowance.

## What the simulated price depends on

The test price is a deterministic function of the **registration year, mileage, power and market**. It does not depend on make, model or equipment, so a test Golf and a test Porsche with the same year, mileage and power get the same price. The intervals are fixed proportions of the price (±20% for 80%, ±35% for 95%, ±50% for 99%).

This is deliberate: test results let you check your parsing, display and error handling, never market logic. **Never show a test price to users as a market value.** Detect test results with `mode: "test"`, `model.synthetic: true` or the `synthetic_result` note.

## What you can test

| Behaviour | How to trigger it with a test key |
| --- | --- |
| A valued result | A structured request with year and mileage |
| Ambiguity warnings and hints | Send make and model without catalogue IDs: `generation_ambiguous` and `drivetrain_unresolved` |
| A pinned vehicle | Send a catalogue `drivetrain_id` |
| `missing_mileage` refusal | Leave out `vehicle.mileage` |
| Mileage baseline | Leave out mileage and set `options.allow_mileage_estimation: true` |
| Assumed equipment | Set `options.allow_equipment_estimation: true`. Test keys assume only a catalogue trim's equipment, never similar vehicles' (`equipment_not_assumed` otherwise) |
| Assumed year | Leave out the year (best-effort resolution): `year_imputed` |
| `precision_below_minimum` refusal | Set `options.min_precision` to `low` or higher |
| Strict refusals | `options.resolution: "strict"` without a year or mileage |
| `catalogue_id_conflict` | Send a `generation_id` and a `drivetrain_id` from a different generation |
| `unknown_catalogue_id` | Send a catalogue ID that does not exist |
| `unsupported_vehicle` | Set `vehicle.flags.is_damaged: true` |
| `invalid_request` with pointers | Send an unknown field, or a string where a number is expected |
| Idempotent replay | Send the same body twice with the same key |
| `idempotency_conflict` | Send a different body with a used key |
| `images_not_used` | Add `images`; test mode never reads them |
| `country_not_enabled` | Value in a market your workspace or key does not allow |
| Rate limiting | Exceed 60 valuation requests in a minute (this also slows your other test traffic) |

## What you cannot test

- **Real prices, grades and intervals.** Only a live key can show how precise results are for your vehicles.
- **Free-text and photo interpretation.** Test mode only runs deterministic text rules, so a description on its own is usually refused (`vehicle_not_identified` or `missing_mileage`). Use structured attributes in tests, and judge text interpretation with a live key.
- **Dependency failures** such as `engine_timeout` or `resolution_unavailable`. Simulate them in your own code with a stub that returns the documented error bodies from the [error reference](/docs/errors).
- **Credit exhaustion.** Test keys never spend credits, so `insufficient_credits` cannot happen. Stub it.
- **`engine_busy`.** Test valuations never use live capacity.

## Writing reliable tests

- **Assert on structure and codes, not on prices.** Check `status`, `error.code`, `precision.grade`, the presence of `valuation.intervals` and your handling of `warnings`. Test prices change from one year to the next because they depend on the vehicle's age.
- **Use unique idempotency keys per test run**, for example including a run ID. A key reused across runs replays the first run's result, which is useful for replay tests and confusing everywhere else.
- **Keep CI under the limits.** Run valuation tests with at most four in parallel and well under 60 per minute, or you will see `concurrency_limited` and `rate_limited` (both free and safe to retry after `retry_after`).
- **Stub the API in unit tests** with recorded responses, and keep a small set of contract tests that call the real API with a test key.
- **Keep test keys out of production** and live keys out of CI. Name keys by environment so the console's usage and logs stay readable.

## A minimal contract test

```python
import os
import uuid

import requests

API = "https://data-services.autouncle.com"
HEADERS = {"Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}"}


def value(body, key):
    return requests.post(f"{API}/v1/valuations", json=body, headers={**HEADERS, "Idempotency-Key": key}, timeout=30)


def test_valuation_contract():
    run = uuid.uuid4().hex
    body = {"country": "DK", "vehicle": {"make": "VW", "model": "Golf", "model_year": 2020,
                                         "mileage": {"value": 60000, "unit": "km"}}}

    first = value(body, f"ci-{run}-golf")
    assert first.status_code == 200
    result = first.json()
    assert result["status"] == "valued"
    assert result["mode"] == "test"
    assert result["precision"]["grade"] == "indicative"
    assert any(note["code"] == "synthetic_result" for note in result["notes"])

    replay = value(body, f"ci-{run}-golf").json()
    assert replay["request_id"] == result["request_id"]
    assert replay["usage"]["idempotent_replay"] is True

    refused = value({"country": "DK", "vehicle": {"make": "VW", "model": "Golf", "model_year": 2020}}, f"ci-{run}-no-km")
    assert refused.status_code == 422
    assert refused.json()["error"]["code"] == "missing_mileage"
```

Run it with `pytest` and a test key in `AUTOUNCLE_API_KEY`.

## The playground

The console playground preselects a test key, so you can try any request by hand before writing code. Live keys are labelled **consumes credits on success** in the key picker.

## Next steps

- [Refusals & missing mileage](/docs/guides/refusals-and-mileage).
- [Production checklist](/docs/production-checklist) before switching to a live key.
- [Credits & billing](/docs/credits).
