# FooPixel API > Examples describe the current API contract. Check feature availability before relying on a workflow. FooPixel accepts supported media uploads and returns a public media URL after validation. See the generated facts below and [pricing and hosting limits](/pricing/) for current account policies. Hosted still images and MP4 can be normalized and need not match the original bytes. Keep source files separately. ## Current API facts (generated) Contract version: `3-edad2f47`; policy version: `2026-09-28-v2`. Check current service capabilities with [GET /v1/capabilities](https://api.foopixel.com/v1/capabilities). | Client | Maximum file | Retention | Upload attempts per UTC day | | --- | ---: | --- | ---: | | Visitor | 5 MiB | 1 day | 25 | | Free account | 20 MiB | No routine expiry for eligible verified Free accounts | 250 | | PRO account | 20 MiB | No routine expiry while PRO is active | 2,500 | Eligible verified Free accounts receive no routine expiry within their storage allowance. Visitor and unclaimed uploads expire after 24 hours. A full storage allowance blocks new admissions without deleting existing files. Read each upload's returned expiry and the signed-in account's current limits. The table shows each tier's general per-file allowance. For a newly issued still-image upload, the effective byte maximum is the smaller of that allowance and 20,000,000 bytes; visitors keep their smaller 5 MiB limit. MP4 and GIF use the tier allowance, up to 20 MiB. Read `policy.maxUploadBytes` in each new upload response and do not send more bytes than it allows. Exact idempotent replays keep the policy captured by the original session. Accepted upload MIME types: `image/gif`, `image/heic`, `image/heif`, `image/jpeg`, `image/png`, `image/webp`, `video/mp4`. Still-image policy version: `2026-10-03-v1`. Automatic optimization defaults to `on` at quality `82`. Free/visitor width: `2000px`; PRO product width cap: `none` (technical limits still apply). An account upload may request `imageProcessing.resizeToAccountLimit: true` to resize to its account limit when needed. New public IDs: 16 Base62 characters. Existing links also accept 16 URL-safe characters or 32 lowercase hexadecimal characters. Published feature settings: motion uploads **available**, credits **available**, CDN service **available**. This setting alone does not establish Free or PRO account eligibility. For a signed-in account, `GET /v1/billing/status` reports effective `cdn.enabled`, `freeCacheAvailable`, and `manualPurgeAllowed`; only active PRO can request manual clearing or saved resized images. Availability also depends on current service status, and a cache hit is not guaranteed. For the full endpoint list and request shapes, see the [OpenAPI specification](/openapi/openapi.yaml). Agents can follow the [FooPixel skill](/SKILL.md) for the supported upload, gallery, and account connection workflow. The [OpenAPI contract](/openapi/openapi.yaml) defines the complete request and response shapes. ## FooGallery Site Connect FooGallery on a WordPress site can request a FooPixel connection with `POST /v1/site-connect/requests`, `apiVersion: "2026-10-04"`, and `requestedGrant: "foogallery-site"`. The response supplies a short-lived `connectRequest.hostedUrl`. Open that exact URL in the site administrator's browser. The signed-in FooPixel Account owner sees the site's canonical hostname and installation path separately, confirms both, and approves the site grant. The site then verifies its original callback state and exchanges the one-time code with PKCE at `POST /v1/site-connect/token`. The retired `2026-09-13` and `requestedScopes` request forms are rejected with `400 site_connect_request_invalid`. See the [OpenAPI contract](/openapi/openapi.yaml) for exact fields, expiry, replay rules, and error responses. For **Try it first**, keep the guest `sessionToken` on the FooGallery server and optionally send it as `x-foopixel-session` only on that request. Send no `Origin`, `Cookie`, or `Authorization`. An invalid, expired, revoked, or linked guest proof returns `400 site_connect_request_invalid` without creating a request. Reusing an `Idempotency-Key` requires the same request and same guest proof; changing the proof returns `409 idempotency_conflict`. With no proof, the usual connection flow applies. Never place the guest token in a URL, page, redirect, callback, or log. The unsigned page waits for account selection before estimating retention. The signed-in owner sees how many trial images and other uploads will be kept, plus a list of every upload that will not fit. If the preview changes before approval or exchange, start a fresh connection request for new consent. On successful exchange, the oldest eligible live uploads that fit receive account retention and can be read with the new bearer using `GET /v1/assets/{id}`; a public image's `publicUrl` stays stable. Overflow keeps its original guest expiry and returns `404` to the Site bearer. Try it first uploads use the ordinary guest allowance: 25 attempts and 50 MiB per day, up to two outstanding uploads, measured for both the guest session and its network; the network can start at most 10 uploads per minute. A rate refusal returns `429 rate_limited` with `Retry-After` before upload bytes are accepted. Wait as directed before retrying. The exchange returns a bearer and refresh token plus `principal.grant: { "profile": "foogallery-site", "consentVersion": 2 }`. `GET /v1/site-connections/self` returns the same grant and the current `effectiveScopes`; `DELETE` on that route disconnects the site. Treat `effectiveScopes` as informational: it can gain new media capabilities without a reconnect, so ignore unknown scope names and handle `403 scope_not_allowed` for an unavailable route. The grant allows eligible media, gallery, usage, and credit-balance features. It cannot change billing or make purchases, delete the account, change members or security settings, or manage other connections. The connection remains subject to current Account owner authority and can be disconnected. The owner's Connections and Library views identify a verified site as `FooGallery · hostname/path`, so two installations on the same host can be distinguished. If a connection or Site term name is shown outside the owner views, it uses the neutral `FooGallery site` label; bearer, gallery, email, and public responses do not all contain a Site name today. A ready asset copied by a current verified Site connection can appear under that Site with the `imported_from` relation in the owner's Library; an owner's manual Site association means only membership, not a verified import or publication. Existing media eligibility and publication controls continue to apply. FooGallery asks its site administrator before spending credits. At present, Site bearers cannot start paid Category evaluation: those owner-browser routes return `403 human_action_required` and create no job or spend. If a spending route becomes available to sites, its API description will specify an administrator-confirmed positive `maxCredits` limit; a request above that limit must return `409 cost_confirmation_exceeded` without spending. Do not assume such a route is available today. ## Media delivery and browser caching Public originals, saved resized images, posters and public gallery media owned by an account can return `Cache-Control: public, max-age=N` on successful full, range and unchanged responses. Passcode gallery media can return `private, max-age=N`, which limits reuse to the receiving browser. `N` is at most 600 seconds and may be shorter when an image or account deadline is near. These responses include `Vary: Origin`. Images without an account, private and recovery downloads, claim handoffs, pending responses and errors remain `no-store`. Each new request checks current access and delivery limits before returning bytes or an unchanged response. A response may be reused without revalidation for up to 10 minutes, capped by applicable deadlines. A copy already received, rendered, or downloaded cannot be recalled. Browser reuse does not count toward delivery allowances. ## Image variants The public media route accepts query parameters to make a WebP delivery variant without changing the saved source. For example: ```text https://media.foopixel.com/a/PUBLIC_ID?width=320 https://media.foopixel.com/a/PUBLIC_ID?width=400&height=300&fit=cover&quality=82 ``` Variant delivery requires a ready, public, still JPEG/PNG/WebP source belonging to an account with active PRO CDN eligibility, resizing availability, and current access permission. An unexpired temporary image may qualify; expired, restricted, deleted, held, free-account, private, animated, GIF, HEIF/HEIC, video, and SVG sources cannot use the still-image variant route. Ready public MP4 videos have the separate poster route described below. The direct public URL without a query continues to return the original representation. | Parameter | Accepted values | Default | | --------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `width` | Positive integer, no greater than the source width or 4,096 pixels | Derived from `height` to preserve aspect ratio; otherwise the source width | | `height` | Positive integer, no greater than the source height or 4,096 pixels | Derived from `width` to preserve aspect ratio; otherwise the source height | | `fit` | `contain` or `cover`; `cover` requires both dimensions and centre-crops | `contain` (no crop or padding) | | `format` | `webp` only | `webp` | | `quality` | `70`, `82`, or `90` | `82` | The result cannot exceed 16 megapixels or enlarge the source. Query order does not change variant identity. Unknown or repeated parameters are rejected. Requests are bounded to 4,096 pixels per side and 16 megapixels, sources up to 20,000,000 bytes, outputs up to 10,000,000 bytes, at most 32 variants per source, 200 generation starts per account per hour, four concurrent generations per account, and 1 GiB of retained variants per account. A requested dimension above the source or safety limit fails; the service never silently enlarges or substitutes the original. Generation may also be limited by account or service capacity. The first eligible GET may be slow while FooPixel validates and saves a variant. A concurrent or deadline-limited request can receive a retryable pending/unavailable response. Once saved, the resized image can be reused for later requests with the same options. CDN caching can speed up subsequent delivery, although a cache hit is not guaranteed for every request. Images owned by an account may be reused by browsers for up to 10 minutes, never past an image expiry or an applicable account deadline. Passcode gallery media is reusable only in the receiving browser. Images uploaded without an account remain `no-store`. Each new request rechecks access, eligibility and delivery limits; a deleted or restricted image stops new requests, while a copy already received, rendered, or downloaded cannot be recalled. Browser reuse does not count toward delivery allowances. Variants do not extend source expiry. `HEAD` only reports an already saved variant, conditional requests use the output ETag, and Range requests are unsupported for variants. An eligible PRO owner can clear the account CDN cache from Profile, or an approved linked agent with `cdn:purge-own` and active PRO can use `POST /v1/account/cdn/purge`. Clearing the cache immediately prevents new requests from using the previous cached responses. Clearing continues in the background; use the purge status endpoint to check completion. This does not delete original or saved resized images, and eligible requests can cache them again. The media library uses 112-pixel variants for eligible PRO public still images and MP4 posters. Public gallery JSON may include `thumbnailUrls` with 320- and 640-pixel choices for eligible images and MP4 videos. Gallery media URLs check current gallery access and membership on every request; private gallery media does not receive public variant URLs. Read the returned map instead of constructing gallery URLs yourself. Image resizing is subject to availability. The `/v1/capabilities` CDN flag alone does not confirm resizing availability. Verify a known eligible resize URL before relying on this feature. ### Video poster variants A ready public MP4 belonging to an active PRO CDN account can provide a still WebP poster at `/a/{publicId}/poster`. For example: ```text https://media.foopixel.com/a/PUBLIC_ID/poster?frame=2.5&width=320 ``` `frame` selects seconds from the start of the video. It defaults to `0`, accepts a nonnegative number with up to three decimal places, and must be strictly before the video's end. Values at or beyond 60 seconds are invalid. `width`, `height`, `fit`, `format`, and `quality` use the image variant table and output bounds above. The MP4 source uses the hosted video input limit of 20 MiB. The poster cannot enlarge the video frame. Different frame times and output options count as separate variants within the same limits. The original MP4 URL remains unchanged. The first eligible GET may create the poster; later matching requests can reuse the saved result. `HEAD` only reports a saved poster. Invalid options return 400, ineligible or private videos 403 or 404, limits 429, and pending or temporarily unavailable generation 503. Range requests return 416. Poster availability depends on current PRO, CDN, access, and service eligibility. Public gallery `thumbnailUrls` provide gallery-authorized poster URLs when eligible; gallery access and membership are checked on delivery. If a poster fails to load, use the original video or a video placeholder. Some ownership and delivery settings are available only through the signed-in account interface. API clients must not attempt account-owner actions. ## Quickstart ### 1. Create an upload session Before creating an upload, reuse the approved FooPixel connection from your host's configured credential store. Refresh expired access credentials using the Authentication section below, then verify the intended account with `GET /v1/session`. Use its bearer for account requests. If account access is expected but unavailable, report the blocker; do not silently create a guest session or another connection request. For intentional guest uploads, reuse the retained guest session or create one with `POST /v1/sessions`, save its credential privately before sending bytes, and use `x-foopixel-session`. Never combine it with a delegated bearer. Agents upload the original bytes and omit `processing`; the server applies the captured account image policy. Read `GET /v1/session` for `imagePolicy` before issuing a new upload. A signed-in account owner can save `autoOptimizeImages` and `imageQuality` (integer 1–100, default 82) through `PUT /v1/account/image-settings`; members can read, while delegated credentials cannot edit settings. The settings route requires a current human browser session, and writes require owner status, trusted origin and CSRF. Free has a 2000px displayed-width limit; PRO has no product width cap. With optimization disabled, the exact submitted bytes, MIME type, checksum, dimensions and embedded metadata (including possible location data) are retained if within policy limits. Review metadata before sharing. The website uploader can preview and edit supported HEVC HEIC/HEIF still images, using native decoding first and a compatible preview fallback when needed. Saved edits are retained as a WebP derivative, or as PNG if the browser cannot encode WebP; when local conversion is unavailable, an unedited original can use server processing. Other HEIF codecs are not editor-supported. A browser that has already resized and optimized a supported image to WebP may send `processing: {"mode":"client","maxWidth":2000,"optimized":true}`. ```http POST https://api.foopixel.com/v1/uploads Content-Type: application/json Idempotency-Key: exampleUpload01 {"filename":"cover.webp","contentType":"image/webp","byteSize":184220,"checksumSha256":"<64 lowercase hex characters>"} ``` The response includes an asset identifier, a one-time upload URL, an expiry, `statusUrl`, and the completion value required below. Its `policy` records `maxUploadBytes` along with the captured `operation` (`optimize`, `preserve`, or `resize_if_needed`), `maxWidth`, `quality` when encoding applies, `outputRule`, expected `outputContentType`, `settingsRevision`, and `policyVersion`. Check `policy.maxUploadBytes` for every new upload and do not send more bytes than it allows. The tier table above shows the general file allowance (20 MiB for accounts); new still-image uploads use the smaller of that allowance and 20,000,000 bytes, while MP4 and GIF can use the tier allowance. Visitors retain their smaller 5 MiB cap. For exact idempotent replays, the original session's captured policy still applies. When `resize_if_needed` is selected, `unchangedContentType` and `resizedContentType` describe conditional output. A Free account with optimization off may send `imageProcessing: {"resizeToAccountLimit":true}` for an oversized still image. It resizes only when needed and does not edit the account preference. JPEG/PNG/WebP keep their format; HEIC/HEIF needing resize becomes WebP. No per-upload quality or arbitrary dimensions are accepted. `checksumSha256` is required and verified against the uploaded bytes. Supply an `Idempotency-Key` (8–128 letters, digits, `_`, or `-`) to replay the same live session only when the immutable request matches. New account still-image uploads have a 20,000,000-byte technical input ceiling, in addition to the account tier's 20 MiB general file allowance. Decoded images are limited to 100,000,000 pixels and 50,000 pixels on either side. WebP encoding is limited to 16,383 pixels on either side; JPEG and PNG encoding has a 50,000px edge ceiling. The 2000px Free limit is a displayed-width product rule. These technical bounds are ceilings, not a promise that every file inside them can be processed: format, source complexity, processing time and service availability may impose tighter practical limits. Local tests on 3 October 2026 accepted a 20,000,000-byte JPEG and rejected 20,000,001 bytes; decoded 100,000,000 pixels and a 50,000px edge passed, while 100,010,000 pixels and 50,001px failed. WebP encoding passed at a 16,383px edge and failed at 16,384px; JPEG and PNG encoding passed at a 50,000px edge. Actual 24MP and a lighter 48MP JPEG retained original bytes or optimized to WebP at original dimensions; a more complex 48MP JPEG encountered a temporary processing failure on repeated attempts. Results for guest and previously issued legacy sessions continue under their captured older image rules. Keep source files and handle both limit and temporary failure responses. Older uploads or batches without a verified creating connection cannot be replayed with a delegated bearer, even with the same `Idempotency-Key` and payload: creation returns `409 idempotency_conflict`, and batch item admission returns `404 not_found`. Recover the existing upload or batch status before considering a new request; do not switch keys to recreate work whose outcome is uncertain. For a multi-file batch, `POST /v1/batches/preflight` estimates current item eligibility and allowance fit; it does not reserve quota. A successful estimate can become stale before batch creation or item upload, so check each item's returned `policy.maxUploadBytes` before sending its bytes and handle the ordinary upload error response if policy or available allowance changes. Preserved HEIC/HEIF originals remain downloadable as the exact submitted bytes. When native preview fails, the website can offer a local preview for supported HEVC files up to 20 MiB, 8192px on either side and 32MP decoded area; that preview is limited to 1024px on its longest side. These are local fallback bounds, separate from the server upload and processing limits above. Preview still depends on browser, device and available resources. A preview failure does not replace or alter the original download. A local 24MP HEIC check preserved the original download checksum and showed a separate 1024px preview. Explicit editor output is a new source file; if hosted, the account's optimization preference may encode that output again. ### 2. Put the bytes directly Read the local file as raw bytes and make one `PUT` to the returned `upload.url`. Use the returned headers exactly. Do not base64-encode the file or send it through an MCP tool as data transport. The session starts within 2 minutes, receives for at most 5 minutes, and ends a stalled receive after 30 seconds. If the result is uncertain, check `statusUrl` with `x-upload-token` rather than replaying the bytes. Status and completion recovery remain available for 24 hours. Set `uploadUrl` and `uploadToken` from the create response. The `PUT` must include every returned `upload.headers` value. ```bash curl -X PUT "$uploadUrl" \ -H "Content-Type: image/webp" \ -H "x-upload-token: $uploadToken" \ --upload-file ./cover.webp ``` ### 3. Complete with the one-time token ```bash curl -X POST "https://api.foopixel.com/v1/uploads/$assetId/complete" \ -H "Content-Type: application/json" \ -d '{"uploadToken":"'$completionToken'"}' ``` The response includes an asset and its `processingStatus`. A ready status reports the actual published `contentType`, `byteSize`, `checksumSha256`, `width`, and `height`. A public asset is deliverable only after it is `ready`; a private asset's direct `/a/{publicId}` URL remains unavailable. `publicUrl` is the full-ID media address, `https://media.foopixel.com/a/{publicId}`. New media and gallery public IDs have 16 letters and digits; existing 16-character and older 32-character links continue to work. Internal asset IDs used by upload and library routes are separate. Uploaded media is validated, processed, and may be moderated before delivery. For an eligible account’s public media, including video, `shareUrl` and `foopixelShareUrl` can also return `https://foopixel.com/{slug}/a/{code}`. Gallery responses have the corresponding `https://foopixel.com/{slug}/g/{code}` share address, while gallery `url` keeps its full-ID address. The 10-character code is case-sensitive. Prefer `shareUrl` when present, keep the ID address as a backup, and handle `null` while a branded link is unavailable. Guest media shares keep the media-host ID URL. In the Library, a human owner can make a ready private item Public to share it, or make a public item Private to stop its direct public links; gallery access follows the gallery's own rules. Readiness is canonical and separate from upload recovery. Creation returns `processingStatus: "pending_upload"` and `uploadStatus: "issued"`, with `publicUrl: null`. Completion returns `asset.processingStatus` and `uploadStatus`; there is no top-level processing status or mixed `state` alias. After completion, request the returned `statusUrl` (`GET https://api.foopixel.com/v1/uploads/{assetId}`) with `x-upload-token: `. The upload token is sufficient; no bearer token is required and adding one does not change its authority. Recovery lasts 24 hours. For delegated uploads, the original connection must remain active, approved for `uploads:read`, and within its allowed idle period. Its short-lived access token may expire without ending upload-token access; revocation, required reapproval, or a change in the authorized client or account ends access. Read `processingStatus` at the top level of that status response (or `asset.processingStatus` on completion and asset envelopes). Poll no more than once per second, for at most 45 status requests. If still `pending_scan`, preserve the URL and token and resume later; report processing pending, not a public link. Stop immediately on processing statuses `rejected`, `deleted`, or `expired`, or upload statuses `failed`, `cancelled`, or `expired`; report `errorCode` when present. HTTP 403/404/410 ends polling; a 429 requires backing off rather than starting a new upload. If `errorCode` is `image_width_exceeded`, read `errorDetails.width`, `maxWidth`, and its `resizeToAccountLimit: true`, `newUploadRequired: true`, `newIdempotencyKeyRequired: true` hints. Submit the original bytes through a new upload with that option and a new idempotency key; do not replay the failed session. An unsuccessful attempt may count. `image_processing_limit_exceeded` means input/decoded/output constraints; `image_animation_unsupported` means an animated WebP or PNG was sent with `preserve` or an unresized `resize_if_needed` policy, which keeps the original, so upload the animation as GIF or MP4 instead; `image_processing_unavailable` is a temporary processing failure. Resolve uncertain status before retrying. Settings and tier changes apply to new issuance, while exact replay retains the captured policy; unissued batch items may see newer settings. The complete processing enum is `pending_upload`, `pending_scan`, `ready`, `rejected`, `deleted`, `expired`. The separate upload enum is `issued`, `receiving`, `received`, `processing`, `ready`, `rejected`, `failed`, `cancelled`, `expired`. `nextAction` is only recovery guidance: `upload`, `wait`, `complete`, `new_session`, or `done`; it does not establish readiness. Only share a non-null `publicUrl`/`shareUrl` when `processingStatus === "ready"`, `visibility === "public"`, and `assetExpiresAt` (status) or `expiresAt` (asset) is null/absent or in the future. Private ready assets keep an authenticated `privateUrl`; validated pending previews also remain private. ## Share URLs The human account owner can claim one organisation slug in [Profile](/dashboard/profile/) only while their primary FooPixel account email is verified and uses an eligible business domain. FooPixel suggests a name from that domain; the owner can choose another available, permitted slug. Personal, relay, disposable, and unverified email addresses are ineligible. Agents and other delegated clients cannot claim or rename a slug. This checks email-domain eligibility, not legal brand ownership. Slugs are unique lowercase names of 3–30 letters, digits, or single internal hyphens, starting with a letter; reserved service names are excluded. Renaming has a 30-day cooldown and requires acknowledgement that old branded links will break. Full-ID links remain available. If eligibility is lost, branded links pause and can resume after verified recovery. Connected custom domains are a later feature: they will need separate domain verification, routing, and HTTPS before the same 10-character codes can be used there. No custom-domain share URL is active now. ## Galleries and delegated access `GET /v1/assets` lists the current guest client's or connected account's media. Guest agents use `x-foopixel-session`; a delegated bearer needs `assets:read-own`. Follow `nextCursor` for older results. A gallery accepts 1–100 owned, ready images (including GIF) or MP4 videos. Create one with `POST /v1/galleries` and `name`, `visibility`, and `assetIds`. A private gallery also requires a 4–128 character passcode. Gallery visibility controls access independently of each asset's direct URL: a public gallery may display a private image without exposing its `/a/{publicId}` URL. `GET /v1/galleries` lists owned galleries; `PATCH /v1/galleries/{id}` renames and `DELETE` closes one without deleting its media. Delegated callers need `galleries:read-own` to list and `galleries:manage-own` to create or change galleries. Guest agents can use their own session token. A public gallery's eligible Free or PRO media views may use the account CDN after each request verifies the gallery and current media membership; private galleries keep passcode-bound delivery without public CDN caching. ## Account features `POST /v1/sessions` creates the current browser client cookie or returns an origin-less agent `sessionToken` for `x-foopixel-session`. `GET /v1/session` returns its actor, account link, tier, expiry, and effective `imagePolicy`; `GET /v1/account/overview` returns current usage, effective limits and credit availability in one response. Browser API requests use `credentials: "include"`. The HttpOnly guest cookie is remembered for up to a year and renewed by `POST /v1/sessions` while valid. It identifies one browser profile across refreshes and tabs; clearing site data removes that access. It does not extend the 24-hour lifetime of guest images. An agent can upload media and create a gallery before the human has an account. Keep the `sessionToken` from `POST /v1/sessions` and send it as `x-foopixel-session` on every request. If a gallery is needed, create it from ready media through `POST /v1/galleries` or the `create_gallery` MCP tool. Call `POST /v1/auth/device` or MCP `start_agent_claim` with that same session, a required agent `name`, and an optional `source`. Send only the returned `claimUrl` to the human. Keep `deviceProof` secret; `deviceId` identifies the request and is not an approval secret. Compatibility fields `verificationUri` and `verificationUriComplete` point to the same FooPixel page. `userCode` is deprecated and non-authorizing; do not ask the human to enter or compare it. The human reviews the agent identity, selected account, exact permissions, and upload adoption and overflow on FooPixel. A signed-out visitor signs in or registers and returns to that same page; a signed-in visitor can change account before approving. The request expires after ten minutes, and the current polling interval is five seconds. Start polling `POST /v1/auth/device/token` or MCP `poll_agent_claim` as soon as you send the link, using the private proof and one stable `Idempotency-Key`; honor `retryAt` in a `202 authorization_pending` response. A pending response can mean human approval is still needed or the agent poll is waiting after approval. Only a successful agent poll activates the connection and issues tokens. That poll adopts the oldest uploads that fit and returns `accountId`, `connectionId`, an access token and a rotating refresh token. Save the identifiers and refresh token securely for later sessions. Use the bearer token thereafter; `GET /v1/session` and token refresh also return the account ID. Adopted uploads receive the chosen account's retention, while overflow keeps its original guest expiry. In-flight uploads must still finish within their original deadlines before receiving that retention. A `403 device_authorization_denied` or `410 device_authorization_expired` ends the request. If account space or uploads change after review, `409 adoption_changed` means the old approval cannot finish: stop polling it, start `POST /v1/auth/device` or `start_agent_claim` again with the same guest session, send the new link for fresh human approval, and poll the new device. If an unfinished request expires or asks you to restart, create a fresh request and send its new link. Reopening an old link does not confirm the changed split. Approval does not recover expired or deleted media. Approved device connections can read their account media with `assets:read-own`, manage galleries with `galleries:read-own` and `galleries:manage-own`, and read the account credit wallet with `credits:read-own` through scoped API calls or `POST /mcp`. The combined account overview requires `usage:read` and `credits:read-own` for linked delegated callers. The overview reports whether credits are available. Existing connections need reapproval to gain scopes they lack. `GET /auth/sign-in` starts browser sign-in and is limited to 5 requests per minute and 20 per hour per IP. Complete sign-in in the same browser with cookies enabled. When that browser has live guest uploads, sign-in presents an adoption summary for the chosen account before any media moves. It shows the oldest uploads that fit and the original expiry of overflow. The human confirms that summary before returning to the validated `returnTo` location; visiting the dashboard does not approve adoption. If the uploads or account allowance change, the page requires a fresh review. Browser reads of `GET /v1/connections` require a signed-in human account; `PATCH /v1/connections/{id}`, `DELETE /v1/connections/{id}`, `POST /auth/refresh`, and `POST /auth/sign-out` also require the exact app Origin plus the `foopixel_csrf` cookie echoed as `x-csrf-token`. After a successful browser sign-out, the dashboard clears account-specific local state, returns to guest controls, and follows the returned logout URL when one is returned. Individual claims use `POST /v1/assets/{id}/claim` for verified Free or active PRO accounts. This replaces `claim-free` and the retired `claim-pending` route. The account's plan and available storage decide retention; the response's `expiresAt` reports the result. Claims require a verified human browser session and CSRF protection; unowned ready assets also require the explicitly issued handoff proof. Same-owner retries do not need the consumed proof or extend an existing expiry. Approved session adoption takes the oldest live uploads that fit the account, with overflow retaining its original expiry; an individual asset claim uses its own proof. Eligible public media owned by a usable verified Free or active PRO account can use CDN delivery. CDN eligibility does not change the original URL or its expiry; successful claims and approved adoption apply the account retention described above. Direct private media URLs and passcode-protected gallery responses do not receive public CDN delivery. An eligible private original may use CDN through an open public gallery’s media URL after each new request checks current gallery access, owner, and membership; its direct private URL remains restricted. Hits and misses count toward the same delivery allowance. Manual purge and saved resized variants remain PRO-only. The account owner can clear its cache in Profile; approved agents with `cdn:purge-own` can clear only their linked account's cache through the API or MCP. ## Choose how a human keeps media Decide by what the human wants, not by which flow is simpler for you: - **They will keep using you with FooPixel:** use device approval (`POST /v1/auth/device`, described above). The human reviews the agent identity, selected account, requested permissions and upload adoption summary on FooPixel, then signs in if needed and approves. Do not ask for a verification code. Upload adoption and ongoing access complete only after your private poll succeeds. Overflow keeps its original guest expiry. In-flight uploads must finish within their original deadlines before receiving approved retention. Uploads you make after linking follow their plan's retention. - **They want to keep particular items without giving you account access:** issue a claim link for each ready item (below) and send it only to them. Each link is one sign-in and one choice per item, so for more than a few items suggest device claim instead. - **Someone else should own an item:** create a separate guest session with `POST /v1/sessions` before uploading that item, upload it there, and send its claim link only to that person. If it shares a session with your operator's media, its adoption by another account stops that session from ever linking to your operator's account (`claim_attribution_conflict`). - **You can't tell:** ask, for example "Do you want me connected to your FooPixel account for future uploads, or just to keep these items?" ## Explicit single-item handoff Use this when the choice above calls for a claim link. Wait for `processingStatus: "ready"`, then call `POST /v1/assets/{id}/claim-link` with the owning active, unlinked guest `x-foopixel-session` and a stable `Idempotency-Key`. A guest browser can use its HttpOnly cookie with an explicit first-party Origin. Upload tokens and delegated bearer tokens do not authorize issuance. A 409 `asset_not_ready` means the item is still processing: keep polling its `statusUrl` and retry once `processingStatus` is `ready`. A 409 `asset_not_claimable` is final for that session (another session, already claimed, rejected, expired, or deleted). A 409 `claim_link_superseded` means a request with a newer `Idempotency-Key` replaced that issuance. Upload creation, completion, status and asset responses contain no claim URL or token. The no-store response contains `{ "claimUrl": "https://foopixel.com/claim/ast_…#token=clm_…", "expiresAt": "…" }`. Send that sensitive fragment link only to the intended human; keep it out of logs, public metadata, galleries and shared transcripts. It expires at the earlier of 24 hours or the asset expiry. The same idempotency key retries the same issuance; a new key explicitly reissues and invalidates the unused old link. Redemption is single-use and requires the verified human owner to sign in and choose **Keep this upload**. `POST /v1/assets/{id}/claim` applies the account's current plan and storage allowance: eligible verified Free accounts have no routine expiry. Active PRO accounts have no routine expiry under their plan. The response reports the actual `expiresAt` (or `null`). Preview does not consume the link. Obsolete upload-time proofs and pending-media token adoption are rejected. Once adopted, the same owner may retry without the consumed token. Existing attribution fences remain: a session containing media adopted by one account cannot later migrate to another account. Single-item adoption does not authorize ongoing agent access or migrate other media or galleries. Browser sign-in and device approval show the chosen account's fit and overflow before adopting live session uploads. Adopted uploads receive account retention; overflow keeps its original expiry. Expired or deleted media cannot be recovered. Ownership changes invalidate outstanding item links. ## Media organization and Library query The signed-in human owner can organize eligible account media through five groups: Agents, Sites, Clients, Projects, and Categories. These relationships make Library views; they do not grant access, change visibility or retention, or claim an item. Client membership includes an asset assigned directly to that Client and one assigned to a Project whose parent is that Client. A Project can have one Client parent. IDs remain stable when a name changes, and a disconnected Agent or Site can still describe an existing relationship. An archived term remains on existing media but cannot receive a new assignment. Each account starts with nine independent Categories. The owner can create, rename, define, exclude, add aliases to, archive and restore Categories; Category IDs stay stable and archived memberships remain on existing media. The API also creates and edits Clients and Projects and lets owners manually assign active Categories, Clients, Projects, and Sites. Owners can associate eligible media with a Site even when it is disconnected; this means Associated with this site, not Published here or imported from it. Verified Agent origins, Site imports, and Site publications remain read-only. New account uploads may include allowed organization IDs when the caller has edit permission; this does not record a new Agent origin. The filters can read relationships that already exist, including site import and publication relations; they do not automatically import from or publish to WordPress or assign AI categories without review. Owner browsers can use these routes. Connected native agents can request `media:search-own` for account-wide eligible media reads, Category, Client, and Project filters, taxonomy and term pickers, individual term details, organization reads, and editorial metadata/history reads. Agent and Site taxonomy names, terms, memberships, facets, and organization-name text matches stay visible only to the owner. Term impact previews remain owner browser actions. `media:metadata-edit-own` permits editorial edits and explicit Category, Client, and Project assignments; it does not permit creating or editing terms, changing Agent origins or Site associations, or submitting AI reviews. Native-agent permissions require explicit human approval and are not added to existing native connections by token refresh. A FooGallery Site bearer with these scopes follows the same Category, Client, and Project visibility and edit restrictions; its site grant is approved as a whole. Browser writes require `Origin: https://foopixel.com`, JSON content type, and `x-csrf-token` matching the browser's CSRF cookie; browser queries require Origin and JSON without CSRF. Bearer requests use the current connection permissions. Guest sessions cannot use these routes. An account upload or batch item may include `organization: { "projectIds": [], "clientIds": [], "categoryIds": [] }`. Each array accepts at most 100 active IDs, with 100 distinct IDs across the object; Client and Project IDs must belong to the account. A native agent needs both `uploads:create` and `media:metadata-edit-own` to include this field. The choice is attached to that upload and cannot be changed on an idempotent retry. `organizationWarnings` reports a skipped ID as `term_unavailable` only when that term is actually missing, archived, or otherwise unavailable. A valid ID skipped because the original delegated permission is no longer available may have no warning; the upload can still become ready. Omit `organization` to keep the existing upload workflow. Start with `GET /v1/media-taxonomies` for the visible taxonomy IDs: all five for owner browsers, or Categories, Clients, and Projects for connected native agents. `GET /v1/media-taxonomies/{taxonomyId}/terms` searches term names with `q` (up to 200 characters), `status=active|archived|all` (default `active`), and a Projects-only `parentId` Client filter. Pages default to 50 terms and accept `limit` 1–100 and the returned `nextCursor`. A picker cursor is tied to its taxonomy revision and options; on `409 media_query_changed`, restart at the first page. `GET /v1/media-terms/{id}` gives one visible term and an `ETag`; unavailable, Agent, and Site IDs all return `404 resource_not_found` for native agents. `GET /v1/media-terms/{id}/preview` is owner-only and estimates a Client or Project change. For Projects, `parentId` previews another active Client, and an empty value previews removing the parent. A Client preview lists up to 100 active Projects plus `moreProjects`. `affectedAssets: null` with `countsUnavailable: true` means an exact impact count could not be supplied; do not treat it as zero. AI analysis can pick Categories in the same request that suggests text. Create the consent with `modalities.includeCategories: true` and send `includeCategories: true` with an `image` or `sampled_frames` analysis. Up to 100 of your active Categories, most used first, are sent with the media samples as names, definitions and exclusions; there is no extra cost. When the run completes, `GET /v1/assets/{id}/metadata` lists unreviewed picks in `activeAnalysis.visual.result.categories` as `termId`, `name` and `confidence` from 0 to 1. Accept or reject them with `POST /v1/assets/{id}/metadata/category-reviews`, sending `runId`, up to 20 `decisions` of `termId` and `action` (`accept` or `reject`), and the organization `ETag` in `If-Match`. Accepted picks become Category memberships recorded as accepted AI suggestions; the response is the updated organization. A newer analysis replaces the run, so earlier picks return `409 analysis_input_changed`. If the Category catalogue changes, a new analysis is run rather than reusing an earlier result. `GET /v1/media-tags` lists the tags already used on your ready media, most used first, as `name` and `assetCount`, so you can reuse an existing tag instead of adding a near-duplicate. Tags that differ only in letter case are combined. Optional `q` (up to 80 characters) matches part of a tag, and `limit` is 1–500 (default 500). `truncated: true` means more tags exist than were returned; narrow with `q`. Change an item's tags with `PATCH /v1/assets/{id}/metadata`. In the Sites taxonomy, `POST /v1/media-taxonomies/{taxonomyId}/terms` with only `name` adds a Site you have not connected, such as a website you publish to by hand. Send a domain or URL; it is saved as the lowercase hostname without `www.`, port or path, and anything else returns `400 site_domain_invalid`. Adding a domain that already exists returns that Site, including a connected Site for the same domain. If a WordPress site with that domain connects later, its media links move to the connected Site shortly after it connects, and the added Site is archived. Agent terms cannot be created. `POST /v1/media-taxonomies/{taxonomyId}/terms` creates a Category, Client or Project with a required `name` (1–200 characters) and optional `description` (up to 4,000), `aliases` (up to 20 strings of 80 characters), `metadata` (up to 4 KiB), and Project `parentId`. Metadata accepts only `colorToken` (lowercase letters, digits and hyphens, up to 40 characters) and `externalReference` (up to 256 characters). For Categories, provide a required nonempty `description` and optional `exclusions` (each up to 4,000 characters), `aliases` (up to 20 strings of 80 characters), and `name`; omit Project `parentId` and metadata. `PATCH /v1/media-terms/{id}` can edit these fields or set `status` to `active` or `archived`. `GET /v1/account/overview` reports `categories.limits` and `categories.usage`: Free permits 30 active, 100 total and 12 KiB of canonical active definitions; PRO permits 3,000 active, 10,000 total and 1,200 KiB. A downgrade retains existing Categories and memberships but permits only edits that do not increase an exceeded allowance. An over-limit catalogue cannot use AI evaluation for any Category until it is within current allowances. Reload your account’s term IDs when updating a client: older shared Category IDs cannot be used for filters or assignments. Remove `definitionVersion` from requests because it no longer identifies the Category catalogue or appears in responses. Category allowance errors are `422 category_limit_exceeded` and `422 category_definitions_too_large`. Send the exact term `ETag` in `If-Match`: missing is `428 if_match_required`, and stale is `412 term_revision_conflict`. Reread before retrying. A Client with active Projects returns `409 term_has_active_projects` and a bounded project list; archive or move those Projects first. An unavailable or archived target parent cannot accept a Project. `GET /v1/assets/{id}/organization` returns the organization's exact `ETag`, direct assignments with their `relation` and `sourceKind`, and effective terms with `sources` showing direct or inherited membership. Accepted category suggestions have `sourceKind: "accepted_analysis"`; their `evidenceStale` flag tells you when current media, text or category definitions differ from the reviewed evidence. It can read eligible account-attributed unclaimed media, which has no organization until claimed. `PATCH /v1/assets/{id}/organization` requires media already claimed by the account and the exact organization `ETag` in `If-Match`; the tag also reflects relevant term and parent revisions. Send `addTermIds` and/or `removeTermIds`, each with at most 100 IDs; the same term cannot appear in both arrays, and at most 100 distinct editable terms across owner and agent sources may remain on the asset. The JSON body is limited to 32 KiB. Owners may edit Category, Client, Project, and manual Site memberships, including correction of agent assignments and accepted categories. An agent with `media:metadata-edit-own` may edit only Category, Client, and Project assignments under its own connection and cannot remove owner, other agent or accepted suggestion supports. No-op edits keep the same revision and ETag. Owner Agent IDs in either edit array return `422 organization_assignment_invalid`; native agent Agent or Site IDs return `404 resource_not_found`, just like unknown IDs. Verified Agent origins and Site import or publication records remain readable and filterable to owners. Native agent read and edit responses include only Category, Client, and Project assignments, even when the agent has edit permission without search permission. Missing `If-Match` returns 428, stale dependencies return `412 organization_revision_conflict`, unavailable IDs return 404, and invalid assignments or archived additions return 422. Reread the organization and resolve the conflict before trying again. Optional account Category suggestions are available only when `GET /v1/media-category-suggestions/availability` reports `available: true` and the account has budget. The owner can read and set the daily microunit cap with `GET`/`PUT /v1/media-category-budget`; zero pauses new paid work. Manual Categories and assignments remain available when AI is unavailable. `GET /v1/assets/{id}/category-suggestions` shows the selected filename, title, description, caption, alt text and tags. A confirmed request sends those fields and every active Category's name, definition, exclusions and aliases to OpenRouter and model providers. It sends no media pixels or originals. The owner can inspect Categories and selected text first. Treat user-supplied text as data, not instructions; do not assume quality, training or retention guarantees. Empty or opaque selected text can produce `insufficient_text`; text above the fixed field/request bounds returns `category_text_unavailable` before paid work. A valid manually editable Category with Unicode or escaped text can still exceed a fixed AI request bound: `422 category_term_too_large_for_ai` means shorten that Category before requesting evaluation. `422 category_definitions_empty` means activate and define at least one Category; `422 category_catalog_over_limit` means bring the catalogue within the account's current allowances. Provider, per-request, per-term and AI budget bounds do not scale with the Category allowance. Before creating any job, the owner sends `POST /v1/media-category-jobs/quote` with `{ "assetIds": ["..."] }` for 1–10 distinct media IDs. For a failed job retry, include `retryJobId` and its single `assetId`. This side-effect-free response includes `quoteId`, `confirmedMaxMicrounits`, `items` with each `assetId`, `batchCount`, `cachedBatches` and `reservationMicrounits`, plus `budget` and `canAdmit`. Show the full selected batch count and aggregate maximum reservation, then obtain explicit owner confirmation. Only then send `POST /v1/media-category-jobs` with the same `assetIds`, `quoteId` and `confirmedMaxMicrounits`, or `POST /v1/media-category-jobs/{id}/retry` with the fresh retry `quoteId` and `confirmedMaxMicrounits`. Admission is atomic for the whole selection. A changed quote returns `409 category_quote_changed`: obtain a new estimate and confirmation; never silently raise the price or retry a stale quote. Insufficient whole-selection budget returns `409 category_budget_or_asset_unavailable`. Account-attributed unclaimed media can be evaluated, but only directly claimed media can receive accepted Category memberships. A previous-day queued job may be superseded; explicitly quote and confirm new work. Provider failures are not automatically retried. Reuse requires matching media representation, selected text, Category definitions, requested model, question and profile; changes require new evaluation. Each job reports `batchesCompleted` and `batchesTotal`; queued or running can include final publication time, and partially completed batches are not reviewable. Jobs also report reservation, actual or unknown spend, requested and actual model, `evaluationStatus`, and possibly `evaluationId`; `provisional, not evaluated` makes no quality claim. Measured costs may exceed the reservation, limiting later work. A confirmed job may show known measured cost, an unknown charge after uncertain submission, or a released reservation when no provider work was needed. `GET /v1/assets/{id}/category-suggestions` returns up to 60 judgments per page, plus `nextCursor`, the exact review `etag`, current `selectedText`, `claimed` and recent `history`. Continue with `?cursor=...` to review later pages; `409 category_cursor_stale` means restart at page one and recheck evidence. Probabilities and evidence sufficiency are separate and overlapping Categories are possible. To accept or reject one unreviewed judgment, send `{ "decision": "accept" }` to `POST /v1/assets/{id}/category-suggestions/{judgmentId}/review` with the exact `If-Match` ETag, or use `reject`. Only acceptance creates a Category membership; a stale accepted membership remains visible until corrected. A missing ETag returns 428, a changed ETag returns 412 and changed evidence or an unreviewable job returns 409. Reread before a new decision; never automatically accept. Connected agents and guest sessions cannot create, cancel, retry or review AI jobs. For several assets, `POST /v1/media-organization/batch` accepts 1–100 distinct `items`, each `{ "assetId": "...", "etag": "...", "operation": { "addTermIds": [], "removeTermIds": [] } }`, with a 1 MiB JSON body and `Idempotency-Key`. Each item follows the same Categories, Clients, Projects, and Sites edit rules; owner Agent IDs return per-item `422 organization_assignment_invalid`, while native agent Agent or Site IDs return per-item `404 resource_not_found`. Batch outcomes and replays contain no term details. Each item succeeds or fails atomically; the response has `items` with per-item `status` and either new `etag`/`revision` or `error`, plus `replayed`. The same actor and exact request can retry the same key for 24 hours. Changing actor or body returns `409 idempotency_conflict`; a concurrent retry may return `409 idempotency_in_progress`, so retry the same key after it completes. Saved outcomes are returned only while the owner still holds each asset. An invalidated replay returns `replayed: true`, `invalidated: true` and each item as `404 resource_not_found` or `409 idempotency_invalidated`, without the old ETag. Refresh affected items and use a new key only for eligible failed work. Owner edits have private history retained for 30 days. `POST /v1/media-query` accepts owner filters `filters.agentIds`, `siteIds`, `clientIds`, `projectIds`, and `categoryIds` (at most 20 IDs in each, 50 distinct IDs total). IDs within a group match any; groups combine with AND. `filters.unassignedTypes` accepts `agent`, `site`, `client`, `project`, or `category`, with `common` also accepted as an alias for `category` (the `common` taxonomy type denotes the account-owned Category group); a group cannot be both selected and unassigned. `filters.siteRelation` can narrow owner Site membership to `member`, `imported_from`, or `published_to`. A native agent may use only `clientIds`, `projectIds`, and `categoryIds`, their corresponding unassigned types, and Client, Project, or Category facets. Agent or Site filters, unassigned types, facets, or `siteRelation` return `403 scope_not_allowed`. Agent and Site names alone cannot match a native agent text query; shared editorial and observation text remains searchable. Optional `q` is up to 500 characters and searches up to ten Unicode words of at least two characters, `sort` is `recent` or `oldest`, and `limit` is 1–100 (default 50). Send `cursor` from `nextCursor` for the next page. The cursor uses the current `createdAt`/ID order and remains valid through uploads, asset changes, assignment changes, and catalog or projection updates. It is invalidated if the account, read permission, filters, sort, or taxonomy definitions change; `409 media_query_changed` means restart from page one. A newly connected Agent or Site does not invalidate it; among Agent and Site terms, only changes to terms named in the filters do. The response still returns the current numeric `catalogRevision`, which does not determine cursor validity. If an asset's `createdAt` changes during paging, its position can move in the order. The result includes eligible ready `assets`, each item's `ownership` (`claimed` or `anonymous`) and `memberships`, `nextCursor`, `indexingPending`, and `catalogRevision`. Attributed unclaimed media can appear without memberships and in unassigned views. An item's membership display may be truncated after 150 visible direct entries; filtering still uses its full relationships. Text indexing can still be pending when the returned page is empty. Request `totals: true` for distinct matching `assets` and `bytes`, and `facets` for counts by selected group. Facets return up to 100 terms per group with `facetMore`. Exact counts may be omitted with `countsUnavailable: true`; continue paging media without assuming missing counts are zero. ## Routes - `POST /v1/uploads` — create an upload session (visitor: 5 MiB maximum; Free/PRO: 20 MiB; JPEG/PNG/WebP/HEIC/HEIF and supported MP4/GIF). - `GET /v1/uploads/{id}` — authenticated upload status; send `x-upload-token`. Readiness is `processingStatus`; `uploadStatus` and `nextAction` (`upload`, `wait`, `complete`, `new_session`, or `done`) are recovery guidance only. - `PUT /v1/uploads/{id}/content` — one raw upload write; send the returned `x-upload-token` and content type. - `POST /v1/uploads/{id}/complete` — validate `{ "uploadToken": "..." }` and complete the uploaded bytes. - `POST /v1/uploads/{id}/cancel` — stop an issued session with the same upload token. - `POST /v1/auth/device` — begin human-approved account authorization for a FooPixel client session. - `GET /v1/auth/device/{deviceId}` — read the no-store approval review used by the FooPixel page; it never approves the request. - `POST /v1/auth/device/{deviceId}/approve` — record the signed-in human's explicit approval for the reviewed account, scopes, and upload snapshot. The agent must still complete its private-proof poll. - `POST /v1/auth/device/{deviceId}/change-account` — clear the pending account selection and return the human to sign-in to choose another account. - `POST /v1/auth/device/token` — poll with `{ "deviceId", "deviceProof" }` and `Idempotency-Key`; respect the returned interval and `202 authorization_pending`. - `GET /auth/device/{deviceId}`, `/claim/{assetId}`, `/auth/session-adoption/{adoptionId}`, `/connect/{requestId}`, and `/auth/error` — human-facing FooPixel pages. Sign-in and callback routes remain API-host redirect plumbing; they do not grant consent by themselves. - `POST /v1/auth/token/refresh` — rotate `{ "refreshToken" }` with `Idempotency-Key`. - `GET /auth/sign-in`, `GET /auth/callback`, `POST /auth/refresh`, and `POST /auth/sign-out` — browser sign-in, callback, renewal, and linked-client revocation. - `GET /v1/connections`, `PATCH /v1/connections/{id}`, and `DELETE /v1/connections/{id}` — human browser connection controls. Rename accepts `{ "name": "..." }` for active or pending connections; a pending rename carries through when the agent finishes polling. Recognized active connections include `mediaView: { "type": "agent" | "site", "termId": "..." }` for an owner Library filter. Pending requests and connections without a verified view omit `mediaView`. - `GET /v1/assets/{id}` — account asset metadata for the matching human browser or a delegated bearer with `assets:read-own`. Rejected, deleted, expired, or withheld media returns 404; read a rejected upload's `errorCode` from its upload `statusUrl`. - `GET /v1/assets?limit=50&cursor=...` — current guest browser or signed-in account media library. Follow `nextCursor` until null to view older media. Guest browser access requires its own client cookie; originless guest agents use their own `x-foopixel-session`. Matching an IP address never grants access. - `GET /v1/media-taxonomies`, `GET /v1/media-taxonomies/{taxonomyId}/terms`, `GET /v1/media-terms/{id}`, `GET /v1/media-tags`, `GET /v1/assets/{id}/organization`, and `POST /v1/media-query` — owner browser or connected agent with `media:search-own`; term creation/editing and impact previews remain owner browser actions. - `PATCH /v1/assets/{id}/organization` and `POST /v1/media-organization/batch` — owner browser or connected agent with `media:metadata-edit-own`, subject to source-specific editing rules. See [Media organization and Library query](#media-organization-and-library-query). - `POST /v1/assets/{id}/claim-link` — explicit guest single-item handoff. - `POST /v1/assets/{id}/claim` — verified human browser claim; the account plan decides retention. - `GET` / `HEAD /a/{publicId}` — stream or inspect public media. - `GET /v1/account/brand` and `POST` / `PATCH /v1/account/brand` — human owner eligibility, availability, claim, and rename. - `GET` / `POST /v1/account/cdn/purge` — linked account purge status or a scoped purge request; delegated callers need `cdn:purge-own`. - `GET` / `POST /v1/galleries`, `PATCH` / `DELETE /v1/galleries/{id}` — owned gallery management. - `GET /v1/credits` and `GET /v1/account/overview` — wallet and combined account usage, limits, and credit availability. - `POST /mcp` — scoped gallery, credit, and account tools; media bytes still use the direct upload route. The machine-readable contract is [`/openapi/openapi.yaml`](/openapi/openapi.yaml). ## Authentication Delegated agents use `Authorization: Bearer ` for upload, session, and usage operations. The account association and connection IDs persist, while access tokens last 15 minutes; a connection is idle-expired after 30 days and must be reauthorized after 90 days. Refresh tokens rotate, and an exact retry may replay the same response for 60 seconds; using the preceding refresh token with another idempotency key is rejected. A device start is limited to 5 per minute and 20 per hour for a shared IP. A device flow returns `claimUrl`, `deviceId`, private `deviceProof`, deprecated `userCode`, compatibility URLs `verificationUri` and optional `verificationUriComplete`, expiry, and poll interval. Give the human only `claimUrl`; the compatibility URLs point to the same FooPixel review page. The code is a non-authorizing reference and is never required for entry or comparison. The request currently lasts ten minutes and the poll interval is five seconds. Never log private proofs or tokens. A delegated bearer is never a human account-control credential. ## Errors Most existing API errors are nested under `error` and include `code`, `message`, and `requestId`: Upload requests with a disabled format return HTTP 400 and `audio_uploads_disabled` before an asset or upload session is created. Batch preflight reports the same code as an item reason; creating a batch containing that format fails atomically with HTTP 400. Invalid account image-setting input returns HTTP 400 and should be corrected. A temporary settings-service failure returns HTTP 503 `image_settings_unavailable`; retry the same owner `PUT` later, or retry a failed settings read before issuing uploads. ```json { "error": { "code": "invalid_request", "message": "The request could not be completed.", "requestId": "req_01JX8W9W" } } ``` The media organization and query routes above return a flat error code, for example `{ "error": "media_query_changed" }`. Some term errors also include `details`, such as the active Projects blocking a Client archive. Use the HTTP status and code together when deciding whether to fix the request, refresh an ETag or cursor, or retry an idempotent batch. ## Availability and media notes The account dashboard supports media, usage and connected-agent controls. Free and PRO allowances are defined in the service policy. The [Profile page](/dashboard/profile/) shows the current checkout availability; paid features require checkout availability and an eligible account. Check the live service for current availability. The automatic WordPress connector remains planned. FooPixel should not be used as the only copy of important media. `GET /v1/assets` lists uploads belonging to the current guest browser or guest agent session; after approved session adoption it lists account media, including uploads from other signed-in browsers. Adopted uploads receive the account's retention when eligible; overflow remains guest media with its original expiry. A linked browser needs its matching human session for account-library reads. A delegated bearer with `assets:read-own` can read its connected account's library and individual asset metadata; an upload token alone grants only private upload-status recovery, not account-library access. Visitor still images retain their existing 2000px width and legacy processing behavior. Newly issued account still images use the captured policy and the account technical bounds described above. The website sends ordinary signed-in originals to the server; explicit editor output is a new selected source. File-size limits and processing availability still apply. MP4 and GIF uploads omit `processing` and use the same upload session, byte checksum and completion workflow. Motion inputs must be at most 60 seconds and 2000 pixels per side within the account byte limit. MP4 is re-encoded for hosting; GIF passes format and frame validation. Video and GIF uploads undergo visual moderation, which does not inspect every frame or audio. If video and GIF hosting is unavailable, the service returns `503 media_hosting_unavailable` before accepting bytes. See the [OpenAPI contract](/openapi/openapi.yaml) for request details. For local preparation before uploading, use the [free browser tools](/tools/). Local editing and downloads do not require an account. The landing page and dashboard accept multiple files. Upload work can run concurrently up to the current account's available receiving slots; PRO allows more than guest or Free when capacity is available. A failed file can be retried; uncertain server status pauses the queue until checked. Keep the tab open for waiting files. Uploaded media remains in the browser’s guest Library after refresh or in another tab; signing in provides access across browsers. ## Agent documentation freshness Install the agent skill from [the canonical FooPixel skill](/SKILL.md) through your host's trusted skill installation mechanism. Account linking and granted scopes continue independently. Instruction refresh loads verified Markdown and data, never downloaded executable code. The optional portable helper is `scripts/agent-docs-refresh.mjs` in the FooPixel repository; an operator may install that reviewed file through their ordinary trusted mechanism. It uses Node 22.13 or later, built-in modules only, and needs no repository dependencies. Hosts receiving only Markdown can implement this same protocol with HTTP and persistent storage tools. Before starting a FooPixel workflow, reuse verified instructions for seven days (604800 seconds), then check [the public manifest](https://foopixel.com/agent-manifest.json) once. A workflow includes the entire upload batch, gallery work and recovery polling, even when resumed or when seven days pass mid-batch. Share one opaque 8–128 character workflow ID containing letters, digits, `_` or `-` across all workers; never issue IDs per image or to bypass a failed refresh. A 100-image batch makes no extra documentation checks for its files or polls. Read the verified skill at the start of each conversation even if no network request is needed. ```sh node scripts/agent-docs-refresh.mjs refresh --workflow-id exampleBatch01 --reason scheduled node scripts/agent-docs-refresh.mjs document --workflow-id exampleBatch01 --document apiGuide node scripts/agent-docs-refresh.mjs document --workflow-id exampleBatch01 --document openapi # Only after reconciling all in-flight requests at a safe boundary: node scripts/agent-docs-refresh.mjs refresh --workflow-id exampleBatch01 --reason compatibility --allow-repin # Only once the whole workflow and its recovery are finished: node scripts/agent-docs-refresh.mjs finish --workflow-id exampleBatch01 ``` All commands accept `--cache-dir`. Refresh also accepts `--reason feature` for a user-requested feature absent from cached guidance. The helper returns JSON with the workflow pin, status, paths and successful-check time; read `skillPath` before continuing. `document` accepts `skill`, `apiGuide` or `openapi`; read its `documentPath`. Exit 0 means verified instructions are usable (`cached`, `unchanged`, `updated`, or disclosed `stale`) or the document/finish command succeeded. Exit 2 means the affected operation is blocked. `finish` removes only that workflow record and retains document bundles. Lost workflow records require reconciling in-flight operations before starting another workflow. Fetch public documentation with direct HTTP, no credentials, search indexing or browser history. The manifest request uses `Cache-Control: no-cache`, a ten-second deadline including body streaming, and a 32 KiB body cap; do not add random cache-busting queries. Accept only schema version 1 and refresh protocol 1, the exact weekly interval, positive integer document byte sizes up to 2 MiB, and canonical HTTPS URLs under `https://foopixel.com/agent-docs//`, with no query, fragment, encoded path escape, credentials or redirects. Production manifests include `productVersion` and `changelogUrl`; validate them when present, tolerate their absence in schema-v1 clients, and ignore unknown fields. Public product metadata does not establish API compatibility. Verify each downloaded file's byte length and SHA-256. Recompute the bundle digest over UTF-8 `JSON.stringify([["instructions.md", skillHash], ["docs.md", guideHash], ["openapi.yaml", openapiHash]])`, in exactly that order without a trailing newline; `documentationVersion` must be `sha256:` followed by the digest. Reject unsupported versions, unsafe URLs, invalid hashes or oversized bodies. The API facts `apiContractVersion` is separate from the documentation version; a prose change alone does not mean old clients are incompatible. Download only the skill during refresh; load the guide and OpenAPI lazily through the workflow's manifest descriptors. Operational links in immutable `instructions.md`, `docs.md` and `openapi.yaml` are siblings. Resolve them through the pinned manifest when reading from HTTP or disk, including through the helper's explicit `document` command; a missing sibling never licenses fetching a mutable current alias. Other site links are absolute. A missing/corrupt pinned document blocks that operation with `snapshot_unavailable`; reconcile requests and consume the workflow's one forced refresh, or stop if already consumed. Only while choosing an unpinned candidate may a missing snapshot/hash mismatch cause one manifest refetch and one corrected snapshot attempt; then stop on failure. Store public state atomically, scoped by documentation origin, API origin and refresh protocol, separately from account credentials. Defaults are `$XDG_CACHE_HOME/foopixel/agent-docs//` or `~/.cache/foopixel/agent-docs//`; origin-key is SHA-256 of the normalized documentation origin, API origin and decimal protocol version joined by newlines without a trailing newline. Only operator-configured origins may override production. Persist the validated manifest, last successful check, last attempt and next automatic attempt, document bundles by version and workflow pins. Future check times are due. Validate cached bytes before using them. Concurrent callers share an ownership-token/PID lock and atomic publication; first-use/forced callers wait at most 30 seconds. Reclaim locks older than 60 seconds only if the owner is confirmed dead, never solely by age. An unchanged manifest advances the successful-check time and product display metadata without document downloads. A scheduled failure retains a verified older skill, reports `stale`, preserves the successful-check time and schedules one-hour backoff shared across workers. There is no maximum stale age; continue supported operations without assuming new capabilities. With no verified skill, block new operations. Unsupported schema/protocol requires updating the installed procedure/helper through the host's trusted installation mechanism. If public HTTP and a verified copy are both unavailable, report that limitation. Without persistent storage, keep the pin in conversation and check once per new conversation; still make no per-image checks. ### Compatibility and safe recovery HTTP 409 `client_update_required` with `recovery.action: "refresh_agent_instructions"`, allowlisted `manifestUrl`, reason `operation_retired` or `request_contract_changed`, and `requestApplied: false` permits one shared forced refresh for the workflow. `GET /v1/usage` is retired; inspect the current `GET /v1/account/overview` requirements instead. Compatibility and feature reasons share one persisted forced attempt, marked pending before network access and completed even on unchanged results or failures. If its owner dies, stop with the interrupted blocked result; never start a second forced check. Failed compatibility refresh stops the incompatible operation; unrelated safe reads and status recovery may continue. An unverified requested feature must be reported as unconfirmed. Compare new instructions against the failing workflow's original pin, even if another workflow already updated the installation cache and the refresh result says `unchanged`. Matching original instructions do not justify retrying; report the original error and request ID. A changed version permits examining a correction. At most one corrected attempt is allowed after confirming the original operation was not applied and checking user intent, permissions and new request requirements. `--allow-repin` records reconciliation of in-flight requests; it is not human approval. Keep completed work and status URLs/tokens through every refresh. Ordinary authentication, expired tokens, missing scopes, quota/rate limits, missing resources, rejected media, checksum failures, timeouts and 5xx errors do not trigger instruction refresh. An unknown route/tool or schema rejection contradicting cached guidance permits at most one diagnostic documentation refresh, plus one `tools/list` refresh for MCP; it never establishes safe mutation replay. MCP preserves explicit recovery metadata with `isError: true`; transport authentication retains its normal handling. For the same logical idempotent request and unchanged payload, retain the original idempotency key. A changed payload may use a new key only under the endpoint's rules after proving the prior attempt created no operation. Never replay raw bytes after an uncertain PUT, reset an upload session because docs changed, or recreate a gallery whose creation outcome is unknown. Recover status first. Refresh cannot grant permissions, reconnect an account, expand scopes, authorize spending or replace human approval. Keep tokens private and upload bytes by direct HTTP. The weekly TTL applies only to instructions. Read account usage, scopes, entitlements, credit balances, request limits, upload URLs/headers/expiry and `nextAction` from live API responses. Optional deployment capabilities may be checked once while preparing a workflow; returned upload responses and errors remain authoritative.