---
title: "REST API"
description: "HTTP endpoint reference for XI Parallax: registration, slots, look-up, manifests, take-down and account stats."
published: 2026-09-21T23:44:21.494178+00:00
updated: 2026-09-21T23:44:21.494178+00:00
tags: ["api", "parallax", "reference", "rest"]
url: https://xiobjects.com/docs/xio/parallax/api/rest
source: XI Objects
---

<!-- xion:doctype xion+markdown -->
<!-- xion:metadata
{
  "version": "1.0",
  "content_type": "application/xion\u002Bmarkdown",
  "source_type": "xi-content/doc",
  "generator": "xio-content-publisher/1.0.0",
  "generated": "2026-09-21T23:42:09.0722961\u002B00:00",
  "encoding": "utf-8",
  "render_intent": "markdown",
  "title": "REST API",
  "slug": "xio/parallax/api/rest",
  "copyright": "\u00A9 2026 XI Objects Inc"
}
-->

# REST API

Every endpoint XI Parallax publishes to an account holder, with its request fields, its response shape and the statuses it answers.

## Setup

Base URL: `https://api.parallax.xiobjects.com`

Authenticate with the beta token an operator minted for your account:

```bash
export BASE="https://api.parallax.xiobjects.com"
export TOKEN="your-beta-token"

curl -sS "$BASE/account/stats" -H "Authorization: Bearer $TOKEN"
```

Every route except `GET /health` requires `Authorization: Bearer <token>`. Missing, malformed, unknown and revoked tokens all answer `401 Unauthorized` with no detail about which it was. The token resolves to your account, which is never expressible as a route parameter, a query-string value or a body field.

Check the service with `GET /health`. It takes no token.

The generated OpenAPI document is published at `https://api.parallax.xiobjects.com/openapi/v1.json`. It takes no token and answers a cross-origin GET, so a browser based viewer can load it by URL.

Every refusal answers `application/problem+json`. See [Errors and refusals](/docs/xio/parallax/errors-and-refusals) for the envelope, every status, and every refusal reason.

## Endpoints

### Service

| Method | Route | Scope | Description |
|--------|-------|-------|-------------|
| `GET` | `/health` | none | Service identity and version |

### Registrations

| Method | Route | Scope | Description |
|--------|-------|-------|-------------|
| `POST` | `/registrations` | account token | Register one image synchronously |
| `DELETE` | `/registrations/{registrationId}` | account token | Take one of your registrations down |

### Registration slots

| Method | Route | Scope | Description |
|--------|-------|-------|-------------|
| `POST` | `/slots` | account token | Open a registration slot |
| `GET` | `/slots/{slotId}` | account token | Report one slot |
| `DELETE` | `/slots/{slotId}` | account token | Abandon a slot and everything in it |
| `POST` | `/slots/{slotId}/uploads` | account token | Upload images into the slot |
| `POST` | `/slots/{slotId}/uploads/missing` | account token | Resume: which declared hashes the slot lacks |
| `GET` | `/slots/{slotId}/manifest` | account token | Report the slot's entries |
| `PUT` | `/slots/{slotId}/entries/{imageHash}/manifests` | account token | Replace one entry's manifest list |
| `DELETE` | `/slots/{slotId}/entries/{imageHash}` | account token | Remove one entry |
| `POST` | `/slots/{slotId}/commit` | account token | Register everything held. Terminal |
| `GET` | `/slots/{slotId}/progress` | account token | Per entry state and the poll counts |

### Look-up

| Method | Route | Scope | Description |
|--------|-------|-------|-------------|
| `POST` | `/lookup` | account token | Look one image up synchronously |

### Look-up slots

| Method | Route | Scope | Description |
|--------|-------|-------|-------------|
| `POST` | `/lookup/slots` | account token | Open a look-up slot |
| `DELETE` | `/lookup/slots/{lookupSlotId}` | account token | Abandon a look-up slot and everything in it |
| `POST` | `/lookup/slots/{lookupSlotId}/queries` | account token | Upload query images into the slot |
| `POST` | `/lookup/slots/{lookupSlotId}/queries/missing` | account token | Resume: which declared hashes the slot lacks |
| `GET` | `/lookup/slots/{lookupSlotId}/manifest` | account token | Report the slot's held queries |
| `DELETE` | `/lookup/slots/{lookupSlotId}/entries/{imageHash}` | account token | Remove one held query |
| `POST` | `/lookup/slots/{lookupSlotId}/commit` | account token | Look every held query up. Terminal |
| `GET` | `/lookup/slots/{lookupSlotId}/progress` | account token | Per query state and the poll counts |
| `GET` | `/lookup/slots/{lookupSlotId}/results` | account token | Read the verdicts back |

### Account

| Method | Route | Scope | Description |
|--------|-------|-------|-------------|
| `GET` | `/account/stats` | account token | Grants, consumed, held, remaining, and lifetime call count |

Administrative routes under `/admin` are not part of this collection. An operator uses them to create your account and mint your token.

## Shared shapes

### Manifest

Returned wherever manifests come back.

| Field | Type | Description |
|-------|------|-------------|
| `type` | `string` | The kind exactly as stated on the multipart part name. |
| `form` | `string` | `json` or `jumbf`. |
| `payload` | `object` or `string` | The posted bytes. Inline JSON when `form` is `json`, base64 when `form` is `jumbf`. |

### Upload outcome

One per image part, in request order. Answered by `POST /slots/{slotId}/uploads` and `POST /lookup/slots/{lookupSlotId}/queries`.

| Field | Type | Description |
|-------|------|-------------|
| `partIndex` | `int` | 1-based position among the image parts in the request. |
| `fileName` | `string?` | The filename declared on the part, if any. |
| `accepted` | `bool` | Whether the slot now holds this image. |
| `imageHash` | `string?` | SHA-256 of the accepted bytes, lowercase hex. Always `null` on a rejection. |
| `rejectionReason` | `string?` | A fixed sentence. `null` when accepted. |
| `registrationRemaining` | `int?` | Remaining registration grant. Carried only on a quota rejection. |
| `lookupRemaining` | `int?` | Remaining look-up grant. Carried only on a quota rejection. |
| `registrationId` | `uuid?` | The already-registered registration, when it is yours. Never populated on a look-up slot. |

### Slot entry

One per entry. Answered by both manifest report routes and by the manifest replace route.

| Field | Type | Description |
|-------|------|-------------|
| `imageHash` | `string` | SHA-256 of the entry's bytes, lowercase hex. |
| `manifestsHash` | `string?` | Digest of the stored manifest list. `null` when the entry carries none. |
| `byteLength` | `long` | The image's byte length. |
| `contentType` | `string?` | The content type the part declared. |
| `createdAt` | `datetime` | When the entry was first recorded. |
| `state` | `string` | `held`, `registered`, `answered`, `failed` or `errata`. |
| `notRegisteredReason` | `string?` | Why an `errata` entry will never be registered. `null` otherwise. |

No manifest body and no image byte is ever returned here.

### Progress

Answered by both progress routes.

| Field | Type | Description |
|-------|------|-------------|
| `slotId` | `string` | The slot's id. |
| `status` | `string` | `open` or `committed`. |
| `counts.total` | `int` | Entries on the slot. |
| `counts.held` | `int` | Entries not yet committed. |
| `counts.registered` | `int` | Entries registered. Registration slots only. |
| `counts.answered` | `int` | Queries that reached a verdict. Look-up slots only. |
| `counts.failed` | `int` | Entries that could not be committed. |
| `counts.errata` | `int` | Entries that will never be registered. Registration slots only. |
| `entries[].imageHash` | `string` | The entry's hash. |
| `entries[].state` | `string` | `held`, `registered`, `answered`, `failed` or `errata`. |
| `entries[].registrationId` | `uuid?` | The registration this entry points at, when it has one. |
| `entries[].failureReason` | `string?` | Why the entry failed, or why an errata will not register. |

---

## GET /health

Service identity and version. Takes no token.

**Response: 200 OK**

```json
{
  "service": "Xio.Parallax.Rest",
  "version": "1.0.0"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `service` | `string` | The service's own name. |
| `version` | `string` | The running build's version. |

---

## POST /registrations

Registers one image synchronously. Accepts `multipart/form-data`.

**Parts:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `manifest[<kind>]` | Form part | No | Zero or more manifests. Content-Type must be `application/json`, `application/jumbf` or `application/c2pa`. All manifest parts must precede the image part. |
| `image` | File part | Yes | Exactly one image. Its content type must be accepted by the deployment. |

```bash
curl -sS -X POST "$BASE/registrations" \
  -H "Authorization: Bearer $TOKEN" \
  -F 'manifest[xi-manifest]={"caption":"carried as given"};type=application/json' \
  -F "image=@photo.png;type=image/png"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `id` | `uuid` | The registration id. |
| `imageHash` | `string` | SHA-256 of the posted bytes, lowercase hex. |
| `manifests` | `array` | Manifests as registered, in the order sent. |
| `engineRecord` | `string` | The engine's own provenance document, opaque. |

```json
{
  "id": "6f0d4d6e-9a0b-4c2e-9b1a-2d3c4e5f6a7b",
  "imageHash": "3b8f1a...9e4c",
  "manifests": [
    { "type": "xi-manifest", "form": "json", "payload": { "caption": "carried as given" } }
  ],
  "engineRecord": "{\"originalImageHash\":\"...\",\"outcome\":\"Registered\",\"engine\":\"...\",\"engineVersion\":\"...\"}"
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 400 | `Malformed request` | Not multipart with a boundary, wrong parts, or a manifest part after the image part |
| 400 | `Invalid manifest` | A manifest part was refused |
| 409 | `Image not registered` | An original of this image is already registered. Carries `registrationId` when that registration is yours |
| 409 | `Open-slot cap reached` | Your open-slot cap is reached. Carries `cap` |
| 413 | `Request too large` | Past the per-request cap |
| 413 | `Manifest too large` | A manifest part is past its cap |
| 413 | `Image too large` | The image is past the per-image cap |
| 415 | `Unsupported image content type` | The image's content type is not accepted |
| 422 | `Image could not be checked` | The engine declined the pre-check |
| 429 | `Quota exceeded` | Carries `registrationRemaining` and `lookupRemaining` |
| 503 | `Engine not configured` | No engine is wired |

See [Register a single image](/docs/xio/parallax/register-single-image).

---

## DELETE /registrations/{registrationId}

Takes one of your registrations down. The image stops matching.

```bash
curl -sS -i -X DELETE "$BASE/registrations/$REGISTRATION_ID" \
  -H "Authorization: Bearer $TOKEN"
```

**Response: 204 No Content**, no body.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Registration not found` | Unknown, already taken down, or not yours |
| 503 | `Take-down refused` | The engine record could not be taken down. The registration is untouched |
| 503 | `Engine not configured` | No engine is wired |

See [Take down a registration](/docs/xio/parallax/take-down).

---

## POST /slots

Opens a registration slot.

```bash
curl -sS -X POST "$BASE/slots" -H "Authorization: Bearer $TOKEN"
```

**Response: 201 Created**, with a `Location` header pointing at the slot.

| Field | Type | Description |
|-------|------|-------------|
| `slotId` | `string` | The slot's opaque id. |
| `expiresAt` | `datetime` | When the slot dies, unless activity moves it. |

```json
{
  "slotId": "sl_8x2k9f3m1q7w5e0r4t6y8u",
  "expiresAt": "2026-09-21T14:32:05.118Z"
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 409 | `Open-slot cap reached` | Carries `cap` |

---

## GET /slots/{slotId}

Reports one slot. Does not refresh its idle window.

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `slotId` | `string` | The slot's id. |
| `status` | `string` | `open` or `committed`. |
| `entryCount` | `int` | Entries on the slot. |
| `openedAt` | `datetime` | When the slot was opened. |
| `expiresAt` | `datetime` | When the slot dies. |

```json
{
  "slotId": "sl_8x2k9f3m1q7w5e0r4t6y8u",
  "status": "open",
  "entryCount": 3,
  "openedAt": "2026-09-21T14:12:05.118Z",
  "expiresAt": "2026-09-21T14:32:05.118Z"
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, abandoned, or not yours |

---

## DELETE /slots/{slotId}

Abandons the slot and everything in it. Nothing was registered, so nothing is undone. Held grant comes back.

**Response: 204 No Content**, no body.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, already gone, or not yours |
| 409 | `Slot is no longer open` | The slot is committed |

---

## POST /slots/{slotId}/uploads

Uploads images into the slot. Accepts `multipart/form-data` carrying several images. Refreshes the slot's idle window.

**Parts:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `manifest[<kind>]` | Form part | No | Zero or more manifests for the next `image` part. |
| `image` | File part | Yes, at least one | One image per part. The part must be named `image`. |

```bash
curl -sS -X POST "$BASE/slots/$SLOT_ID/uploads" \
  -H "Authorization: Bearer $TOKEN" \
  -F 'manifest[xi-manifest]={"note":"v1"};type=application/json' \
  -F "image=@first.png;type=image/png" \
  -F "image=@second.jpg;type=image/jpeg"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `outcomes` | `array` | One upload outcome per image part, in request order. See [Upload outcome](#upload-outcome). |

```json
{
  "outcomes": [
    { "partIndex": 1, "fileName": "first.png", "accepted": true, "imageHash": "3b8f1a...9e4c", "rejectionReason": null, "registrationRemaining": null, "lookupRemaining": null, "registrationId": null },
    { "partIndex": 2, "fileName": "second.jpg", "accepted": false, "imageHash": null, "rejectionReason": "The account's remaining registration grant does not cover this image.", "registrationRemaining": 0, "lookupRemaining": 12, "registrationId": null }
  ]
}
```

An image already held under the same hash is replaced whole, taking the new request's manifests. It adds no second entry and no second reservation.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 400 | `Malformed request` | Not multipart with a boundary, or a part this conversation does not accept |
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 409 | `Slot is no longer open` | The slot is committed |
| 413 | `Request too large` | Past the per-request cap |
| 429 | `Quota exceeded` | No image in the request fit. Carries both remaining counts |
| 503 | `Engine not configured` | No engine is wired |

See [Register in batches](/docs/xio/parallax/register-in-batches).

---

## POST /slots/{slotId}/uploads/missing

Answers which of your declared hashes the slot does not already hold. Refreshes the slot's idle window.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `hashes` | `string[]` | No | Image hashes, 64 lowercase hex characters each, no duplicates. Omitted or null reads as an empty list. |

```bash
curl -sS -X POST "$BASE/slots/$SLOT_ID/uploads/missing" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"hashes":["3b8f1a...9e4c","ffff...ffff"]}'
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `missing` | `string[]` | The declared hashes the slot does not hold, in declared order. |

```json
{ "missing": ["ffff...ffff"] }
```

An entry the slot remembers as an errata is never reported missing.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 400 | `Too many declared hashes` | Past the declared-list cap |
| 400 | `Invalid declared hash` | A value is not a unique 64-character lowercase hex hash |
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 409 | `Slot is no longer open` | The slot is committed |

---

## GET /slots/{slotId}/manifest

Reports the slot's entries. Does not refresh the idle window. Never returns a manifest body or an image byte.

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `entries` | `array` | One [slot entry](#slot-entry) per entry, ordered by image hash. |

```json
{
  "entries": [
    {
      "imageHash": "3b8f1a...9e4c",
      "manifestsHash": "c41d7b...80ae",
      "byteLength": 184320,
      "contentType": "image/png",
      "createdAt": "2026-09-21T14:12:41.882Z",
      "state": "held",
      "notRegisteredReason": null
    }
  ]
}
```

This route names no registration id, on any entry.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, or not yours |

---

## PUT /slots/{slotId}/entries/{imageHash}/manifests

Replaces one entry's whole manifest list. Accepts the same multipart idiom the upload does. Refreshes the slot's idle window.

**Parts:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `manifest[<kind>]` | Form part | No | Zero or more manifests. A well formed body carrying none clears the list. No other part is accepted. |

```bash
curl -sS -X PUT "$BASE/slots/$SLOT_ID/entries/$IMAGE_HASH/manifests" \
  -H "Authorization: Bearer $TOKEN" \
  -F 'manifest[xi-manifest]={"note":"replaced"};type=application/json'
```

**Response: 200 OK**, the updated [slot entry](#slot-entry).

```json
{
  "imageHash": "3b8f1a...9e4c",
  "manifestsHash": "9ac2f0...1b77",
  "byteLength": 184320,
  "contentType": "image/png",
  "createdAt": "2026-09-21T14:12:41.882Z",
  "state": "held",
  "notRegisteredReason": null
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 400 | `Malformed request` | Not multipart with a boundary |
| 400 | `Invalid manifest` | A part was refused, or a part is not a manifest part |
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 404 | `Entry not found` | The slot holds no entry under that hash |
| 409 | `Slot is no longer open` | The slot is committed |
| 413 | `Manifest too large` | A part is past the per-manifest cap |

---

## DELETE /slots/{slotId}/entries/{imageHash}

Removes one entry and releases whatever it was holding. Idempotent: removing an entry that is already gone still answers `204`. Refreshes the slot's idle window.

**Response: 204 No Content**, no body.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 404 | `Entry not found` | The hash is not a valid image hash |
| 409 | `Slot is no longer open` | The slot is committed |

---

## POST /slots/{slotId}/commit

Registers every held entry. This is the only call that registers anything, and it is terminal.

```bash
curl -sS -X POST "$BASE/slots/$SLOT_ID/commit" -H "Authorization: Bearer $TOKEN"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `slotId` | `string` | The slot's id. |
| `status` | `string` | Always `committed`. |
| `entries[].imageHash` | `string` | The entry's hash. |
| `entries[].state` | `string` | `registered`, `errata` or `failed`. |
| `entries[].registrationId` | `uuid?` | The registration this entry points at, when it has one. |
| `entries[].failureReason` | `string?` | Why the entry failed, or why an errata will not register. |

```json
{
  "slotId": "sl_8x2k9f3m1q7w5e0r4t6y8u",
  "status": "committed",
  "entries": [
    { "imageHash": "3b8f1a...9e4c", "state": "registered", "registrationId": "6f0d4d6e-9a0b-4c2e-9b1a-2d3c4e5f6a7b", "failureReason": null },
    { "imageHash": "a17c05...2f91", "state": "errata", "registrationId": null, "failureReason": "This image is a derivative of an image that is already registered." }
  ]
}
```

One entry's outcome never affects its siblings. An `errata` or `failed` entry is charged nothing.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 409 | `Slot is no longer open` | Already committed or abandoned, or a concurrent commit won |
| 429 | `Quota exceeded` | The billable count exceeds your remaining registration grant. Nothing is registered and the slot stays open |
| 503 | `Engine not configured` | No engine is wired |

---

## GET /slots/{slotId}/progress

Reports the slot's state and every entry's own. Polling only. Does not refresh the idle window.

**Response: 200 OK**, the [progress](#progress) shape.

```json
{
  "slotId": "sl_8x2k9f3m1q7w5e0r4t6y8u",
  "status": "committed",
  "counts": { "total": 2, "held": 0, "registered": 1, "answered": 0, "failed": 0, "errata": 1 },
  "entries": [
    { "imageHash": "3b8f1a...9e4c", "state": "registered", "registrationId": "6f0d4d6e-9a0b-4c2e-9b1a-2d3c4e5f6a7b", "failureReason": null },
    { "imageHash": "a17c05...2f91", "state": "errata", "registrationId": "2c1e7f80-5d3b-4a92-8e11-0f4c6b7d8a90", "failureReason": "This image is already registered." }
  ]
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, purged, or not yours |

---

## POST /lookup

Looks one image up. Accepts `multipart/form-data` carrying exactly one image file part and nothing else.

**Parts:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| any file part | File part | Yes | Exactly one image. The part name carries no meaning. |

No query-string parameter and no other form field is accepted. There is no top-K, threshold or options object.

```bash
curl -sS -X POST "$BASE/lookup" \
  -H "Authorization: Bearer $TOKEN" \
  -F "image=@query.png;type=image/png"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `matched` | `bool` | `true` when any account holds a live registration for this image. |
| `candidates` | `array` | Only your own matching registrations. Empty for a match you do not own, and empty for no match. |
| `candidates[].registrationId` | `uuid` | Your registration for this image. |
| `candidates[].manifests` | `array` | The manifests it was registered with. |

```json
{
  "matched": true,
  "candidates": [
    {
      "registrationId": "6f0d4d6e-9a0b-4c2e-9b1a-2d3c4e5f6a7b",
      "manifests": [
        { "type": "xi-manifest", "form": "json", "payload": { "owner": "me" } }
      ]
    }
  ]
}
```

No score, rank or similarity value exists anywhere in this response.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 400 | `Malformed request` | A query-string parameter, a wrong part count, an extra part, or a malformed body |
| 413 | `Request too large` | Past the per-request cap |
| 413 | `Image too large` | Past the per-image cap |
| 422 | `Image could not be checked` | No verdict was produced. Nothing charged |
| 429 | `Quota exceeded` | Carries both remaining counts |
| 503 | `Engine not configured` | No engine is wired |

See [Look up a single image](/docs/xio/parallax/look-up-single-image).

---

## POST /lookup/slots

Opens a look-up slot. Look-up slots have their own cap, counted separately from registration slots.

```bash
curl -sS -X POST "$BASE/lookup/slots" -H "Authorization: Bearer $TOKEN"
```

**Response: 201 Created**, with a `Location` header pointing at the slot.

| Field | Type | Description |
|-------|------|-------------|
| `lookupSlotId` | `string` | The slot's opaque id. |
| `expiresAt` | `datetime` | When the slot dies, unless activity moves it. |

```json
{
  "lookupSlotId": "sl_4d7h2j9k1m3n5p7q9r2s4t",
  "expiresAt": "2026-09-21T14:32:05.118Z"
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 409 | `Open-slot cap reached` | Carries `cap` |

---

## DELETE /lookup/slots/{lookupSlotId}

Abandons the look-up slot. Every held query's pixels are deleted and every reservation comes back. Nothing was looked up, so nothing is charged.

**Response: 204 No Content**, no body.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, already gone, or not yours |
| 409 | `Slot is no longer open` | The slot is committed |

---

## POST /lookup/slots/{lookupSlotId}/queries

Uploads query images. No engine call happens here. Refreshes the slot's idle window.

**Parts:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| any file part | File part | Yes, at least one | One query image per part. The part name carries no meaning. |

A look-up request carries the image alone. No manifest part, no other form field and no query-string parameter is accepted. Any of them refuses the whole request, and a refused request holds nothing.

```bash
curl -sS -X POST "$BASE/lookup/slots/$LOOKUP_SLOT_ID/queries" \
  -H "Authorization: Bearer $TOKEN" \
  -F "a=@first.png;type=image/png" \
  -F "b=@second.png;type=image/png"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `outcomes` | `array` | One [upload outcome](#upload-outcome) per image part, in request order. `registrationId` is never populated here. |

```json
{
  "outcomes": [
    { "partIndex": 1, "fileName": "first.png", "accepted": true, "imageHash": "3b8f1a...9e4c", "rejectionReason": null, "registrationRemaining": null, "lookupRemaining": null, "registrationId": null },
    { "partIndex": 2, "fileName": "second.png", "accepted": false, "imageHash": null, "rejectionReason": "This lookup slot already holds the maximum number of query images.", "registrationRemaining": null, "lookupRemaining": null, "registrationId": null }
  ]
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 400 | `Malformed request` | A query-string parameter, a non-image part, more images than the per-request cap, or a malformed body |
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 409 | `Slot is no longer open` | The slot is committed |
| 413 | `Request too large` | Past the per-request cap |
| 429 | `Quota exceeded` | No image in the request fit. Carries both remaining counts |
| 503 | `Engine not configured` | No engine is wired |

---

## POST /lookup/slots/{lookupSlotId}/queries/missing

Answers which of your declared hashes the look-up slot does not already hold. Refreshes the slot's idle window.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `hashes` | `string[]` | No | Image hashes, 64 lowercase hex characters each, no duplicates. |

```bash
curl -sS -X POST "$BASE/lookup/slots/$LOOKUP_SLOT_ID/queries/missing" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"hashes":["3b8f1a...9e4c","ffff...ffff"]}'
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `missing` | `string[]` | The declared hashes the slot does not hold. |

```json
{ "missing": ["ffff...ffff"] }
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 400 | `Too many declared hashes` | Past the declared-list cap |
| 400 | `Invalid declared hash` | A value is not a unique 64-character lowercase hex hash |
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 409 | `Slot is no longer open` | The slot is committed |

---

## GET /lookup/slots/{lookupSlotId}/manifest

Reports the held queries. Does not refresh the idle window.

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `entries` | `array` | One [slot entry](#slot-entry) per held query. |

A query carries no manifest of its own, so `manifestsHash` and `notRegisteredReason` are always `null` here.

```json
{
  "entries": [
    {
      "imageHash": "3b8f1a...9e4c",
      "manifestsHash": null,
      "byteLength": 184320,
      "contentType": "image/png",
      "createdAt": "2026-09-21T14:12:41.882Z",
      "state": "held",
      "notRegisteredReason": null
    }
  ]
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, or not yours |

---

## DELETE /lookup/slots/{lookupSlotId}/entries/{imageHash}

Removes one held query, deletes its pixels and gives its reservation back. Idempotent. Refreshes the slot's idle window.

**Response: 204 No Content**, no body.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 404 | `Entry not found` | The hash is not a valid image hash |
| 409 | `Slot is no longer open` | The slot is committed |

---

## POST /lookup/slots/{lookupSlotId}/commit

Looks every held query up. This is the only call that triggers the engine's look-up, and it is terminal. It answers the results directly.

```bash
curl -sS -X POST "$BASE/lookup/slots/$LOOKUP_SLOT_ID/commit" -H "Authorization: Bearer $TOKEN"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `lookupSlotId` | `string` | The slot's id. |
| `queries[].imageHash` | `string` | The query's hash. |
| `queries[].state` | `string` | `answered`, `held` or `failed`. |
| `queries[].result` | `object?` | The verdict, when the query reached one. `matched` plus `candidates`, the same shape `POST /lookup` answers. |
| `queries[].failureReason` | `string?` | Why the query could not be answered. |

```json
{
  "lookupSlotId": "sl_4d7h2j9k1m3n5p7q9r2s4t",
  "queries": [
    {
      "imageHash": "3b8f1a...9e4c",
      "state": "answered",
      "result": {
        "matched": true,
        "candidates": [
          { "registrationId": "6f0d4d6e-9a0b-4c2e-9b1a-2d3c4e5f6a7b", "manifests": [] }
        ]
      },
      "failureReason": null
    },
    {
      "imageHash": "a17c05...2f91",
      "state": "answered",
      "result": { "matched": false, "candidates": [] },
      "failureReason": null
    }
  ]
}
```

A query the engine would not serve lands `failed`, never as no match, and is charged nothing.

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, or not yours |
| 409 | `Slot is no longer open` | Already committed or abandoned |
| 429 | `Quota exceeded` | Carries both remaining counts |
| 503 | `Engine not configured` | No engine is wired |

---

## GET /lookup/slots/{lookupSlotId}/progress

Reports the slot's state and each query's own, without the verdict bodies. Polling only. Does not refresh the idle window.

**Response: 200 OK**, the [progress](#progress) shape. `counts.registered` and `counts.errata` are always `0` on a look-up slot.

```json
{
  "slotId": "sl_4d7h2j9k1m3n5p7q9r2s4t",
  "status": "committed",
  "counts": { "total": 2, "held": 0, "registered": 0, "answered": 2, "failed": 0, "errata": 0 },
  "entries": [
    { "imageHash": "3b8f1a...9e4c", "state": "answered", "registrationId": null, "failureReason": null },
    { "imageHash": "a17c05...2f91", "state": "answered", "registrationId": null, "failureReason": null }
  ]
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, purged, or not yours |

---

## GET /lookup/slots/{lookupSlotId}/results

Reads the verdicts back. Same shape the commit answered. Readable until the slot's `expiresAt`, then the slot is purged and this route answers `404`.

```bash
curl -sS "$BASE/lookup/slots/$LOOKUP_SLOT_ID/results" -H "Authorization: Bearer $TOKEN"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `lookupSlotId` | `string` | The slot's id. |
| `queries` | `array` | One entry per query the slot holds, with its state, verdict and failure reason. |

```json
{
  "lookupSlotId": "sl_4d7h2j9k1m3n5p7q9r2s4t",
  "queries": [
    {
      "imageHash": "3b8f1a...9e4c",
      "state": "answered",
      "result": { "matched": true, "candidates": [] },
      "failureReason": null
    }
  ]
}
```

**Statuses:**

| Status | Title | Meaning |
|--------|-------|---------|
| 404 | `Slot not found` | Unknown, expired, purged, or not yours |

See [Look up in batches](/docs/xio/parallax/look-up-in-batches).

---

## GET /account/stats

Reports your own account's grants and usage. No other account's stats are expressible.

```bash
curl -sS "$BASE/account/stats" -H "Authorization: Bearer $TOKEN"
```

**Response: 200 OK**

| Field | Type | Description |
|-------|------|-------------|
| `registrations.grant` | `int` | Registrations granted. |
| `registrations.consumed` | `int` | Registrations spent. |
| `registrations.held` | `int` | Images reserved across your open registration slots. |
| `registrations.remaining` | `int` | `grant` minus `consumed`. Not reduced by `held`. |
| `lookups.grant` | `int` | Look-ups granted. |
| `lookups.consumed` | `int` | Look-ups spent. |
| `lookups.held` | `int` | Queries reserved across your open look-up slots. |
| `lookups.remaining` | `int` | `grant` minus `consumed`. Not reduced by `held`. |
| `callCount` | `int` | Every authenticated call your account has made. |

```json
{
  "registrations": { "grant": 10, "consumed": 3, "held": 2, "remaining": 7 },
  "lookups": { "grant": 50, "consumed": 12, "held": 0, "remaining": 38 },
  "callCount": 84
}
```

See [Quotas and stats](/docs/xio/parallax/quotas-and-stats).
<!-- xion:trust
{
  "v": 1,
  "canon_v": 1,
  "ctx": "xiobjects.com/content",
  "hash_blake3_hex": "52dfa1878cda654765eba6682d4a9364e6b7421f53a5c95e5e54862ef1683a84",
  "hash_sha256_hex": null,
  "sig_alg": "ed25519",
  "sig_b64": "v0SSOL6tHxSHZlyQ9BirzOLxryYXhs4QIXPaSbRpg29d7FwS6eiVjw9_xD9_xXVCFiBHuVMiDWBvtYijSRHmAQ",
  "pubkey_b64": "VrV0rqYGnIqutEwPTr11jfrjH9wmtkOlL68wL0W6ed0",
  "x509_chain_pem": [
    "-----BEGIN CERTIFICATE-----\nMIIB9DCCAaagAwIBAgIQbmGqXUijS3XuRUHDuVME8jAFBgMrZXAwLjEsMCoGA1UE\nAwwjWEkgT2JqZWN0cyBJbmMgQ29udHJvbCBJbnRlcm1lZGlhdGUwHhcNMjYwOTIx\nMjMzNjU1WhcNMjYxMDIxMjMzNjU1WjBLMR4wHAYDVQQDDBV4aW8tY29udGVudC1w\ndWJsaXNoZXIxFzAVBgNVBAoMDlhJIE9iamVjdHMgSW5jMRAwDgYDVQQLDAdDb250\nZW50MCowBQYDK2VwAyEAVrV0rqYGnIqutEwPTr11jfrjH9wmtkOlL68wL0W6ed2j\ngbwwgbkwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI\nKwYBBQUHAyQwZQYDVR0jBF4wXIAUOym3mFmw/qs1fgKrujCkxhrTk7KhLqQsMCox\nKDAmBgNVBAMMH0luc3RpdHV0ZSBvZiBQcm92ZW5hbmNlIFJvb3QgQ0GCFFJgN/ix\nQn72H6h3T5lEr9f8lJQFMB0GA1UdDgQWBBTnptRqwN8T\u002B5J0zUSRl65iscaPUzAF\nBgMrZXADQQBTzG1wSuUk70ymEN3Mj6XxsS1c5egjDoy\u002BW/V2kko5c2a1Cs7c/kiD\n6H2y9z1DNSH5qjzLZcm9JKPN0mjCy8MO\n-----END CERTIFICATE-----\n",
    "-----BEGIN CERTIFICATE-----\nMIIByDCCAXqgAwIBAgIUUmA3\u002BLFCfvYfqHdPmUSv1/yUlAUwBQYDK2VwMCoxKDAm\nBgNVBAMMH0luc3RpdHV0ZSBvZiBQcm92ZW5hbmNlIFJvb3QgQ0EwHhcNMjUxMTAy\nMDMxNzEyWhcNMzAxMTAxMDMxNzEyWjAuMSwwKgYDVQQDDCNYSSBPYmplY3RzIElu\nYyBDb250cm9sIEludGVybWVkaWF0ZTAqMAUGAytlcAMhAFSS/pggSRmTcAMko7uc\nATH8OHgxVymd5mBFlPXbJkgio4GtMIGqMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD\nVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQ7KbeYWbD\u002BqzV\u002BAqu6MKTGGtOTsjBlBgNV\nHSMEXjBcgBQAZRTDswSVORu\u002BkUOKX6WvrOvmQKEupCwwKjEoMCYGA1UEAwwfSW5z\ndGl0dXRlIG9mIFByb3ZlbmFuY2UgUm9vdCBDQYIUJqoJlpiSFg\u002B7W5IJLMrLttgR\nQp4wBQYDK2VwA0EA5FOht7YOsVRPp/FOKMQ\u002B3Mo9JxrvGR3ylKWAWNm6OUV7N3DB\nI9cD62wU5I0d0EKDBy0CX9DnoqUyxv5yguraAA==\n-----END CERTIFICATE-----\n",
    "-----BEGIN CERTIFICATE-----\nMIIBaTCCARugAwIBAgIUJqoJlpiSFg\u002B7W5IJLMrLttgRQp4wBQYDK2VwMCoxKDAm\nBgNVBAMMH0luc3RpdHV0ZSBvZiBQcm92ZW5hbmNlIFJvb3QgQ0EwHhcNMjUxMTAy\nMDMwNTEyWhcNMzUxMDMxMDMwNTEyWjAqMSgwJgYDVQQDDB9JbnN0aXR1dGUgb2Yg\nUHJvdmVuYW5jZSBSb290IENBMCowBQYDK2VwAyEAEWNZl\u002Br3IC7\u002BgBh90Yo1kWk1\npZCVzVuFdFT7qBBU8W2jUzBRMB0GA1UdDgQWBBQAZRTDswSVORu\u002BkUOKX6WvrOvm\nQDAfBgNVHSMEGDAWgBQAZRTDswSVORu\u002BkUOKX6WvrOvmQDAPBgNVHRMBAf8EBTAD\nAQH/MAUGAytlcANBAO6QeydOFNrN75qNyftggYudsxMyl4w9qWkSdZ6hlhrRcbSr\niG9Si0kbrIJOwYB/LTBU0RM4Rl\u002Bo9PM3Qp0mPwo=\n-----END CERTIFICATE-----\n"
  ],
  "key_id": "_A4EpsXvfBfVEGu5deK65yN4EjBJsUpgkdoS25zPEFk",
  "created_at": "2026-09-21T23:42:09Z"
}
-->