# Picem 0.1 — integration guide

Base URL: https://picem.whatonearth.work
OpenAPI: /openapi.json · Interactive reference: /api-reference
Capability discovery: GET /v1/capabilities

## Authentication and ownership

Create an App and scoped API key in /console. Keep the key on your backend, in a secret store. Never embed it in frontend JavaScript, mobile binaries, URLs, logs or public Agent Skills. Requests use `Authorization: Bearer $PICEM_API_KEY`.
An App key accesses only that App's assets. `owner_ref` is your user ID for filtering; it is NOT an authorization boundary. Your backend must check the current user's ownership before requesting a private link. The console uses an HttpOnly session cookie and same-origin CSRF checks.

## Upload a small image

```sh
curl --fail-with-body https://picem.whatonearth.work/v1/assets \
  -H "Authorization: Bearer $PICEM_API_KEY" \
  -H "Idempotency-Key: profile-user123-revision7" \
  -F file=@photo.jpg -F purpose=avatar -F visibility=public -F owner_ref=user123
```

The response's `id` is the permanent asset ID. HTTP 202 means accepted, not ready. Poll `GET /v1/assets/{id}` every 2–5 seconds until `status` is `ready` or `failed`; use bounded exponential backoff for 429/503. Store the asset ID in your application's database. Only `ready` responses contain usable variants.
Purposes: `avatar`, `image`, `video`, `attachment`. Default visibility is `private`.
Presets: avatar-128 / avatar-256 / avatar-512; thumb-640 / display-1280 / display-1920; playback-mp4 / poster. `original` always preserves the input. GIF originals are supported; animated avatars are rejected.
Image limits: 20 MB / 40 megapixels. Originals report measured integer width, height and size_bytes. Images are processed in OSS; video encoding runs in Alibaba IMS.

Reuse the same Idempotency-Key for the exact same logical request after a timeout. A different payload with the same key returns 409. Keys are isolated per App and endpoint. Identical ready small-upload assets are deduplicated only within the same App, owner, purpose and visibility. Direct-upload sessions do not deduplicate.

## Direct upload (up to 1 GB video, 100 MB attachment)

1. `POST /v1/upload-sessions` with JSON `{ "name":"clip.mov", "size_bytes":123456, "purpose":"video", "visibility":"private", "owner_ref":"user123" }` and an Idempotency-Key.
2. Send a multipart POST to the returned `upload.url`. Include every entry from `upload.fields` as a form field, and the original bytes in `upload.file_field` (`file`), LAST. Let your HTTP client set the multipart Content-Type and boundary. The signed policy binds one object key and exact file size; do not alter fields.
3. Call the returned `complete_url` with Picem authentication. Completion is idempotent and checks the OSS object size before copying to its permanent source key.
4. Poll the asset until ready. `GET /v1/upload-sessions/{id}` refreshes a short-lived policy while the one-hour session remains valid. A completed session can be completed again to recover its asset.

This release uses one POST per file, not multipart resumable upload. A network interruption requires retrying the file within the same session. Do not blindly create new sessions. Source URLs are not public delivery URLs.

## Public and private delivery

For public assets, use `variants[variant].url` directly. URLs include the immutable asset ID and variant, and are CDN cacheable. For private assets, call:

```sh
curl --fail-with-body https://picem.whatonearth.work/v1/assets/ASSET_ID/access \
 -H "Authorization: Bearer $PICEM_API_KEY" -H 'Content-Type: application/json' \
 -d '{"variant":"original","expires_in":300}'
```

Signed private URLs last 30–900 seconds. Treat them as bearer credentials; do not persist them or include them in logs. Generate new links on demand. Attachments are delivered as downloads, with `application/octet-stream`, to prevent active content from executing under the site domain.

## Video

Check `purposes` in /v1/capabilities first. Video is accepted only when the live deployment enables it after IMS verification; otherwise the API returns `video_not_enabled`.

Upload MOV/MP4 or another ffprobe-readable video with `purpose=video` (up to 30 minutes). Picem preserves the original and asks IMS for H.264/yuv420p MP4 (up to 1920×1080) and a JPEG poster. The server probes metadata but never locally encodes video. `ready` requires actual playable output and poster verification.
`unknown_outcome` means a paid submission may have succeeded without an acknowledged job ID. Automatic resubmission is blocked: ask the maintainer to reconcile the IMS task first. Other confirmed failures may be retried through `POST /v1/assets/{id}/retry` with a write-scoped key.

## Listing, deletion and limits

`GET /v1/assets?owner_ref=user123&limit=20` returns `items` and `next_cursor`. Pass the cursor to get the next page. Results are newest first. `GET /v1/usage` reports original bytes and the App quota (initially 10 GB); derivatives and retained versions incur additional OSS storage.
`PATCH /v1/assets/{id}` with `{ "name":"new name" }` requires `write` scope.
`DELETE /v1/assets/{id}` requires `delete` scope and returns 202. New access authorization stops immediately. Published objects are removed and public CDN refresh is checked before `deleted`. Previously downloaded/browser-cached copies cannot be revoked. Source objects and historical OSS versions are retained for recovery; deletion is not erasure of all retained bytes.
60 new uploads/sessions per App per minute; Nginx imposes an additional per-IP request limit. Stay below both, handle 429 with backoff.

## Error handling

Application errors contain `error.code`, `error.retryable` and `request_id`. Input-validation errors use FastAPI's `detail` array. Edge limits can return an Nginx error body: check HTTP status before assuming JSON.
401: fix/reissue key. 403: insufficient scope/disabled uploads. 404: absent or inaccessible asset. 409: idempotency/state conflict. 413: too large; use direct upload if supported. 422: invalid bytes/input. 429: back off or raise quota. 5xx: retry the SAME logical request and retain request ID for support.
Do not upload from arbitrary URLs: this release accepts file bytes, which avoids a server-side URL-fetch/SSRF surface.
