> ## 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.

# 9Pic Integration

> One skill to build and review API key, guest search, clips, and selling integrations

## Overview

`9pic-integration` is the one 9Pic skill for coding agents. It covers every partner integration with `https://api.9pic.ai`:

* **Any API call:** create and send an API key, keep it on your backend, and read the shared envelope, pagination, and errors.
* **Participant app:** a find-my-photos app with selfie search (**9Pic FaceFind**), bib search (**9Pic BibTrack**), a photo grid, timeline, and **9Pic Motion** clips. Guests see previews.
* **Selling originals:** you take payment in your own checkout, then your backend unlocks original photos.
* **Review:** once your app is built, the agent checks it against these practices and writes a report of **Must have** and **Good to have** fixes.

The skill names the [API Reference](https://docs.9pic.ai/api-reference.md) page for each call. Paths, fields, and examples live there.

## Install

```bash theme={null}
npx skills add https://docs.9pic.ai
```

See [Agent Skills](https://docs.9pic.ai/agent-skills.md) for updating installed skills and the discovery index.

## Call Path

The guest app never sends `X-API-Key`. Your frontend talks to your backend; your backend talks to 9Pic. The sale step is optional.

```mermaid theme={null}
sequenceDiagram
  participant Guest
  participant App as Partner app
  participant Backend as Partner backend
  participant Pay as Partner payment
  participant Pic as 9Pic API

  App->>Backend: Event details (no API key)
  Backend->>Pic: Event Details with X-API-Key
  Pic-->>Backend: Flags and name
  Guest->>App: Selfie or bib
  App->>Backend: Search once
  Backend->>Pic: POST FaceFind or GET BibTrack
  Pic-->>Backend: request_id plus preview URLs
  Backend->>Backend: Save request_id with event and user
  Guest->>App: Next page or refresh
  App->>Backend: Same request_id or bib
  Backend->>Pic: GET cached results
  Pic-->>Backend: thumbnail_url and img_url
  Backend-->>App: Previews only
  opt Partner sells originals
    Guest->>Pay: Pay (partner checkout)
    Pay-->>Backend: Payment confirmed
    Backend->>Backend: Sale confirmed in partner system
    Backend->>Pic: Download Originals
    Pic-->>Backend: Presigned original URLs
    Backend-->>App: Originals for this purchase
  end
```

## Pick the Job

Every integration follows [Keep the Key Off the Frontend](#keep-the-key-off-the-frontend) and the [API Pages](#api-pages). Then:

| You are building | Also follow |
| - | - |
| A backend or script that only calls the API | Nothing else |
| A guest-facing app: find-my-photos, gallery, selfie or bib search, timeline, clips | [Participant App](#participant-app) |
| Paid photos through your own checkout | [Participant App](#participant-app), then [Sell Originals](#sell-originals) |
| A check of an app you already built | [Review Your App](#review-your-app) |
| Photo pipeline, Flow, Certify, Buzz, 9Pic Checkout | [Out of Scope](#out-of-scope) |

## Keep the Key Off the Frontend

Anyone holding your API key can read your events, download paid original files, and start photo processing that spends your credits. It must never reach a browser, mobile app, or desktop client: not in source, not in a build, not in a response.

* Read it only in server code, from a server-only environment variable such as `NINEPIC_API_KEY`.
* Never give it a public prefix that bundlers ship to the browser: `VITE_`, `NEXT_PUBLIC_`, `REACT_APP_`, `EXPO_PUBLIC_`, `NUXT_PUBLIC_`, `PUBLIC_`, `GATSBY_`. Never import it into a client component.
* Mobile and desktop apps call your backend too. Don't put the key in app config, `Info.plist`, `BuildConfig`, or a bundled `.env`.
* Expose specific backend routes (event, search, page, download after a sale). Don't build a catch-all proxy that forwards any path to `api.9pic.ai` with the key attached; that gives anyone the key's full access.
* Never return the key, echo request headers, or log it. Keep `.env` files out of version control.

> [!WARNING]
>
> **Check before you ship.** Build your frontend, then search the build output and all client-side source for the key value, `X-API-Key`, and `api.9pic.ai`. None of them may appear. In the browser's network panel, every request should go to your own backend.

## API Pages

| When you need | Read |
| - | - |
| Create and send the key | [Authentication](https://docs.9pic.ai/api-reference/authentication.md) |
| Base URL, IDs, envelope, pagination, `request_id` | [Conventions](https://docs.9pic.ai/api-reference/conventions.md) |
| Status codes and retries | [Errors](https://docs.9pic.ai/api-reference/errors.md) |
| A first live call | [Ping](https://docs.9pic.ai/api-reference/ping.md) |
| Discover `event_id` values; optional `cover_image` | [List Events](https://docs.9pic.ai/api-reference/list-events.md) |
| Feature flags before search | [Event Details](https://docs.9pic.ai/api-reference/event-details.md) |
| Selfie search | [Face Search](https://docs.9pic.ai/api-reference/face-search.md) |
| Bib search | [BIB Search](https://docs.9pic.ai/api-reference/bib-search.md) |
| How browse, search, video, and downloads group | [Memories](https://docs.9pic.ai/api-reference/memories.md) |
| Full gallery | [All Photos](https://docs.9pic.ai/api-reference/all-photos.md) |
| One photo | [Image Details](https://docs.9pic.ai/api-reference/image-details.md) |
| Time browse | [Photo Timeline](https://docs.9pic.ai/api-reference/photo-timeline.md) |
| Clips | [Video Clipping Search](https://docs.9pic.ai/api-reference/video-clipping-search.md) |
| Unwatermarked files after a sale | [Download Originals](https://docs.9pic.ai/api-reference/download-original-photos.md) |
| Shared models | [API Models](https://docs.9pic.ai/api-reference/models.md) |
| Thumbnail, large preview, and original fields | [Image model](https://docs.9pic.ai/api-reference/models/image.md) |
| Next page | [Pagination](https://docs.9pic.ai/api-reference/models/pagination.md) |
| Tokens in the dashboard | [Developer Zone](https://docs.9pic.ai/dashboard-guide/developer-zone.md) |

> [!NOTE]
>
> Developer Zone is open on **Business**, **Custom**, **Enterprise** and during **Trial**. Pay As You Go and Pro organisations cannot create API keys, and keys made earlier are refused until the plan includes them again.

## Participant App

**1. Load the event and flags**

Call [Event Details](https://docs.9pic.ai/api-reference/event-details.md) and hide selfie, bib, video, or timeline UI when the matching flag is off. A disabled feature returns `403`; retrying will not enable it.

**2. Search once**

For a selfie, follow [Face Search](https://docs.9pic.ai/api-reference/face-search.md): the backend creates and saves the search ID (`request_id`) before the POST, the frontend puts it in the URL, and pagination and refresh GET with it. See [Save the Search ID](#save-the-search-id). For a bib, follow [BIB Search](https://docs.9pic.ai/api-reference/bib-search.md) and keep the bib in the URL. Do not start a new search on remount or page load.

**3. Render results from thumbnails**

Use the search response, or [All Photos](https://docs.9pic.ai/api-reference/all-photos.md), [Image Details](https://docs.9pic.ai/api-reference/image-details.md), and [Photo Timeline](https://docs.9pic.ai/api-reference/photo-timeline.md) for unfiltered browse. Grid cells use thumbnail URLs from the [Image model](https://docs.9pic.ai/api-reference/models/image.md).

**4. Optional clips**

Start [Video Clipping Search](https://docs.9pic.ai/api-reference/video-clipping-search.md) with the FaceFind `request_id` instead of a second selfie. Start and poll as in [Video Clips](#video-clips), and handle every status that page documents.

**5. Keep guests on previews**

Grid and lightbox use `thumbnail_url` and `img_url`. Strip `meta_info` and `original_url` on your backend. If you sell unwatermarked files after your own checkout, follow [Sell Originals](#sell-originals).

## Save the Search ID

This is the setup we recommend for every participant app. Please follow it: it keeps your API key private, keeps searches fast, and avoids running the same selfie search twice. The partner backend saves the search ID (`request_id`); the frontend carries it in the URL; every later page, refresh, share, clip and download reuses it.

| Layer | Owns | Never does |
| - | - | - |
| **Your frontend** (browser or mobile app) | Selfie or BIB input, the search ID in the URL, the photo grid and viewer | Holds the API key or calls `api.9pic.ai` directly |
| **Your backend** | The API key, every 9Pic call, saved search IDs, purchases and original downloads | Uploads the same selfie again for paging, refresh or downloads |
| **9Pic API** | Face and BIB matching, cached results, preview and original URLs | Knows your users, sessions or orders |

### Backend

Generate a UUID for each new selfie search and save it **before** you call [Face Search](https://docs.9pic.ai/api-reference/face-search.md), then send it as `request_id`. Or omit `request_id` and save the one 9Pic returns. Either way, save it before you reply to the browser.

| Save | Why |
| - | - |
| `request_id` (the search ID) | Pages, refreshes, shares, clips and downloads all use it. |
| `event_id` | A search ID belongs to one event; read it back with the same event. |
| Your user, session or order ID | Restore "my photos" later and tie a purchase to the right search. |
| BIB number, for BIB searches | Only when something later needs it, such as a sale: BIB search has no search ID, so the BIB is the identifier for results and downloads. |
| Last final clip status | So a reload after `no_matches` shows "No videos found" instead of starting clipping again. |
| When the search was created | Your own retention and clean-up rules. |

Keep these in a real database: serverless and multi-instance hosts don't share local files, so shared links break if search IDs live in a file on one server.

Don't keep the selfie image after the search. Nothing later needs it, and not storing it is better for participants' privacy.

* **Dropped connection:** read the search back with the saved ID. If it completed, you get the stored result. A `404` means it was never recorded: POST again with the same ID.
* **Failed or empty search:** a search ID keeps its first stored result. To try again, start a fresh search with a new ID.
* **New selfie:** a new selfie is a new search, so give it a new ID. Never reuse an ID across events.

Example routes on **your** backend (a pattern, not 9Pic endpoints):

| Your route | Calls on 9Pic |
| - | - |
| `GET /api/events/:eventId` | [Event Details](https://docs.9pic.ai/api-reference/event-details.md) |
| `POST /api/events/:eventId/searches` (selfie upload) | Saves a new search ID, then [Face Search](https://docs.9pic.ai/api-reference/face-search.md) POST |
| `GET /api/events/:eventId/searches/:searchId?page=N` | [Face Search](https://docs.9pic.ai/api-reference/face-search.md) GET |
| `GET /api/events/:eventId/bibs/:bib?page=N` | [BIB Search](https://docs.9pic.ai/api-reference/bib-search.md) |
| `POST /api/events/:eventId/searches/:searchId/clips` | [Video Clipping Search](https://docs.9pic.ai/api-reference/video-clipping-search.md), reusing the ID |
| `POST /api/orders/:orderId/downloads` (after payment) | [Download Original Photos](https://docs.9pic.ai/api-reference/download-original-photos.md) |

### Frontend

* **Put the search ID in the URL** after the first search, for example `/events/456/results/ddc661a7-8861-4793-9437-af42a82d12f8`. On load, if the URL has an ID, ask your backend for that search; never upload the selfie again.
* **Page with the same ID.** Infinite scroll, Load more and numbered pages all read the same search until `hasNextPage` is `false`.
* **Upload once.** Refreshes, remounts, back navigation and new pages must not trigger a new upload.
* **Choose shareable or private with the organiser.** Shareable is the default, like the 9Pic gallery: anyone with the link sees those previews, and search IDs are random UUIDs. Private: your backend serves a saved search only to the user or session that created it. Without logins, use an anonymous `httpOnly` session cookie as the user.

> [!WARNING]
>
> Uploading the selfie again on every refresh or page change runs a brand-new search each time. It is slower, repeats face matching, and breaks shared links.

## Gallery Behaviour

These are the rules the 9Pic hosted gallery follows. Build the same **behaviour**; the layout, components, framework and styling are your choice. Where a rule gives a number (page size, file limit, poll interval), use it rather than inventing one.

### State lives in the URL

* The URL holds the active view: the search type and its value (selfie search ID or bib), plus the timeline day and time window. A refresh or a shared link reopens the same view and never starts a new search.
* Starting a search adds a history entry, so Back returns to the event page. A "Back" or "Clear search" control returns to the plain event URL.
* The open photo, scroll position and loaded pages do not need to survive a refresh. A photo param such as `?image=<image_id>` is optional.

### Event first

* Read [Event Details](https://docs.9pic.ai/api-reference/event-details.md) once per page load. Show only the searches whose flags are on; clips need both `video_search` and `video_selfie_search`. If every search flag is off, show no search box, just the gallery.
* If `event_enabled` is false, show a "Photos coming soon" message instead of search and grid. The search endpoints do not block on this flag, so the app must.
* Some settings the hosted gallery follows are not available with an API key: whether downloads are allowed, whether all photos can be browsed, the photo count, the selfie capture mode, the organiser's bib format, albums, password and sale window. Agree them with the organiser; do not guess them from other fields. ([Event Settings](https://docs.9pic.ai/api-reference/event-settings.md) returns some of them, but only to an organiser admin signed in with OAuth.)
* `name_search` reports an organiser setting. The public API has no name-search endpoint, so do not build one.

### Selfie search

1. **Consent first.** Before the camera or file picker, show a short step: what makes a good selfie (one person, face to camera, good light, no sunglasses or face covering), how the selfie is used (only to find this participant's photos, per your privacy policy), and an "I understand" button. If a small selfie thumbnail stays on the device (step 4), say so instead of promising that nothing is kept.
2. **Capture.** Offer camera, file upload, or both, as the organiser prefers (the hosted gallery defaults to both). Accept JPEG and PNG only (reject HEIC and others with a clear message), one file. Respect EXIF orientation and compress in the browser (JPEG quality about 0.6, downscaling if needed) so the upload is at most 1.5 MB. Phone photos are often larger than that, so compress first and reject only a file that is still too large.
3. **Search once.** Create the search first, then upload to it, as the hosted gallery does: the browser asks your backend for a new search, the backend creates and saves the search ID and returns it, and the browser uploads the selfie for that ID. If the upload fails on a network error, or your backend answers `502`, `503`, or `504` (for example a serverless timeout), try again for the same ID, up to 3 attempts in total, 1 s apart; never retry a `429`. On serverless hosts, give the selfie route a timeout long enough for Face Search. Because every attempt carries the same ID, a retry can't run a second search. For an ID it has already sent to 9Pic, the backend reads the result back first (see [Save the Search ID](#save-the-search-id)).
4. **Show results.** On success put the ID in the URL and show "Found N photos" with the participant's own selfie thumbnail (kept on the device only) and a **Search someone else** action.
5. **Remember one search per event.** Remember the last search ID per event that found photos (on the device, or with the signed-in user). Clicking the selfie button again on that event reopens it instead of asking for a new upload. **Search someone else** forgets that search and its thumbnail and returns to the event page; the next selfie gets a new ID. A failed upload is not remembered. When a search finds nothing, show the empty state with a **Try another photo** action that starts a new search.

Empty result text: "No photos found matching your selfie. Please try another photo."

### BIB search

* While typing, accept letters, digits, `-`, `/` and spaces only. Before searching, trim and uppercase.
* If the organiser gives a bib format (a pattern, a numeric length range, or a number range), validate against it. Otherwise allow search for digits only (any length), digits followed by one letter, or at least 3 characters of letters, digits and `-`.
* Put the bib in the URL and URL-encode it in the backend path.
* Empty result text: "No photos found for bib number \{bib}. Please try another bib number." An unknown bib returns an empty list, not an error. A `404` means the event has no photo setup: show "Photos coming soon", not the empty-bib text.

### Grid and paging

* Every list (all photos, selfie, bib, timeline) uses pages of **32**. Load the next page automatically when the user scrolls within about 800 px of the end (a Load more button is fine too). Stop when the loaded count reaches `pagination.total` or `hasNextPage` is false. A new search resets the list. Drop duplicate `image_id`s.
* Browse-all: the hosted gallery shows only the first page of all photos unless the organiser turns on show-all-photos. The public All Photos and Timeline endpoints return every photo, so agree with the organiser whether to offer browsing beyond page 1.
* Grid tiles use `thumbnail_url`, lazy-loaded, with their space reserved from `width` and `height` so rows don't jump. Justified rows match the hosted gallery; any layout that keeps aspect ratios is fine.
* A tile that fails to load shows "Couldn't load photo" and a Retry for that tile only.
* Retry a failed page load (network error or `5xx`) up to 2 times with exponential backoff from 300 ms; a `400`, `401`, `403` or `404` will not change on retry. If it still fails, keep the photos already shown and offer a retry (the hosted gallery reloads the page).
* On `429`, back off as [Errors](https://docs.9pic.ai/api-reference/errors.md#429-too-many-requests) describes for background page loads. For a selfie upload, don't retry automatically; ask the participant to wait a moment and try again.

### Viewer

* Opens on tap. Shows `img_url`, never an original. Zoom (pinch, wheel, double-tap), swipe, Left/Right arrows, Escape to close. Covers the photos loaded so far.
* For selfie and bib results only, warm the loaded results' `img_url`s in the background: start after about 1 s, 2 at a time (3 on a 4G connection), skip on Save-Data or 2G, pause while the tab is hidden.

### Caching: what to keep, what not to build

* 9Pic already caches each search under its search ID. That is the cache. Do not hash selfies, invent cache keys, or store result pages in your own database; read pages through the backend with the saved ID.
* In the browser, keep only: the last search ID per event, the selfie thumbnail for that search, and optional UI preferences such as language.
* Use image URLs exactly as returned. Do not build URLs from `image_id`, change size segments, or add query strings.
* Do not store presigned download URLs (they expire after about an hour) or the API key anywhere. Clip status is read fresh while polling; only the last final status is saved (see [Video clips](#video-clips)).

### Timeline

* Day list (first day selected by default), then an hour histogram, then a time window in 15-minute steps with 15, 30, 60 and 120 minute presets. With no hour picked, the window spans the hours that have photos. Each change updates the URL.
* Timeline times are the event's **local clock time stored as if it were UTC**. Format every timestamp with `timeZone: 'UTC'` and build `from`/`to` the same way. Never convert to the viewer's time zone, or every photo shifts by the viewer's offset.
* Empty states: "No timestamped photos yet" for an event without timeline data; "No photos found for the selected time" for an empty window.

### Video clips

* Start clips with a POST carrying the saved search ID and no file. If clipping is already processing or completed, the start just returns the current status, so a refresh can safely POST again. Do not read the status first to decide whether to start, because a job that never started reads as `processing`. Save the last final status (`completed`, `no_matches`, `no_segments`, `failed`, `clip_failed`) with the saved search: after a reload, a tab switch, or a remount, a saved `no_matches` / `no_segments` shows "No videos found" without a new POST; anything else POSTs as above.
* Then poll the status every **10 s** while it is processing. A manual Refresh has a 30 s cooldown.
* Show: processing with progress, completed clips, and "No videos found" for `no_matches` / `no_segments` (don't POST again after these: another POST runs clipping again). For `failed` / `clip_failed` show an error with **Try again**, which POSTs again with the same ID; if that fails too, offer a new selfie search. Send `force_recreate=true` only for completed clips that were deleted or expired, or a job stuck in processing.
* A `409` means the event's video watermark isn't ready yet: say clips are being prepared and to try again later. A `400` on a file-less start, for a search ID your backend saved, means no video data exists for that search: show "No videos found" (as the hosted gallery does), or offer a video search with a fresh selfie upload. Don't branch on the message text.
* `video_url` is HLS. Use native playback on Safari and an HLS player elsewhere. When `purchase_required` is true (one flag on `data` for the whole response, not on each clip; the organiser sells clips), play them as watermarked previews labelled as such, and offer no download; clip sales are not available through the public API, so agree with the organiser how clips are sold. Otherwise offer `download_url` only when `download_status` is `ready`. Field details are on [Video Clipping Search](https://docs.9pic.ai/api-reference/video-clipping-search.md).

### Free downloads

* When the organiser allows free downloads, the hosted gallery offers a download button on each tile and in the viewer, and **Download all** on selfie and bib results (zipped in batches of up to 20 MB or 100 files). These are the large preview images. Agree with the organiser whether to offer them.
* Paid unwatermarked originals go through your checkout and [Sell Originals](#sell-originals), never through the gallery directly.

### Look and Feel (Optional)

* Header with the organiser's logo and theme colour; event name, date and optional banner above the search. The public API returns no logo or theme colour, so supply your own assets or skip the header.
* Search actions as a sticky bottom bar on phones; large tap targets; a full-screen viewer.
* Any of these may be replaced by your own design system.

> [!NOTE]
>
> [Branding & Theme](https://docs.9pic.ai/dashboard-guide/settings/branding.md) and [Photo Branding](https://docs.9pic.ai/dashboard-guide/photos/branding.md) show how the hosted gallery looks. They are a visual reference only.

## Sell Originals

Use this when you take payment yourself and 9Pic supplies the photos. Guests see thumbnails and watermarked images. After you confirm a sale in your system, your backend calls [Download Originals](https://docs.9pic.ai/api-reference/download-original-photos.md). Payment stays in your checkout: 9Pic does not process the card, create the order, or send the receipt.

**1. Check originals before selling**

Before you open sales for an event, call [Download Originals](https://docs.9pic.ai/api-reference/download-original-photos.md) once for a known photo (`image_id`). A `403` means original storage is off for the event; a `400` ("image\_id is invalid") for a photo you just listed means its original was not stored. Also confirm the event has a [watermark](https://docs.9pic.ai/dashboard-guide/photos/branding.md), because `img_url` carries one only when it was set up before processing.

**2. Find photos**

Build the [Participant App](#participant-app): Event Details flags, then [Face Search](https://docs.9pic.ai/api-reference/face-search.md) and/or [BIB Search](https://docs.9pic.ai/api-reference/bib-search.md). Persist bib or `request_id`.

**3. Show previews only**

Grid: `thumbnail_url`. Large view: `img_url` (watermarked when the event has a watermark). See the [Image model](https://docs.9pic.ai/api-reference/models/image.md). Do not send unwatermarked files to an unpaid guest.

**4. Take payment**

Your own system and your choice of provider. 9Pic is not involved in this step. If you want help with your checkout, ask your agent.

**5. Keep an order ID for each sale**

Recommended: store your order ID with the buyer and what it unlocks (bib, selfie `request_id`, or image ids), so your backend knows what to unlock and can re-issue download links later.

**6. Unlock originals**

Your backend calls [Download Originals](https://docs.9pic.ai/api-reference/download-original-photos.md) using the identifier on that page. Check `successful_count` and `failed_urls`: a `200` can still list missing files. Generate presigned URLs on demand; do not cache them past expiry.

## Review Your App

After you build, ask your agent to review the integration, for example: "Review my 9Pic integration against best practices." With the skill installed, the agent:

**1. Reads your code without changing it**

It runs a quick scan for the key and for catch-all proxies, then finds every 9Pic call, the backend routes behind them, and where search IDs are stored. The scan needs nothing installed and prints only file names, line numbers and variable names, never your code or key. The agent does not change your code or install packages, and it never writes your API key into the report.

**2. Runs the checks**

Each check gets Pass, Fail, or Not applicable, with the file and line it found. Features your app does not have, such as clips or selling, are marked Not applicable. Each bug is reported once, under the most specific check; real bugs outside the checklist go under Other findings. Your payment setup is not scored, but the agent can suggest general practices and will help with payment questions if you ask.

**3. Writes a report**

It saves `9pic-review.md` at the root of your repository and summarises the failed **Must have** items in chat.

**4. Offers fixes**

If your key was found in client code or a log, it tells you to rotate it in [Developer Zone](https://docs.9pic.ai/dashboard-guide/developer-zone.md) first. It asks before fixing anything. After a fix, it re-runs that check and updates the report.

When the agent builds an app with the skill, it runs the **Must have** checks on its own code before it finishes.

### Must have

Security, privacy, money, and correctness. Fix every failure here.

| ID | Check |
| - | - |
| M1 | The API key never reaches a client: not in the frontend build or source, not behind a public env prefix such as `VITE_` or `NEXT_PUBLIC_`, not in mobile config. See [Keep the Key Off the Frontend](#keep-the-key-off-the-frontend). |
| M2 | No catch-all proxy to `api.9pic.ai`; the key is never logged, echoed, or returned; `.env` files are in `.gitignore` and not tracked. A missing `.gitignore` fails. |
| M3 | Every 9Pic call matches its [API Reference](https://docs.9pic.ai/api-reference.md) page; no endpoints for name search, Flow, Certify, Buzz, or 9Pic Checkout. |
| M4 | [Event Details](https://docs.9pic.ai/api-reference/event-details.md) is read before search; search UI follows its flags; `event_enabled` false shows "Photos coming soon". |
| M5 | A selfie is POSTed once per search; refresh, paging, remount, clips, and downloads reuse the saved `request_id`, which is in the URL. |
| M6 | Search IDs are saved in a database with the event and your user or session before your backend replies to the browser. Best: generated and saved before the Face Search POST; saving the ID 9Pic returns is also valid. A JSON file, in-memory map or browser-only storage fails; SQLite is fine on a single persistent server. |
| M7 | Image objects sent to the browser carry only `image_id`, `thumbnail_url`, `img_url`, `width`, `height` (renamed is fine); `meta_info` and `original_url` are stripped until a sale. Pagination and `request_id` may pass. |
| M8 | Image URLs are used exactly as returned. |
| M9 | A consent step comes before the camera or file picker, and the selfie image is not stored after the search. |
| M10 | Timeline timestamps and `from`/`to` use `timeZone: 'UTC'`. |
| M11 | Clips start with a POST, poll no faster than every 10 s, and are not restarted after `no_matches` / `no_segments`, even after a reload or tab switch. |
| M12 | A `429` never triggers an automatic selfie upload retry. |
| M13 | When selling: originals are requested by your backend, only for what a sale your system has confirmed covers; `failed_urls` is checked; presigned URLs are not cached; a test download succeeded before sales opened. |

### Good to have

Brings your app up to the 9Pic hosted gallery. Numbers and text are in [Gallery Behaviour](#gallery-behaviour).

| ID | Check |
| - | - |
| G1 | Pages of 32, next page near the end of the list, duplicates dropped, stops at the last page |
| G2 | Selfie capture: JPEG/PNG only, EXIF orientation, compressed to at most 1.5 MB, safe retries for the same search ID |
| G3 | One remembered search per event, **Search someone else**, **Try another photo** on an empty result, and the documented empty-state text |
| G4 | BIB input filtered, trimmed, uppercased, validated, and URL-encoded |
| G5 | Per-tile retry for a photo that fails to load; page loads retried with backoff |
| G6 | Viewer with zoom, swipe, arrow keys and Escape, showing `img_url` |
| G7 | The URL holds the search and timeline window; starting a search adds a history entry; a refresh reopens the same view |
| G8 | Clip UI: progress, refresh cooldown, `409` / `400` handling, HLS playback, paid-clip previews |
| G9 | Searches are deliberately shareable or private, as agreed with the organiser |
| G10 | Phone layout: sticky search actions, large tap targets, full-screen viewer |

## Out of Scope

* Import, process, and go-live are organiser pipeline endpoints. See [Memories](https://docs.9pic.ai/api-reference/memories.md).
* 9Pic Checkout and payment-provider APIs. Payment is your own system.
* **Flow**, **Certify**, and **Buzz** have no public `/ext/` API.
* To run your 9Pic account from an assistant instead of writing code, connect the [9Pic AI MCP server](https://docs.9pic.ai/dashboard-guide/ai-assistants.md).

## Related

> **[Authentication](https://docs.9pic.ai/api-reference/authentication.md)**
>
> Create a token and send `X-API-Key`.

> **[Face Search](https://docs.9pic.ai/api-reference/face-search.md)**
>
> POST once, GET many, persist `request_id`.

> **[Download Originals](https://docs.9pic.ai/api-reference/download-original-photos.md)**
>
> Presigned unwatermarked files after a confirmed sale.

> **[Conventions](https://docs.9pic.ai/api-reference/conventions.md)**
>
> Envelope, identifiers, pagination, `request_id`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.