---
title: "Look Up in Batches"
description: "Open a look-up slot, upload query images, commit once, and read a verdict per image. The batch look-up conversation."
published: 2026-09-21T23:43:47.038839+00:00
updated: 2026-09-21T23:43:47.038839+00:00
tags: ["lookup", "parallax", "rest", "slots"]
url: https://xiobjects.com/docs/xio/parallax/look-up-in-batches
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:02.7405911\u002B00:00",
  "encoding": "utf-8",
  "render_intent": "markdown",
  "title": "Look Up in Batches",
  "slug": "xio/parallax/look-up-in-batches",
  "copyright": "\u00A9 2026 XI Objects Inc"
}
-->

# Look Up in Batches

A look-up slot works like a registration slot. Open it, upload query images across as many requests as you like, then commit. Commit is the only step that looks anything up.

Uploading a query image never calls the engine. Do not expect a verdict from an upload response. It tells you only whether the image was accepted.

## The conversation

1. `POST /lookup/slots` opens a look-up slot.
2. `POST /lookup/slots/{lookupSlotId}/queries` uploads query images. Repeat as needed.
3. `POST /lookup/slots/{lookupSlotId}/queries/missing` tells you which of your hashes the slot does not hold.
4. `GET /lookup/slots/{lookupSlotId}/manifest` reports what the slot holds.
5. `DELETE /lookup/slots/{lookupSlotId}/entries/{imageHash}` removes one query.
6. `POST /lookup/slots/{lookupSlotId}/commit` looks every held query up. This is terminal.
7. `GET /lookup/slots/{lookupSlotId}/results` reads the verdicts back.
8. `GET /lookup/slots/{lookupSlotId}/progress` reports per query state.
9. `DELETE /lookup/slots/{lookupSlotId}` abandons the slot and everything in it.

Look-up slots and registration slots are separate. A registration slot id is not addressable under `/lookup/slots`, and a look-up slot id is not addressable under `/slots`. Each kind has its own open-slot cap and its own expiry clocks.

## Open a slot

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

`201 Created`, with a `Location` header pointing at the new slot.

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

Past your open look-up slot cap, the call answers `409 Open-slot cap reached` with a `cap` extension.

## Upload query images

`POST /lookup/slots/{lookupSlotId}/queries` takes a `multipart/form-data` body of image file parts. Part names carry no meaning: every file part is a query image.

A look-up request carries the image alone. No manifest part, no extra form field, no query-string parameter. 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" \
  -F "c=@third.png;type=image/png"
```

```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": true, "imageHash": "a17c05...2f91", "rejectionReason": null, "registrationRemaining": null, "lookupRemaining": null, "registrationId": null },
    { "partIndex": 3, "fileName": "third.png", "accepted": true, "imageHash": "b90e33...41cd", "rejectionReason": null, "registrationRemaining": null, "lookupRemaining": null, "registrationId": null }
  ]
}
```

The outcome shape is the one the registration upload answers. `registrationId` is never populated here: a look-up carries no registration outcome at all.

Uploads are idempotent by image hash. Uploading the same bytes again answers the same hash and adds no second query and no second reservation.

Each accepted query reserves one look-up from your grant while it is held. An image that does not fit your remaining grant is refused on its own with `The account's remaining lookup grant does not cover this image.` When none of a request's images fit, the whole request answers `429 Quota exceeded`.

## Ceilings on one look-up slot

| Ceiling | What happens |
|---------|---------------|
| Query images per request | The whole request is refused with `400 Malformed request`, detail `Uploading lookup queries accepts at most N image file parts.` Nothing is held. |
| Query images per slot | The over-ceiling image is refused on its own with `This lookup slot already holds the maximum number of query images.` The images that fit are still held. |

## Resume

`POST /lookup/slots/{lookupSlotId}/queries/missing` answers which of your declared hashes the slot does not hold.

```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"]}'
```

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

Each hash must be 64 lowercase hex characters and appear once. A list past the cap answers `400 Too many declared hashes`; a value that is not a unique hash answers `400 Invalid declared hash`.

## Report what the slot holds

`GET /lookup/slots/{lookupSlotId}/manifest` lists the held queries. A query carries no manifest of its own, so `manifestsHash` and `notRegisteredReason` are always `null` on this conversation.

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

## Remove a query

`DELETE /lookup/slots/{lookupSlotId}/entries/{imageHash}` answers `204 No Content`. It is idempotent. Removing a query deletes its pixels and gives its reservation back.

## Commit

`POST /lookup/slots/{lookupSlotId}/commit` is the only call that looks anything up. It is terminal, and it answers the results directly, so you do not have to poll.

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

```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": true, "candidates": [] },
      "failureReason": null
    },
    {
      "imageHash": "b90e33...41cd",
      "state": "answered",
      "result": { "matched": false, "candidates": [] },
      "failureReason": null
    }
  ]
}
```

| `state` | Meaning |
|---------|---------|
| `answered` | The query reached a verdict. `result` carries it. |
| `held` | The query was never reached. No verdict, no charge. |
| `failed` | The query could not be answered. `failureReason` says why. No charge. |

The verdict in `result` is exactly the shape `POST /lookup` answers: `matched`, plus your own registrations in `candidates` when the match is yours. A match another account holds answers `matched: true` with an empty `candidates`. See [Look up a single image](/docs/xio/parallax/look-up-single-image).

A query the engine would not serve is never reported as no match. It lands `failed`, carrying the engine's own refusal in `failureReason`.

Whole-request refusals:

| Status | Title | When |
|--------|-------|------|
| `404` | `Slot not found` | The slot id is unknown, or it is not yours. |
| `409` | `Slot is no longer open` | The slot is already committed or abandoned. |
| `429` | `Quota exceeded` | Your look-up grant does not cover this commit. |
| `503` | `Engine not configured` | The deployment has no engine wired. |

## Read the results again

`GET /lookup/slots/{lookupSlotId}/results` answers the same shape the commit did, read back from the slot.

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

Results stay readable until the slot's `expiresAt`. After that the slot is purged and every route on it answers `404`. Copy anything you need out before then.

## Progress

`GET /lookup/slots/{lookupSlotId}/progress` reports the slot's state and per query state without the verdict bodies.

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

`counts.registered` and `counts.errata` belong to the registration conversation. On a look-up slot they are always `0`.

## Abandon

`DELETE /lookup/slots/{lookupSlotId}` answers `204 No Content`. Every held query's pixels are deleted and every reservation comes back. Nothing was looked up, so nothing is charged.

A committed slot cannot be abandoned: the call answers `409 Slot is no longer open`.

## How long a look-up slot lives

The same two clocks a registration slot has. An idle window that moves forward on every command, and an absolute ceiling fixed at open. `expiresAt` on the open response and every slot read tells you where you are.

Commands that push the idle window forward: uploading queries, asking what is missing, removing an entry, and a commit's own completion. Reads do not.

If a look-up slot expires before you commit, every held query is gone, its pixels are deleted, and nothing was charged.

## Ownership

A look-up slot belongs to the account whose token opened it. Another account's slot id answers exactly the same `404 Slot not found` body as an id that never existed.
<!-- xion:trust
{
  "v": 1,
  "canon_v": 1,
  "ctx": "xiobjects.com/content",
  "hash_blake3_hex": "199628ed1e9a668a2584e557ebf3153f76720d4a80bd2dcd0bf8db38151c6fab",
  "hash_sha256_hex": null,
  "sig_alg": "ed25519",
  "sig_b64": "AyULO83vuUKCUJwHVln3Ux8LmEDxFBfjZHBLFqtyse9UB9P15Wcxc1cbzustgNyIhL1_bZAAEGomnnnN59i-Aw",
  "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:02Z"
}
-->