> ## Documentation Index
> Fetch the complete documentation index at: https://docs.9pic.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Face Search

> Search event photos by uploading a selfie

## Overview

Face Search is the API surface for **9Pic FaceFind**, our AI face recognition photo search. Upload a participant's selfie and the API returns paginated photos that match the selfie above the event's confidence threshold.

Each search is identified by a `request_id` (UUID). You can:

* Omit `request_id` and let 9Pic generate one. The generated UUID is returned in the response so you can re-fetch the same results later without re-uploading the selfie.
* Pass an existing `request_id` to retrieve cached results without re-running face matching.

<Note>
  Selfie/face search must be enabled for the event. Check the `selfie_search` flag from [Event Details](/api-reference/event-details) before calling this.
</Note>

## Why `request_id` Matters

A face search is **expensive on our side and yours**. Each `POST /faces` triggers an image upload, face matching against the event's face index, confidence filtering, and a persisted result row. The `request_id` is what lets you do this work **exactly once** per user-intent and reuse the result everywhere else.

Treat `request_id` as the **canonical identifier for a single selfie search**, not as a session token. The full idempotency model is documented in [Conventions](/api-reference/conventions#request-id-idempotency).

### What problem it solves

| Problem                                  | What naive integrations do              | What `request_id` enables                                                                                    |
| ---------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| User refreshes the results screen        | Re-upload selfie + re-run face matching | `GET /faces/{request_id}` returns the cached result instantly                                                |
| User pages through results               | POST again per page                     | One POST, many `GET /faces/{request_id}?page=N` calls                                                        |
| Network blip on `POST /faces`            | Treat as "new search" and re-upload     | Retry the same POST with the same `request_id` (idempotent)                                                  |
| Download flow needs the same matched set | Re-search before download               | Pass `request_id` as the `identifier` to [Download Original Photos](/api-reference/download-original-photos) |

### How it optimises your system

* **Idempotent retries.** If `POST /faces` fails partway (timeout, 5xx, dropped connection), retrying with the same `request_id` resolves to the cached result on success — no double-charged face-match calls and no duplicate selfie uploads.
* **Cheap pagination.** Hold the `request_id` in your UI state. Every page change becomes a `GET /faces/{request_id}?page=N&page_size=32` instead of a re-search.
* **Decouple search trigger from result rendering.** Your upload screen does the POST and stashes `request_id`. Your results screen, share screen, and download screen all read from `GET /faces/{request_id}`. None of them need the selfie file.
* **Stable, shareable references.** A `request_id` lets your backend log, audit, retry, or reconcile a specific selfie search without storing the user's selfie image.
* **Avoid rate limiting.** External API calls that perform face matching are subject to stricter limits than read-only calls. Reusing `request_id` shifts your traffic from `POST /faces` (rate-limited, billable compute) to `GET /faces/{request_id}` (cheap reads), keeping integrations comfortably under quota even at peak.

### Recommended Integration Sequence — "POST once, GET many"

<Steps>
  <Step title="POST once">
    Call `POST /faces` with the user's selfie. Do not pass `request_id`; let the server generate one. Read `request_id` from the response and persist it (URL state, local storage, your DB).
  </Step>

  <Step title="GET many">
    Every subsequent action that needs results (pagination, refresh, deep-link, share) calls `GET /faces/{request_id}?page=N&page_size=32`.
  </Step>

  <Step title="Retry safely">
    On POST failure, retry with the **same** `request_id` you intended for that selfie. The result is cached server-side once any POST succeeds.
  </Step>

  <Step title="Hand off to download">
    Pass `request_id` as `identifier` (with `method: "selfie"`) to [Download Original Photos](/api-reference/download-original-photos). No new search is needed.
  </Step>
</Steps>

<Tip>
  Persist `request_id` in your URL (e.g. `/results/:request_id`). It survives refreshes, deep-links, and share intents without re-uploading the selfie.
</Tip>

<Warning>
  A new `POST /faces` (with a fresh `request_id`) is treated as a new billable face search. Avoid issuing fresh POSTs on every page change, refresh, or component remount.
</Warning>

## Endpoints

```
POST /api/v1/ext/{org_id}/event/{event_id}/faces
GET  /api/v1/ext/{org_id}/event/{event_id}/faces/{request_id}
```

## POST /faces — Upload a Selfie

Uploads a selfie image and returns the matching event photos.

### Path Parameters

| Parameter  | Type   | Required | Description                 |
| ---------- | ------ | -------- | --------------------------- |
| `org_id`   | number | Yes      | Your organisation ID.       |
| `event_id` | number | Yes      | The event to search within. |

### Query Parameters

| Parameter   | Type   | Required | Description                                  |
| ----------- | ------ | -------- | -------------------------------------------- |
| `page`      | number | No       | Page number (default: `1`).                  |
| `page_size` | number | No       | Images per page (default: `32`, max: `100`). |

<Note>
  Results are paginated. See the [Pagination Model](/api-reference/models/pagination).
</Note>

### Multipart Form Fields

| Field        | Type   | Required | Description                                                          |
| ------------ | ------ | -------- | -------------------------------------------------------------------- |
| `file`       | file   | Yes      | Selfie image (JPEG/PNG, max 5 MB recommended).                       |
| `request_id` | string | No       | UUID. If omitted, 9Pic generates one and returns it in the response. |

### Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -i \
    -H "X-API-Key: <your_9pic_api_key>" \
    -F "file=@selfie.jpg" \
    "https://api.9pic.ai/api/v1/ext/903/event/456/faces?page=1&page_size=32"
  ```

  ```python Python theme={null}
  import requests

  with open("selfie.jpg", "rb") as f:
      response = requests.post(
          "https://api.9pic.ai/api/v1/ext/903/event/456/faces",
          headers={"X-API-Key": "<your_9pic_api_key>"},
          files={"file": ("selfie.jpg", f, "image/jpeg")},
          params={"page": 1, "page_size": 32},
      )

  data = response.json()
  print(response.status_code, data["data"]["request_id"])
  ```

  ```javascript JavaScript theme={null}
  const formData = new FormData();
  formData.append("file", selfieFile);

  const response = await fetch(
    "https://api.9pic.ai/api/v1/ext/903/event/456/faces?page=1&page_size=32",
    {
      method: "POST",
      headers: { "X-API-Key": "<your_9pic_api_key>" },
      body: formData,
    }
  );

  const result = await response.json();
  console.log(result.data.request_id, result.data.images.length);
  ```
</CodeGroup>

To reuse an existing `request_id`, add `-F "request_id=<uuid>"` (cURL), include it in `data={"request_id": "<uuid>"}` (Python), or `formData.append("request_id", "<uuid>")` (JavaScript).

### Example Response

<Tabs>
  <Tab title="Matches Found">
    ```json theme={null}
    {
      "responseType": "success",
      "message": "Computed face search",
      "data": {
        "request_id": "ddc661a7-8861-4793-9437-af42a82d12f8",
        "confidence_percentage": 90,
        "images": [
          {
            "img_url": "https://photos.9pic.ai/imgs/456/large/a4402318-0cf4-4e1e-b8e8-ac8e8f2fc244.jpg",
            "height": 4860,
            "width": 3240,
            "image_id": "a4402318-0cf4-4e1e-b8e8-ac8e8f2fc244",
            "thumbnail_url": "https://photos.9pic.ai/imgs/456/small/a4402318-0cf4-4e1e-b8e8-ac8e8f2fc244.jpg",
            "original_url": null
          }
        ],
        "pagination": {
          "total": 12,
          "currentPage": 1,
          "totalPages": 1,
          "hasNextPage": false,
          "hasPreviousPage": false,
          "page_size": 32
        }
      }
    }
    ```
  </Tab>

  <Tab title="No Matches">
    ```json theme={null}
    {
      "responseType": "success",
      "message": "No image found similar to your selfie",
      "data": {
        "request_id": "ddc661a7-8861-4793-9437-af42a82d12f8",
        "confidence_percentage": 90,
        "images": [],
        "pagination": {
          "total": 0,
          "currentPage": 0,
          "totalPages": 0,
          "hasNextPage": false,
          "hasPreviousPage": false,
          "page_size": 32
        }
      }
    }
    ```
  </Tab>
</Tabs>

## GET /faces/{request_id} — Fetch Cached Search

Returns the cached result for a previous selfie search using its `request_id`. Useful for paging through results or refreshing a UI without re-uploading the selfie.

### Path Parameters

| Parameter    | Type   | Required | Description                                     |
| ------------ | ------ | -------- | ----------------------------------------------- |
| `org_id`     | number | Yes      | Your organisation ID.                           |
| `event_id`   | number | Yes      | The event the search belongs to.                |
| `request_id` | string | Yes      | UUID returned by a previous `POST /faces` call. |

### Query Parameters

| Parameter   | Type   | Required | Description                                  |
| ----------- | ------ | -------- | -------------------------------------------- |
| `page`      | number | No       | Page number (default: `1`).                  |
| `page_size` | number | No       | Images per page (default: `32`, max: `100`). |

<Note>
  Results are paginated. See the [Pagination Model](/api-reference/models/pagination).
</Note>

### Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -i \
    -H "X-API-Key: <your_9pic_api_key>" \
    "https://api.9pic.ai/api/v1/ext/903/event/456/faces/ddc661a7-8861-4793-9437-af42a82d12f8?page=2&page_size=32"
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      "https://api.9pic.ai/api/v1/ext/903/event/456/faces/ddc661a7-8861-4793-9437-af42a82d12f8",
      headers={"X-API-Key": "<your_9pic_api_key>"},
      params={"page": 2, "page_size": 32},
  )
  print(response.status_code, response.json())
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api.9pic.ai/api/v1/ext/903/event/456/faces/ddc661a7-8861-4793-9437-af42a82d12f8?page=2&page_size=32",
    { headers: { "X-API-Key": "<your_9pic_api_key>" } }
  );
  console.log(await response.json());
  ```
</CodeGroup>

### Example Response

The response shape is identical to `POST /faces`. Use the same model and the same `Matches Found` / `No Matches` payloads above.

## Response Models

| Model                                                                                      | Description                                            |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| <a href="/api-reference/models/search-responses#facesearchresponse">FaceSearchResponse</a> | Top-level face search response envelope.               |
| <a href="/api-reference/models/image">ImageItem</a>                                        | Image object returned inside `data.images[]`.          |
| <a href="/api-reference/models/pagination">PaginationInfo</a>                              | Pagination metadata returned inside `data.pagination`. |

`original_url` is `null` in the standard face search response.

<Tip>
  Use the `request_id` from this response with [Download Original Photos](/api-reference/download-original-photos) (`method: "selfie"`) to fetch presigned download links for the matched originals.
</Tip>

## Error Responses

| Status | Meaning                                                                                                    |
| ------ | ---------------------------------------------------------------------------------------------------------- |
| `400`  | `request_id` is not a valid UUID, or the uploaded selfie file is empty.                                    |
| `401`  | API key is missing.                                                                                        |
| `403`  | API key is invalid, inactive, token/event ownership mismatch, or selfie search is disabled for this event. |
| `404`  | Event configuration not found, or `request_id` is unknown (GET only).                                      |
| `429`  | Rate limit exceeded. Back off and retry with the same `request_id` to hit the cache.                       |
| `500`  | Internal failure that prevented the request from being recorded.                                           |

See [Errors](/api-reference/errors) for canonical descriptions and retry guidance.
