# Authentication (/docs/authentication)



Every `/api/v1` request authenticates with an API key passed as a bearer token:

```bash
Authorization: Bearer vlr_live_...
```

Keys look like `vlr_live_` followed by a random secret. Valara stores only a
hash of the key plus a short display prefix, so the full secret is shown to you
exactly once when the key is created. Treat it like a password.

## Creating a key [#creating-a-key]

Once you have an account, you provision keys yourself:

1. Open [**Settings → API Keys**](/settings/api-keys) in the dashboard.
2. Name the key for where it runs (e.g. "CI pipeline") and choose its scopes.
3. Copy the full secret. It is shown **once**; store it before you leave the
   page.

Key management is session-authenticated (the dashboard, or the CLI's device
session). An API key **cannot** create or revoke keys, so a leaked key can
never mint new ones or escalate its own scopes. The same actions are available
programmatically with a session credential at `POST` / `GET /api/v1/keys` and
`DELETE /api/v1/keys/{id}`.

## Scopes [#scopes]

Each key carries a set of scopes. A request that needs a scope the key lacks is
rejected with `403 forbidden` before any work happens (so a read-only key can
never start a billable review).

| Scope            | Grants                                               |
| ---------------- | ---------------------------------------------------- |
| `reviews:read`   | List reviews, read review status, and fetch results. |
| `reviews:write`  | Submit new reviews (`POST /api/v1/reviews`).         |
| `reviews:delete` | Delete a review.                                     |

## How a request is resolved [#how-a-request-is-resolved]

1. A `Authorization: Bearer vlr_live_…` header is verified against your key.
2. The key resolves to the owning Valara user; reviews, credits, and history are
   scoped to that user.
3. The required scope for the endpoint is enforced.

Browser sessions (the Valara web app) authenticate with cookies and carry full
access once the account is approved; accounts still pending admin approval are
rejected with `403 forbidden`. API keys are the path for machines, agents, and
CI.

## Failure modes [#failure-modes]

| Status | Type           | Meaning                                                                                                                                                 |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `unauthorized` | Missing, malformed, revoked, or expired key.                                                                                                            |
| `403`  | `forbidden`    | Valid key, but it lacks the scope the endpoint requires. Session and bearer credentials also receive `403` while the account is pending admin approval. |

Both use the standard [error envelope](/docs/errors). Credits remain the hard
financial ceiling: a review that would exceed your balance returns `402`
`insufficient_credits` rather than running.


# Errors (/docs/errors)



Every `/api/v1` error returns the same JSON envelope, so a client can handle all
failures with one code path:

```json
{
  "error": {
    "type": "insufficient_credits",
    "message": "Your credit balance is too low to start this review.",
    "param": null,
    "request_id": "req_8f3a…"
  }
}
```

| Field        | Description                                                                                |
| ------------ | ------------------------------------------------------------------------------------------ |
| `type`       | A stable, machine-readable error category (see below). Branch on this, not on the message. |
| `message`    | A human-readable explanation. May change; not for programmatic use.                        |
| `param`      | The offending input field, when the error is about one.                                    |
| `request_id` | Correlates the response with Valara server logs. Include it when you contact support.      |

## Error types [#error-types]

| Type                   | HTTP  | Meaning                                                                                        |
| ---------------------- | ----- | ---------------------------------------------------------------------------------------------- |
| `invalid_request`      | `400` | The request was malformed or failed validation. Check `param`.                                 |
| `unauthorized`         | `401` | Missing or invalid credentials. See [Authentication](/docs/authentication).                    |
| `forbidden`            | `403` | The key is valid but lacks the required scope, or the account is still pending admin approval. |
| `insufficient_credits` | `402` | Your credit balance is too low to start the review.                                            |
| `not_found`            | `404` | No such review, or the result is not ready yet.                                                |
| `rate_limited`         | `429` | Reserved. Rate limiting is not enforced yet, so no endpoint returns this today.                |
| `server_error`         | `500` | An unexpected error on Valara's side. Safe to retry with backoff.                              |

## Handling guidance [#handling-guidance]

* **Branch on `type`**, never on `message` or the raw status code alone.
* **`402 insufficient_credits`** is the financial ceiling. Credits are checked
  before any work runs, so you are never charged for a rejected submit.
* **`404` on a result** usually means the review is still `processing`. Poll
  `GET /api/v1/reviews/{id}` until it is `completed`, then fetch the result.
* **`429 rate_limited`** is reserved for when rate limiting is enabled; it is
  not enforced today. Handle it defensively anyway (back off and retry, honoring
  `Retry-After`) so your client is ready when it ships.
* **Always log `request_id`.** It is the fastest way for support to find your
  request.


# Quickstart (/docs)



Valara turns an appraisal PDF into a scored, citation-grounded review: USPAP and
GSE compliance checks, quality scoring, and page-level citations for every
finding. The API is built for machines and AI agents, so the contract is
asynchronous: you **submit** a review, **track** its progress (stream or poll),
then **fetch** the result.

This page walks the whole flow with `curl`. It uses one API key.

<Callout type="info">
  Once you [have an account](/signup), you create your own key in
  [**Settings → API Keys**](/settings/api-keys) (the secret is shown once). See
  [Authentication](/docs/authentication) for keys and scopes.
</Callout>

## 1. Submit an appraisal [#1-submit-an-appraisal]

`POST /api/v1/reviews` starts an asynchronous review. The simplest transport is
a raw `application/pdf` body with parameters in the query string. This path also
avoids multipart size limits for large reports.

```bash
curl -X POST "https://getvalara.com/api/v1/reviews?review_type=residential&filename=123-main-st.pdf" \
  -H "Authorization: Bearer vlr_live_..." \
  -H "Content-Type: application/pdf" \
  --data-binary @123-main-st.pdf
```

You get back a `202 Accepted` with a review resource whose `status` is
`processing`:

```json
{
  "id": "9f2c…",
  "review_type": "residential",
  "status": "processing",
  "created_at": "2026-06-29T17:00:00.000Z"
}
```

The `id` is the appraisal's content hash, so re-submitting the same PDF is
idempotent. If that appraisal was already reviewed, you get `200 OK` with a
`completed` review instead of `202`. (See [Review
lifecycle](/docs/review-lifecycle).)

<Callout type="info">
  Prefer to upload out-of-band? Send a JSON body instead:
  `{"blob_url": "https://…", "filename": "123-main-st.pdf", "review_type": "residential"}`.
</Callout>

## 2. Track until it completes [#2-track-until-it-completes]

Reviews run for minutes. The preferred way to track one is the event stream:
`GET /api/v1/reviews/{id}/events` is a Server-Sent Events stream of progress
events that stays open while the review is `processing` and closes the moment it
reaches a terminal state, so you react on completion instead of waiting for a
poll.

```bash
curl -N "https://getvalara.com/api/v1/reviews/9f2c…/events" \
  -H "Authorization: Bearer vlr_live_..."
```

The stream exists only while the review is `processing`; once it is `completed`
or `failed`, the endpoint returns `404` and you fetch the result directly. See
[Review lifecycle](/docs/review-lifecycle) for the event shape and `start_index`
resume.

<Callout type="info">
  No long-lived connection? Poll instead: `GET /api/v1/reviews/{id}` every few
  seconds until `status` is `completed` or `failed`. It reads the same durable
  run, so a poll never restarts the work.
</Callout>

## 3. Fetch the result [#3-fetch-the-result]

Once `completed`, `GET /api/v1/reviews/{id}/result` returns the scored,
citation-grounded result. Its shape is a discriminated union on `review_type`
(`residential` or `commercial`).

```bash
curl "https://getvalara.com/api/v1/reviews/9f2c…/result" \
  -H "Authorization: Bearer vlr_live_..."
```

The result carries the overall score, findings (each with page-level
citations), and a narrative summary. See the
[result schema](/docs/api/getReviewResult) for the full shape.

## Next steps [#next-steps]

* [Authentication](/docs/authentication) — keys, scopes, and the `Authorization` header.
* [Review lifecycle](/docs/review-lifecycle) — statuses, idempotency, and the stream/poll model.
* [Errors](/docs/errors) — the error envelope and how to handle each type.
* [API reference](/docs/api) — every endpoint, generated from the live contract.


# MCP server (/docs/mcp)



Valara hosts a remote [Model Context Protocol](https://modelcontextprotocol.io)
server so AI agents (Claude, Cursor, VS Code, Codex, and any MCP client) can run
and read appraisal reviews as tools, without you writing a line of integration
code. It is the same review engine behind the [REST API](/docs/api), exposed as
agent tools.

```text
https://getvalara.com/api/mcp
```

The transport is MCP Streamable HTTP. Authentication uses the same first-party
API keys as the REST API, passed as a bearer token, so a single `vlr_live_…` key
works for both. See [Authentication](/docs/authentication) to create one.

## Connect a client [#connect-a-client]

The server is hosted, so there is nothing to install: each client just needs the
URL and your API key. Create a key under
[**Settings → API Keys**](/settings/api-keys) with the scopes you want the agent
to have (`reviews:read` for read-only triage, add `reviews:write` to let it
submit), then drop it into the client's config.

### Claude Desktop / Cursor / generic clients [#claude-desktop--cursor--generic-clients]

Most clients accept a `mcpServers` block. Use the `mcp-remote` bridge, which
forwards your key as an `Authorization` header:

```json title="claude_desktop_config.json"
{
  "mcpServers": {
    "valara": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://getvalara.com/api/mcp",
        "--header",
        "Authorization: Bearer vlr_live_your_key_here"
      ]
    }
  }
}
```

Cursor (`~/.cursor/mcp.json`) can also connect to the URL directly with headers:

```json title="~/.cursor/mcp.json"
{
  "mcpServers": {
    "valara": {
      "url": "https://getvalara.com/api/mcp",
      "headers": { "Authorization": "Bearer vlr_live_your_key_here" }
    }
  }
}
```

### Claude Code (CLI) [#claude-code-cli]

```bash
claude mcp add --transport http valara https://getvalara.com/api/mcp \
  --header "Authorization: Bearer vlr_live_your_key_here"
```

Replace `vlr_live_your_key_here` with the secret shown once when you created the
key. Restart the client and the Valara tools appear.

## Tools [#tools]

| Tool                | Scope            | What it does                                                             |
| ------------------- | ---------------- | ------------------------------------------------------------------------ |
| `submit_review`     | `reviews:write`  | Start a review from a PDF URL. Returns a review id immediately.          |
| `get_review`        | `reviews:read`   | Check a review's status (`processing` → `completed`).                    |
| `get_review_result` | `reviews:read`   | Fetch the full scored result once completed.                             |
| `get_findings`      | `reviews:read`   | Flat, severity-tagged list of issues with citations: the fastest triage. |
| `list_reviews`      | `reviews:read`   | List your reviews, newest first (cursor paginated).                      |
| `delete_review`     | `reviews:delete` | Delete your ownership of a review.                                       |

Each tool scopes to the API key's owner and enforces the scopes above, exactly
like the REST API. A tool call that needs a scope the key lacks returns an error
the agent can read, rather than doing any work.

## The async contract [#the-async-contract]

Reviews take minutes, so `submit_review` does **not** block. The typical agent
flow is:

1. `submit_review` with a `blob_url` and `review_type` → returns a review id.
2. Poll `get_review` until `status` is `completed`.
3. `get_findings` (for triage) or `get_review_result` (for the full DTO).

Re-submitting the same PDF is a no-op that returns the existing review (reviews
are content-addressed by the PDF's hash), so an agent can safely retry. Each new
review deducts credits from the key owner's balance.

## Notes [#notes]

* **Key management is not exposed over MCP.** Creating or revoking keys requires
  a dashboard or CLI session, never an API key, so a leaked key can never mint
  new ones. Manage keys at [Settings → API Keys](/settings/api-keys).
* **Review tool calls before approving them.** As with any MCP server, prefer a
  client that confirms tool calls, and be cautious when combining Valara with
  other servers, to avoid prompt-injection from untrusted content.
* **Prefer the stream.** Agents that want live progress can also use the REST
  [event stream](/docs/api/streamReviewEvents); MCP tools follow the poll model
  above.


# Review lifecycle (/docs/review-lifecycle)



A review takes minutes, not milliseconds, so the API never blocks on one. You
**submit** a review, **track** its progress (stream or poll), then **fetch** the
result. This is the contract agents and integrations should build against.

## Statuses [#statuses]

`GET /api/v1/reviews/{id}` reports one of four statuses:

| Status       | Meaning                                           |
| ------------ | ------------------------------------------------- |
| `processing` | The review is running. Keep polling.              |
| `completed`  | Done. Fetch the result at `/reviews/{id}/result`. |
| `failed`     | The review could not be completed.                |
| `cancelled`  | The review was superseded or cancelled.           |

## Tracking progress [#tracking-progress]

You can either **stream** progress or **poll** for it. Both read the same
durable run, so neither restarts the work and a dropped connection never loses
progress.

### Stream (preferred) [#stream-preferred]

`GET /api/v1/reviews/{id}/events` is a Server-Sent Events stream of progress
events. It stays open while the review is `processing` and closes when the
review reaches a terminal state, so a client reacts the moment the review
finishes instead of waiting for the next poll. After a dropped connection,
reconnect with `?start_index=N` to resume from an event offset.

The stream exists only for a `processing` review. Once the review is
`completed`, `failed`, or `cancelled`, the endpoint returns `404`; fetch the
result instead.

### Poll (fallback) [#poll-fallback]

When a long-lived connection is inconvenient (serverless callers, simple
scripts), poll `GET /api/v1/reviews/{id}` every few seconds until the status is
`completed` or `failed`.

## Idempotency by content hash [#idempotency-by-content-hash]

A review's `id` is the SHA-256 content hash of the appraisal PDF. Submitting the
same bytes twice refers to the same review, so retries are safe by construction
and you never pay twice for the same appraisal.

This is why submit has two success codes:

| Code           | When                                                                                 |
| -------------- | ------------------------------------------------------------------------------------ |
| `202 Accepted` | A new review started. `status` is `processing`.                                      |
| `200 OK`       | That appraisal was already reviewed. The `completed` review is returned immediately. |

A client can treat both the same way: read `status`, and if it is not yet
`completed`, poll.

## Submitting [#submitting]

`POST /api/v1/reviews` accepts two transports:

* **Raw PDF** — `Content-Type: application/pdf` with the file as the body and
  `review_type` / `filename` in the query string. Best for large reports.
* **JSON** — `{ "blob_url", "filename", "review_type" }` referencing an
  already-uploaded file.

`review_type` accepts `residential`, `commercial`, or `auto`. With `auto`, the
server classifies the PDF before any credits are charged, and the submit
response reports the resolved type: `review_type` is always `residential` or
`commercial`, and `review_type_source` says how it was chosen (`user`,
`detected`, or `default`). Commercial reviews cost more credits, so prefer an
explicit type when you already know it. Defaults to `residential` when omitted.

## Deleting [#deleting]

`DELETE /api/v1/reviews/{id}` removes a review and returns `204 No Content`.
Requires the `reviews:delete` scope.

## Fetching results [#fetching-results]

Once `completed`, `GET /api/v1/reviews/{id}/result` returns the scored result.
Calling it before completion returns `404 not_found` — poll status first. The
result shape is a discriminated union on `review_type`; see the
[result schema](/docs/api/getReviewResult).


# Create an API key (/docs/api/createApiKey)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Submit an appraisal for review (/docs/api/createReview)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Delete a review (/docs/api/deleteReview)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Retrieve a review (/docs/api/getReview)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Retrieve a review result (/docs/api/getReviewResult)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# API Reference (/docs/api)



The Valara REST API. Every page below is generated from the live OpenAPI
contract (`public/openapi/v1.json`), which is itself derived from the request
and response schemas the `/api/v1` routes serve, so the reference cannot drift
from the real API.

New here? Start with the [Quickstart](/docs), then
[Authentication](/docs/authentication) for keys and scopes.

## Reviews [#reviews]

Submit an appraisal, track it, and retrieve the scored result.

* [Submit a review](/docs/api/createReview) — `POST /reviews`
* [List reviews](/docs/api/listReviews) — `GET /reviews`
* [Retrieve a review](/docs/api/getReview) — `GET /reviews/{id}`
* [Retrieve a review result](/docs/api/getReviewResult) — `GET /reviews/{id}/result`
* [Stream review progress](/docs/api/streamReviewEvents) — `GET /reviews/{id}/events`
* [Delete a review](/docs/api/deleteReview) — `DELETE /reviews/{id}`

## Keys [#keys]

Create and manage API keys. These are session-authenticated (dashboard or CLI);
an API key cannot manage keys.

* [Create an API key](/docs/api/createApiKey) — `POST /keys`
* [List API keys](/docs/api/listApiKeys) — `GET /keys`
* [Revoke an API key](/docs/api/revokeApiKey) — `DELETE /keys/{id}`


# List API keys (/docs/api/listApiKeys)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# List reviews (/docs/api/listReviews)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Revoke an API key (/docs/api/revokeApiKey)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Stream review progress (SSE) (/docs/api/streamReviewEvents)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

