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.
Install
Call Path
The guest app never sendsX-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.aiwith the key attached; that gives anyone the key’s full access. - Never return the key, echo request headers, or log it. Keep
.envfiles out of version control.
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 asrequest_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
404means 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.
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
hasNextPageisfalse. - 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
httpOnlysession cookie as the user.
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 once per page load. Show only the searches whose flags are on; clips need both
video_searchandvideo_selfie_search. If every search flag is off, show no search box, just the gallery. - If
event_enabledis 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_searchreports an organiser setting. The public API has no name-search endpoint, so do not build one.
Selfie search
- 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.
- 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.
- 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, or504(for example a serverless timeout), try again for the same ID, up to 3 attempts in total, 1 s apart; never retry a429. 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). - 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.
- 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.
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
404means 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.totalorhasNextPageis false. A new search resets the list. Drop duplicateimage_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 fromwidthandheightso 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; a400,401,403or404will 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 buildfrom/tothe 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 savedno_matches/no_segmentsshows “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). Forfailed/clip_failedshow an error with Try again, which POSTs again with the same ID; if that fails too, offer a new selfie search. Sendforce_recreate=trueonly for completed clips that were deleted or expired, or a job stuck in processing. - A
409means the event’s video watermark isn’t ready yet: say clips are being prepared and to try again later. A400on 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_urlis HLS. Use native playback on Safari and an HLS player elsewhere. Whenpurchase_requiredis true (one flag ondatafor 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 offerdownload_urlonly whendownload_statusisready. 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.
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.
Related
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.
