Skip to main content

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 page for each call. Paths, fields, and examples live there.

Install

See Agent Skills 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.

Pick the Job

Every integration follows Keep the Key Off the Frontend and the API Pages. Then:

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

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 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: 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. For a bib, follow BIB Search 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, Image Details, and Photo Timeline for unfiltered browse. Grid cells use thumbnail URLs from the Image model.
4

Optional clips

Start Video Clipping Search with the FaceFind request_id instead of a second selfie. Start and poll as in 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.

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.

Backend

Generate a UUID for each new selfie search and save it before you call Face Search, 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. 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):

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.
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.
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 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 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.
  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).
  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.”
  • 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_ids.
  • 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 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_urls 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).

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.

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, 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.
Branding & Theme and Photo Branding 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. 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 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, because img_url carries one only when it was set up before processing.
2

Find photos

Build the Participant App: Event Details flags, then Face Search and/or BIB Search. 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. 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 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 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.

Good to have

Brings your app up to the 9Pic hosted gallery. Numbers and text are in Gallery Behaviour.

Out of Scope

  • Import, process, and go-live are organiser pipeline endpoints. See Memories.
  • 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.

Authentication

Create a token and send X-API-Key.

Face Search

POST once, GET many, persist request_id.

Download Originals

Presigned unwatermarked files after a confirmed sale.

Conventions

Envelope, identifiers, pagination, request_id.