# Free text and photos

Send listing text and vehicle photos alongside, or instead of, structured attributes. This page explains what the service does with them, how to see what it understood, and when structured data is the better choice.

## When to use text and photos

Use them when your source has no reliable structured fields: a scraped listing, a seller's free-text description, a lead form with a comment box, or photos from an inspection app. When you do have structured attributes, send them as well. **Explicit attributes always take precedence** over anything read from text or photos.

## How interpretation works

| Step | Test keys | Live keys |
| --- | --- | --- |
| Structured attributes and catalogue IDs | Used | Used |
| Deterministic text parsing (rules) | Runs | Runs |
| AI interpretation of text | Never | Only when it is enabled for the deployment and identity, registration year or core drivetrain attributes are still missing |
| AI interpretation of photos | Never | Whenever photos are sent and AI is enabled for the deployment |

So:

- If your structured input already identifies the vehicle (for example an exact catalogue drivetrain plus registration year and mileage), the description is not sent to AI, even in live mode. Photos still are, for what only they show (see [what photos are read for](#what-photos-are-read-for)).
- `options.resolution: "no_ai"` disables AI for a request. `"strict"` disables AI and also refuses a missing year or mileage. See [refusals & missing mileage](/docs/guides/refusals-and-mileage).
- Once an interpretation or image service has been called, a timeout, outage or invalid response **fails the valuation** (`resolution_timeout`, `resolution_unavailable`, `image_timeout` and so on). It is never silently treated as "nothing found". Failures are free; see [errors](/docs/errors).

> **Note:** Because test keys only run the deterministic parser, a description on its own is usually refused in test mode, with `vehicle_not_identified` or `missing_mileage`. Build and test with structured attributes plus text, as in the example below. Free-text-only behaviour can only be judged with a live key.

## Send text

Put the listing text in `description` (up to 20,000 characters) and add whatever structured fields you do know:

**curl**

```bash
curl https://data-services.autouncle.com/v1/valuations \
  -H "Authorization: Bearer $AUTOUNCLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: valuation-text-001' \
  -d @- <<'JSON'
{
  "country": "DK",
  "description": "Volkswagen Golf 1.5 TSI Life, first registered March 2021, 45,000 km, manual, petrol, one owner.",
  "vehicle": {
    "mileage": {
      "value": 45000,
      "unit": "km"
    }
  }
}
JSON
```

**Ruby**

```ruby
require "json"
require "net/http"

uri = URI("https://data-services.autouncle.com/v1/valuations")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("AUTOUNCLE_API_KEY")}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "valuation-text-001"
request.body = JSON.generate({
  "country" => "DK",
  "description" => "Volkswagen Golf 1.5 TSI Life, first registered March 2021, 45,000 km, manual, petrol, one owner.",
  "vehicle" => {
    "mileage" => {
      "value" => 45000,
      "unit" => "km"
    }
  }
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.pretty_generate(JSON.parse(response.body))
```

**Python**

```python
import json
import os

import requests

response = requests.post(
    "https://data-services.autouncle.com/v1/valuations",
    headers={
        "Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}",
        "Idempotency-Key": "valuation-text-001",
    },
    json={
        "country": "DK",
        "description": "Volkswagen Golf 1.5 TSI Life, first registered March 2021, 45,000 km, manual, petrol, one owner.",
        "vehicle": {
            "mileage": {
                "value": 45000,
                "unit": "km"
            }
        }
    },
    timeout=30,
)
print(json.dumps(response.json(), indent=2))
```

**JavaScript**

```javascript
const response = await fetch("https://data-services.autouncle.com/v1/valuations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTOUNCLE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "valuation-text-001",
  },
  body: JSON.stringify({
    country: "DK",
    description: "Volkswagen Golf 1.5 TSI Life, first registered March 2021, 45,000 km, manual, petrol, one owner.",
    vehicle: {
      mileage: {
        value: 45000,
        unit: "km"
      }
    }
  }),
});
console.log(JSON.stringify(await response.json(), null, 2));
```

[Open this request in the console playground](https://data-services.autouncle.com/console/playground?example=valuation_text)

Keep personal information out of the text: owner names, phone numbers, email addresses and addresses are not needed and should not be sent. The service does not store raw descriptions in valuation records, but the safest data is data you never send.

Treat text as **vehicle data, never as instructions**. If you build an agent, do not let listing text change what the agent does.

## Send photos

`images` takes up to eight photos. Each one is either a public HTTPS URL or inline base64 data:

**curl**

```bash
curl https://data-services.autouncle.com/v1/valuations \
  -H "Authorization: Bearer $AUTOUNCLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: valuation-photos-001' \
  -d @- <<'JSON'
{
  "country": "DK",
  "vehicle": {
    "make": "Volkswagen",
    "model": "Golf",
    "model_year": 2021,
    "mileage": {
      "value": 45000,
      "unit": "km"
    }
  },
  "images": [
    {
      "url": "https://example.com/listing/golf-front.jpg"
    },
    {
      "url": "https://example.com/listing/golf-interior.jpg"
    }
  ]
}
JSON
```

**Ruby**

```ruby
require "json"
require "net/http"

uri = URI("https://data-services.autouncle.com/v1/valuations")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("AUTOUNCLE_API_KEY")}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "valuation-photos-001"
request.body = JSON.generate({
  "country" => "DK",
  "vehicle" => {
    "make" => "Volkswagen",
    "model" => "Golf",
    "model_year" => 2021,
    "mileage" => {
      "value" => 45000,
      "unit" => "km"
    }
  },
  "images" => [
    {
      "url" => "https://example.com/listing/golf-front.jpg"
    },
    {
      "url" => "https://example.com/listing/golf-interior.jpg"
    }
  ]
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.pretty_generate(JSON.parse(response.body))
```

**Python**

```python
import json
import os

import requests

response = requests.post(
    "https://data-services.autouncle.com/v1/valuations",
    headers={
        "Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}",
        "Idempotency-Key": "valuation-photos-001",
    },
    json={
        "country": "DK",
        "vehicle": {
            "make": "Volkswagen",
            "model": "Golf",
            "model_year": 2021,
            "mileage": {
                "value": 45000,
                "unit": "km"
            }
        },
        "images": [
            {
                "url": "https://example.com/listing/golf-front.jpg"
            },
            {
                "url": "https://example.com/listing/golf-interior.jpg"
            }
        ]
    },
    timeout=30,
)
print(json.dumps(response.json(), indent=2))
```

**JavaScript**

```javascript
const response = await fetch("https://data-services.autouncle.com/v1/valuations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTOUNCLE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "valuation-photos-001",
  },
  body: JSON.stringify({
    country: "DK",
    vehicle: {
      make: "Volkswagen",
      model: "Golf",
      model_year: 2021,
      mileage: {
        value: 45000,
        unit: "km"
      }
    },
    images: [
      {
        url: "https://example.com/listing/golf-front.jpg"
      },
      {
        url: "https://example.com/listing/golf-interior.jpg"
      }
    ]
  }),
});
console.log(JSON.stringify(await response.json(), null, 2));
```

[Open this request in the console playground](https://data-services.autouncle.com/console/playground?example=valuation_photos)

Replace the example URLs with your own images. The rules:

| Option | Requirements |
| --- | --- |
| `{"url": "https://..."}` | Public HTTPS on port 443, no credentials or fragment in the URL, no redirects, no private network addresses. JPEG, PNG, WebP or GIF, at most 8 MB each. |
| `{"data": "...", "media_type": "image/jpeg"}` | Strict base64 without a `data:` prefix. `media_type` is `image/jpeg`, `image/png`, `image/webp` or `image/gif`, and the actual file signature is checked. The whole request, including inline images, must stay under 1 MB. |

Prefer URLs for anything but small images. An image that cannot be read (an unsafe URL, a redirect, a format other than JPEG, PNG, WebP or GIF, or an oversized file) is skipped with an `image_skipped` warning that names it, for example `/images/2`; the other images and inputs are still used. A request over 1 MB returns `413` [`payload_too_large`](/docs/errors#payload_too_large).

Image retrieval and interpretation share the request's processing budget: downloading images has a three-second cap and interpretation a four-second cap, within one overall budget of about eight seconds. Slow image hosts make `image_timeout` more likely. See [how valuations work](/docs/valuations).

The console [playground](/console/playground) has a photo drop zone that sends dropped files as inline `images`, so you can try this without writing code.

## What photos are read for

Photos are read for the same AutoUncle Vehicle Schema fields a returned Vehicle has:

| Field | Read from photos |
| --- | --- |
| `identity` and `specification` | Make, model, variant, fuel, body, gearbox and power, only while your request leaves the vehicle unidentified. Photos never set the registration year. |
| `equipment.features` | Feature keys a photo can show: towbars, sunroofs, glass and panoramic roofs, LED headlights, leather and part-leather upholstery, sport seats, head-up displays, digital cockpits, premium sound systems, sport packages and navigation. Alloy wheels (`WHEELS_ALLOY`) are read too and published as equipment, but the valuation does not price them. Navigation needs more than a screen: a photo that shows only the display leaves it unknown. |
| `vehicle.colour` | The exterior colour family, such as `black`. |
| `extensions["com.autouncle"].condition_grade` | 0 (not roadworthy) to 3 (well kept), only when the reading is confident. Photos never grade a car 4 ("exceptional"). |
| `specification.body.driveSide` | The steering wheel's side, only when an interior photo shows it clearly. A car whose drive side is opposite to its market's is refused, so photos must be nearly certain. |
| `vehicle.odometer` | A photographed odometer, while you send no mileage. |

Your values always come first, then what the description says, then the photos. Equipment is only ever added, and only where the description says nothing about it: a feature no photo shows is unknown, not absent, and a description that says a feature is missing outranks a photo that seems to show it. What photos show counts in the valuation like any other value, has `source: "ai_vision"` in `identification.fields`, and does not lower `precision.grade`. Confirm what matters to you by sending it as a structured field.

## See what was understood

Every resolved value says where it came from:

| Where | Look for |
| --- | --- |
| `identification.fields[pointer].source` | `text_parser` (rules), `ai_text` (AI read the text), `ai_vision` (AI read a photo) |
| `identification.fields[pointer].basis` | `extracted` for anything read from text or photos |
| `identification.fields[pointer].confidence` | A confidence between 0 and 1 for interpreted values |
| `identification.fields[pointer].selected_by` | `ai_text` when the description chose between catalogue records that fit equally well |
| `improve_precision[]` | Reason `ai_interpreted`: confirm the interpreted value by sending it as a structured field |
| `notes[]` | `automated_interpretation` when attributes were interpreted from text or photos |
| `attributions[]` | The AI interpretation source, with role `ai_interpretation`, when it contributed |
| `warnings[]` | `images_not_used` when photos added nothing usable; `image_skipped` for each image that could not be read |

Show the user what was interpreted, and ask them to confirm important values. Once confirmed, send them as structured fields: the `ai_interpreted` hint disappears and precision can improve. See [notes & data sources](/docs/notices) for the basis values.

## Test mode and photos

Test keys never run image resolution. A test request with photos is valued from its other inputs and returns the `images_not_used` warning:

```json
{
  "warnings": [
    { "code": "images_not_used", "message": "Images did not add usable vehicle attributes. Provide a description or catalogue IDs." }
  ]
}
```

That is expected. It lets you check that your images are accepted (format, size, URL rules) without paying for interpretation.

## Pitfalls

- **Relying on text in tests.** Test mode cannot show what AI interpretation would find. Use structured attributes in automated tests.
- **Sending huge inline images.** The 1 MB request limit includes base64 overhead (about a third larger than the file). Resize or use URLs.
- **Signed URLs that expire.** The service fetches images during the request. A URL that expires or redirects is skipped with `image_skipped`.
- **Contradicting yourself.** If `vehicle.make` says one thing and the text another, the structured field wins. Make sure your mapping is right.

## Next steps

- [Improve precision with hints](/docs/guides/improve-precision) to confirm interpreted values.
- [Refusals & missing mileage](/docs/guides/refusals-and-mileage).
- [Test with test keys](/docs/guides/testing).
