# Vista Ingest API — v1

Base URL: `https://ingest.vista-eas.com/ingest/v1`
All responses are JSON. Errors use `application/problem+json` (RFC 9457) with a machine-readable `code`;
the list of codes is at `GET /errors`. The server never explains *why* a check failed beyond the code.

You have been issued a `client_id` and a `client_secret`. The secret is never sent over the wire.

## 1. Mandatory headers

Every request under `/auth`, `/uploads`, `/jobs`, `/images` MUST carry:

| Header          | Value                                                                    |
|-----------------|--------------------------------------------------------------------------|
| `X-Client-Id`   | your client id                                                           |
| `X-Request-Id`  | a fresh UUID v4 per request. Reusing one within 15 minutes → `409`       |
| `Accept`        | `application/vnd.vista.v1+json`                                          |

Requests under `/uploads`, `/jobs`, `/images` MUST additionally carry:

| Header              | Value                                                                       |
|---------------------|-----------------------------------------------------------------------------|
| `Authorization`     | `Bearer <session_token>` (see §2)                                           |
| `X-Vista-Signature` | `t=<unix_seconds>,v1=<hex>` — request signature (see §3)                    |

Rate limit: 30 requests per rolling minute per client → `429` + `Retry-After`.

## 2. Session handshake

1. `GET /auth/nonce` → `200 {"nonce": "...", "expires_in": 60}`. A nonce is single-use and expires in 60 s.
2. `POST /auth/session` with `Content-Type: application/json` and body **exactly**:
   ```json
   {"client_id": "<client_id>", "nonce": "<nonce>", "proof": "<hex>"}
   ```
   where `proof = HMAC-SHA256(key = client_secret, message = client_id + "." + nonce)`, lowercase hex.
   → `201 {"session_token": "...", "token_type": "Bearer", "expires_in": 300}`

A session lives 300 s. Obtain a new one when it expires; do not cache it across runs.

## 3. Request signature

Each signed request computes:

```
canonical = t + "\n" + METHOD + "\n" + PATH + "\n" + sha256_hex(body)
v1        = HMAC-SHA256(key = client_secret, message = canonical)   # lowercase hex
X-Vista-Signature: t=<t>,v1=<v1>
```

* `t` — current Unix time in seconds (10 digits). Accepted window: ±60 s of server time.
* `METHOD` — upper-case HTTP method.
* `PATH` — the full request path **including the `/ingest/v1` prefix**, without query string.
  Example: `/ingest/v1/uploads/3f9c…/content`.
* `sha256_hex(body)` — hex SHA-256 of the exact request body bytes. For a request without a body this is the
  SHA-256 of the empty string: `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`.

Signatures are verified before anything else. Redirected requests are **not** re-signed by the server:
a client that follows a redirect must sign the new request itself.

## 4. Upload an image

### 4.1 Declare
`POST /uploads` — `Content-Type: application/json`, body with exactly these four keys:

```json
{"filename": "photo.jpg", "content_type": "image/jpeg", "size": 123456, "sha256": "<hex of file bytes>"}
```

* `content_type` ∈ `image/jpeg`, `image/png`, `image/webp`; `size` ≤ 8 MiB; `filename` `[A-Za-z0-9._-]{1,80}`.

→ `201` with `Location: /ingest/v1/uploads/{upload_id}/content` and `{"upload_id": "...", "expires_in": 120}`.

### 4.2 Send bytes
`PUT /uploads/{upload_id}/content`

* Body: the **raw file bytes** — not multipart, not base64, not JSON.
* `Content-Type`: must equal the declared `content_type`.
* `Content-Length`: must equal the declared `size`.
* `X-Content-SHA256`: hex SHA-256 of the body (must match both the body and the declaration).
* The bytes must decode as the declared image format.
* An upload can be consumed once and must be completed within 120 s of declaration.

→ `202` with `Location: /ingest/v1/jobs/{job_id}`, `Retry-After: 2` and `{"job_id": "...", "status": "queued"}`.

## 5. Wait for processing

`GET /jobs/{job_id}`

* `200 {"status": "processing"}` + `Retry-After: 2` — not ready; poll again **no earlier** than `Retry-After`.
  Polling earlier → `429 poll_too_early`.
* `303 See Other` with `Location: /ingest/v1/images/{image_id}` and an empty body — done.

## 6. Fetch the result

`GET /images/{image_id}` → `200`:

```json
{"image_id": "...", "filename": "photo.jpg", "content_type": "image/jpeg", "bytes": 123456,
 "sha256": "...", "width": 1920, "height": 1080, "format": "jpeg", "created_at": 1758400000,
 "delivery_url": "https://ingest.vista-eas.com/ingest/v1/files/...?exp=...&sig=...",
 "delivery_expires_in": 600, "receipt": "VR1-XXXXXXXXXXXXXXXXXXXX"}
```

`delivery_url` is a public, unsigned-request URL valid for 10 minutes. `receipt` is the proof of a completed
ingest; hand it to the reviewer.

## 7. Status codes you will meet

`200 201 202 303 400 401 404 405 406 409 410 413 415 422 429`. See `GET /errors`.
