# peinture API — full documentation --- # Overview # peinture API Image storage & processing. You upload photos; the service stores the originals privately, generates a set of sizes and formats, and serves them publicly. This is the developer reference. ## Shape of the API Metadata and control is **JSON-RPC 2.0** over a single endpoint: ``` POST https://peinture.gumeniuk.com/rpc Content-Type: application/json ``` Every request is an envelope: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.list", "params": { "limit": 50 } } ``` A success returns `result`; a failure returns `error` (with an HTTP status of 200 either way — see [errors](/docs/errors)). Raw bytes — [upload](/docs/uploads), archive download, and image serving — are plain HTTP, not JSON-RPC. Each method also has a REST-shaped alias at `POST /rpc/{method}` (e.g. `POST /rpc/image.list`) so OpenAPI tooling can address one method per path. ## Quickstart 1. **Log in** for an access token: ```bash curl -s https://peinture.gumeniuk.com/rpc -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"auth.login","params":{"email":"you@example.com","password":"…"}}' # → { "result": { "access_token": "…", "refresh_token": "…", "user": {…} } } ``` 2. **Upload** an image (multipart, not JSON-RPC): ```bash curl -s https://peinture.gumeniuk.com/api/v1/images -H "Authorization: Bearer $TOKEN" \ -F 'file=@photo.jpg' -F 'meta={"title":"Photo"}' ``` 3. **List** your images: ```bash curl -s https://peinture.gumeniuk.com/rpc -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.list","params":{"limit":50}}' ``` ## Next - [Authentication](/docs/authentication) — access tokens, refresh, and personal access tokens - [Methods](/docs/methods) — the full method reference - [Errors](/docs/errors) — error codes and how to handle them - Machine-readable: [OpenRPC](/openrpc.json) · [OpenAPI 3.1](/openapi.json) · [Postman](/docs/peinture.postman_collection.json) --- # Authentication Every non-public method requires a bearer token in the `Authorization` header: ``` Authorization: Bearer ``` peinture accepts two kinds of bearer token there — a short-lived **access token** for interactive sessions, and a long-lived **personal access token** for scripts and other apps. ## Access & refresh tokens Call [`auth.login`](/docs/methods/auth.login) with an email and password to receive a pair: - an **access token** — a JWT with a 15-minute lifetime, sent as `Authorization: Bearer ` on every call; - a **refresh token** — used to obtain a new pair without re-entering the password. Refresh proactively before the access token expires. On a `token_expired` (`1002`) error, call [`auth.refresh`](/docs/methods/auth.refresh) once with your refresh token and retry the original request. Refresh tokens slide on each use up to a 90-day absolute session cap, after which the user must log in again. [`auth.logoutAll`](/docs/methods/auth.logoutAll) revokes every session for the user — every issued access and refresh token, including the one making the call. ## Personal access tokens To authorize another application without sharing your password, mint a **personal access token** (PAT) with [`token.create`](/docs/methods/token.create). Send it exactly like an access token — `Authorization: Bearer pnt_…` — but it never needs refreshing: it stays valid until it expires (if you set an expiry) or you revoke it. - The secret (`pnt_…`) is shown **once** at creation; only a hash and a short hint are stored. - A PAT is **never** an admin token and may call **only** the `image.*` methods plus the upload and archive HTTP routes. Everything else returns `forbidden` (`1003`). - Revoke a PAT individually with [`token.revoke`](/docs/methods/token.revoke); it does not affect your other tokens or your password sessions. The `token.*` and `account.*` methods are self-management and are reachable **only** with a password-session access token, never a PAT. --- # Rate limits peinture applies a small number of protective limits. They exist to keep the service stable, not to meter usage. ## Login throttling Repeated failed logins are throttled: after a few failures for a given account and client, each further attempt is delayed with an exponential backoff (up to a few minutes) before the credentials are even checked. A throttled attempt returns `unauthorized` (`1001`). Successful logins are never delayed. Only [`auth.login`](/docs/methods/auth.login) is throttled; refresh, personal-access-token, and image calls are not. ## Batch limits The `POST /rpc` endpoint accepts JSON-RPC batches (an array of request objects), bounded as follows: - at most **20** requests per batch — a larger batch is rejected before it is parsed; - at most **4** requests execute concurrently; - each method is capped at **10 seconds**, so the worst-case batch completes well within the request timeout. The single-method alias routes (`POST /rpc/{method}`) do **not** accept batches — send a batch to `POST /rpc`. ## Body size - `POST /rpc` and the authentication endpoints cap the request body at **1 MiB** — more than enough for metadata and control-plane calls. - `POST /api/v1/images` (upload) allows a much larger body (the configured upload maximum, 100 MB by default). An oversized body returns `413 Request Entity Too Large`. There are no per-second request quotas on the read/write methods; be a considerate client. --- # Pagination List endpoints use **cursor pagination**, not page numbers or offsets, so a bulk import of old photos never reshuffles the top of an in-progress listing. [`image.list`](/docs/methods/image.list) returns: ```json { "items": [ /* … */ ], "next_cursor": "018f3a6b-9d1e-7c4a-8b2f-1a2b3c4d5e6f" } ``` To fetch the next page, pass the returned `next_cursor` back as the `cursor` parameter. When `next_cursor` is empty (`""`), you have reached the last page. Items are ordered by capture time — `taken_at`, falling back to `created_at` — newest first. The cursor is opaque; do not parse or construct it, and do not assume it is a timestamp or an id. Use the optional `limit` parameter (default 50) to control page size. ```bash # first page curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.list","params":{"limit":50}}' # next page — feed next_cursor back in curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"image.list","params":{"cursor":"018f3a6b-9d1e-7c4a-8b2f-1a2b3c4d5e6f"}}' ``` --- # Serving modes Generated renders (thumbnails, sizes, crops) are served in one of two modes. The mode is an operator setting; as an API client you consume whatever render URLs the API hands you — both shapes always work. ## direct (default) Render URLs point straight at the public bucket or its custom domain, e.g. ``` https://img.peinture.gumeniuk.com/r/{id}/{crop}.{ext}?v={spec_version} ``` The app is not in the data path — the CDN/bucket serves the bytes. ## proxy Render URLs point back at the app, which streams the bytes from the public store: ``` https://peinture.gumeniuk.com/i/{id}/{crop}.{ext} ``` The `GET /i/{id}/{file}` route always exists, even in direct mode, as a fallback. It serves only public renders — never originals. ## Cache-busting Every render URL carries a `?v={spec_version}` query parameter. When an image is regenerated (new preset set, or [`image.regenerate`](/docs/methods/image.regenerate)), the version changes, so a browser or CDN never serves a stale render under the same URL. You do not choose the mode per request; [`image.get`](/docs/methods/image.get) and [`image.list`](/docs/methods/image.list) return URLs already in the active shape. --- # Uploads & binary routes Three surfaces move raw bytes rather than JSON-RPC. They are plain HTTP because they stream binary data. ## Upload — `POST /api/v1/images` Multipart form upload of an original. Requires a bearer token (an access token or a personal access token). - `file` (required) — the image file (png, jpg, or webp). - `meta` (optional) — a JSON string, `{"title":"…","description":"…"}`. On success returns `201` with the created image (the same shape as [`image.get`](/docs/methods/image.get)). The original is stored privately and its renders are enqueued; the image starts in a non-ready state and becomes `ready` once its renders are generated. ```bash curl -s https://peinture.gumeniuk.com/api/v1/images \ -H "Authorization: Bearer $TOKEN" \ -F 'file=@sunset.jpg' \ -F 'meta={"title":"Sunset","description":"From the pier"}' ``` Errors: `400` (missing/empty/unsupported image), `401` (bad token), `413` (too large). ## Archive — `GET /api/v1/images/{id}/archive?token=…` Downloads a zip of every render plus the original. The `token` comes from [`image.archiveToken`](/docs/methods/image.archiveToken) — a 5-minute, owner-scoped link. Ready images only. Returns `application/zip`. ## Render serve — `GET /i/{id}/{crop}.{ext}` Streams a single public render through the app (see [serving modes](/docs/serving-modes)). Never serves originals. Public — no token required. --- # Methods # Methods Every JSON-RPC method, grouped by namespace. Call over `POST /rpc`, or use the per-method alias `POST /rpc/{method}`. ## account | Method | Auth | Summary | |---|---|---| | [`account.setEmail`](/docs/methods/account.setEmail) | bearer | Change the caller's email address | | [`account.setPassword`](/docs/methods/account.setPassword) | bearer | Change the caller's password | ## album | Method | Auth | Summary | |---|---|---| | [`album.create`](/docs/methods/album.create) | bearer | Create a folder | | [`album.crop.delete`](/docs/methods/album.crop.delete) | bearer | Delete a folder crop rule (re-renders the folder) | | [`album.crop.list`](/docs/methods/album.crop.list) | bearer | List a folder's crop rules | | [`album.crop.set`](/docs/methods/album.crop.set) | bearer | Add or update a folder crop rule (re-renders the folder) | | [`album.delete`](/docs/methods/album.delete) | bearer | Delete a folder | | [`album.list`](/docs/methods/album.list) | bearer / PAT | List the caller's folders (default first) | | [`album.move`](/docs/methods/album.move) | bearer / PAT | Move a photo into a folder | | [`album.reorder`](/docs/methods/album.reorder) | bearer | Set the manual order of photos within a folder | | [`album.update`](/docs/methods/album.update) | bearer | Rename/re-describe a folder | ## auth | Method | Auth | Summary | |---|---|---| | [`auth.login`](/docs/methods/auth.login) | public | Log in with email and password | | [`auth.logoutAll`](/docs/methods/auth.logoutAll) | bearer | Revoke all of the caller's sessions | | [`auth.refresh`](/docs/methods/auth.refresh) | public | Exchange a refresh token for a new token pair | ## image | Method | Auth | Summary | |---|---|---| | [`image.archiveToken`](/docs/methods/image.archiveToken) | bearer / PAT | Mint a short-lived link to download the full archive | | [`image.crop.add`](/docs/methods/image.crop.add) | bearer / PAT | Add a custom crop (max 5 per image) | | [`image.crop.delete`](/docs/methods/image.crop.delete) | bearer / PAT | Delete a custom crop by code | | [`image.delete`](/docs/methods/image.delete) | bearer / PAT | Delete an image | | [`image.exif.setVisibility`](/docs/methods/image.exif.setVisibility) | bearer / PAT | Toggle which EXIF groups are exposed publicly | | [`image.get`](/docs/methods/image.get) | bearer / PAT | Get one of the caller's images with full detail | | [`image.getPublic`](/docs/methods/image.getPublic) | public | Get a ready image's public view (no authentication) | | [`image.list`](/docs/methods/image.list) | bearer / PAT | List the caller's images (cursor-paginated) | | [`image.regenerate`](/docs/methods/image.regenerate) | bearer / PAT | Re-render all sizes for an image | | [`image.update`](/docs/methods/image.update) | bearer / PAT | Update an image's title and/or description | ## rpc | Method | Auth | Summary | |---|---|---| | [`rpc.discover`](/docs/methods/rpc.discover) | public | Return the OpenRPC document describing this API | ## token | Method | Auth | Summary | |---|---|---| | [`token.create`](/docs/methods/token.create) | bearer | Create a personal access token | | [`token.list`](/docs/methods/token.list) | bearer | List the caller's personal access tokens | | [`token.revoke`](/docs/methods/token.revoke) | bearer | Revoke a personal access token | --- # Errors # Errors Errors are returned in the JSON-RPC `error` object. The HTTP status is **200** even for an error — inspect the body, not the status. ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": 2001, "message": "not_found", "data": "…" } } ``` `error.data`, when present, carries client-safe detail (a validation reason, a limit). Internal detail is never leaked into it. ## Application codes | Code | Message | Meaning & remedy | |---|---|---| | `1001` | `unauthorized` | Missing or invalid token, bad login, or a throttled login. Re-authenticate. | | `1002` | `token_expired` | The access token expired. Call [auth.refresh](/docs/methods/auth.refresh) once and retry. | | `1003` | `forbidden` | Not the owner, or the method is outside a personal access token's scope. | | `2001` | `not_found` | No such resource owned by the caller. | | `2002` | `validation_failed` | The request was malformed; `error.data` says why. | | `3001` | `storage_error` | A transient backend failure while verifying your request (e.g. a database blip). Your token is fine — retry with backoff. | | `4001` | `custom_crop_limit_exceeded` | The image already has the maximum of five custom crops. | ## JSON-RPC codes Transport-level errors use the standard JSON-RPC range: | Code | Meaning | |---|---| | `-32700` | Parse error — the body is not valid JSON. | | `-32600` | Invalid request — not a conforming JSON-RPC object (e.g. a batch on an alias route, or a method that disagrees with the path). | | `-32601` | Method not found. | | `-32602` | Invalid params — the params did not match the method's schema. | | `-32603` | Internal error. | | `-32605` | The request exceeded the per-method time limit. | A `batch_too_large` invalid-request (`-32600`) is returned when a batch exceeds 20 requests (see [rate limits](/docs/rate-limits)). --- # Glossary Terms used across this API.
Original
The exact file you uploaded, stored privately. Never served publicly; only downloadable by the owner via the archive.
Render
A generated variant of an image at a specific size and format (e.g. a 2048px webp). Served from the public store. Each render URL carries a ?v= cache-buster.
Preset
A shared, admin-defined size/format applied to every image (e.g. thumb, large). Changing a preset re-renders the whole library.
Crop (custom crop)
A per-image size beyond the shared presets, added by the owner with image.crop.add. Up to five per image. Identified by a code matching ^[a-z0-9][a-z0-9-]{0,31}$.
spec_version
A per-image counter bumped whenever the render set changes. It is the ?v= value on render URLs, so a regenerate never serves stale bytes from a cache.
EXIF visibility
Per-image toggles (gps, camera, datetime, settings) controlling which metadata groups are exposed on the public view. Set with image.exif.setVisibility.
Serve mode
Whether render URLs point at the public bucket (direct) or are streamed through the app (proxy). See serving modes.
Personal access token (PAT)
A long-lived bearer token (prefix pnt_) for scripts, scoped to the image.* methods. See authentication.
--- # Changelog Notable changes to the public API. The version is surfaced as `info.version` in the [OpenRPC](/openrpc.json) and [OpenAPI](/openapi.json) documents. ## 1.0.0 - Initial documented surface: `auth.*`, `image.*`, `account.*`, `token.*`, and the `rpc.discover` service-discovery method. - Machine-readable documents published: OpenRPC (`/openrpc.json`, also returned by `rpc.discover`), an OpenAPI 3.1 wrapper (`/openapi.json`) with a `POST /rpc/{method}` alias per method, and a Postman collection. - Cursor pagination on `image.list`; personal access tokens scoped to `image.*`; per-image EXIF visibility toggles; up to five custom crops per image. --- # account.setEmail # `account.setEmail` bearer **Change the caller's email address** Verifies the current password and updates the email. No confirmation email is sent. JWT session only. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `current_password` | string | yes | The caller's current password (re-authentication) | | `email` | string | yes | The new email address | ## Result Returns `userResult`: | Field | Type | Description | |---|---|---| | `user *` | `userDTO` | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "account.setEmail", "params": { "current_password": "correct-horse-battery-staple", "email": "ada@analyticalengine.example" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "user": { "id": "018f3a5c-1c7a-7e3b-9c2a-3f4b5a6c7d8e", "email": "ada@analyticalengine.example", "is_admin": false } } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"account.setEmail","params":{"current_password":"correct-horse-battery-staple","email":"ada@analyticalengine.example"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "account.setEmail", params: {"current_password":"correct-horse-battery-staple","email":"ada@analyticalengine.example"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"account.setEmail","params":{"current_password":"correct-horse-battery-staple","email":"ada@analyticalengine.example"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # account.setPassword # `account.setPassword` bearer **Change the caller's password** Verifies the current password, sets a new one, and returns a fresh token pair (the calling session stays signed in; all other sessions are revoked). JWT session only. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `current_password` | string | yes | The caller's current password (re-authentication) | | `new_password` | string | yes | The new password to set | ## Result Returns `tokenPairResult`: | Field | Type | Description | |---|---|---| | `access_expires_at *` | string | RFC3339 expiry of the access token | | `access_token *` | string | Short-lived bearer token (15-minute TTL); send as Authorization: Bearer \ | | `refresh_token *` | string | Long-lived token used with auth.refresh to obtain a new pair | | `user *` | `userDTO` | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "account.setPassword", "params": { "current_password": "correct-horse-battery-staple", "new_password": "Tr0ub4dour\u00263-fresh" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMThmM2E1Yy0xYzdhLTdlM2IifQ.c2ln", "refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.c2ln", "access_expires_at": "2026-03-14T09:41:53Z", "user": { "id": "018f3a5c-1c7a-7e3b-9c2a-3f4b5a6c7d8e", "email": "ada.lovelace@example.com", "is_admin": false } } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"account.setPassword","params":{"current_password":"correct-horse-battery-staple","new_password":"Tr0ub4dour\u00263-fresh"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "account.setPassword", params: {"current_password":"correct-horse-battery-staple","new_password":"Tr0ub4dour\u00263-fresh"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"account.setPassword","params":{"current_password":"correct-horse-battery-staple","new_password":"Tr0ub4dour\u00263-fresh"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.create # `album.create` bearer **Create a folder** Creates a private, non-default folder. is_default and visibility are server-set. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `name` | string | yes | Folder name (1-200 chars) | | `description` | string | no | Optional description | ## Result Returns `albumDTO`: | Field | Type | Description | |---|---|---| | `created_at *` | string | | | `description *` | string | | | `id *` | string | | | `image_count *` | integer | | | `is_default *` | boolean | | | `name *` | string | | | `visibility *` | string | | ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.create", "params": { "name": "Iceland 2024", "description": "Ring road, March." } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50", "name": "Iceland 2024", "description": "Ring road, March.", "is_default": false, "visibility": "private", "image_count": 42, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.create","params":{"name":"Iceland 2024","description":"Ring road, March."}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.create", params: {"name":"Iceland 2024","description":"Ring road, March."}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.create","params":{"name":"Iceland 2024","description":"Ring road, March."}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.crop.delete # `album.crop.delete` bearer **Delete a folder crop rule (re-renders the folder)** ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `album_id` | string | yes | Folder UUID | | `code` | string | yes | Crop code to delete | ## Result Returns `jobResult`: | Field | Type | Description | |---|---|---| | `job_id *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.crop.delete", "params": { "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50", "code": "hero" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "job_id": 4242 } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.crop.delete","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","code":"hero"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.crop.delete", params: {"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","code":"hero"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.crop.delete","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","code":"hero"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.crop.list # `album.crop.list` bearer **List a folder's crop rules** ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `album_id` | string | yes | Folder UUID | ## Result Returns `cropRuleListResult`: | Field | Type | Description | |---|---|---| | `crops *` | `array` | | ## Errors | Code | Message | When | |---|---|---| | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.crop.list", "params": { "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "crops": [ { "code": "hero", "width": 1200, "height": 630, "mode": "fill", "format": "webp", "quality": 82 } ] } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.crop.list","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.crop.list", params: {"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.crop.list","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.crop.set # `album.crop.set` bearer **Add or update a folder crop rule (re-renders the folder)** Upserts a per-folder crop; every photo in the folder is re-rendered. code must match ^[a-z0-9][a-z0-9-]{0,31}$; at least one of width/height in 1-15000; mode defaults to fill, format to webp, quality to 80. Returns the recrop job id. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `album_id` | string | yes | Folder UUID | | `code` | string | yes | Crop code, ^[a-z0-9][a-z0-9-]{0,31}$ | | `width` | integer | yes | Target width in px (0-15000) | | `height` | integer | yes | Target height in px (0-15000) | | `mode` | string | no | Fit mode; defaults to fill | | `format` | string | no | Output format; defaults to webp | | `quality` | integer | no | Encoder quality 1-100; defaults to 80 | ## Result Returns `jobResult`: | Field | Type | Description | |---|---|---| | `job_id *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.crop.set", "params": { "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50", "code": "hero", "width": 1200, "height": 630, "mode": "fill", "format": "webp", "quality": 82 } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "job_id": 4242 } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.crop.set","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","code":"hero","width":1200,"height":630,"mode":"fill","format":"webp","quality":82}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.crop.set", params: {"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","code":"hero","width":1200,"height":630,"mode":"fill","format":"webp","quality":82}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.crop.set","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","code":"hero","width":1200,"height":630,"mode":"fill","format":"webp","quality":82}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.delete # `album.delete` bearer **Delete a folder** Moves the folder's photos to the default folder, re-renders exactly those, and soft-deletes the folder. Returns the recrop job id; job_id 0 means the folder was empty (no recrop). The default folder cannot be deleted. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | ## Result Returns `jobResult`: | Field | Type | Description | |---|---|---| | `job_id *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2001` | `not_found` | No such resource owned by the caller. | | `1003` | `forbidden` | Not the owner, or outside a token's scope. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.delete", "params": { "id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "job_id": 4242 } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.delete","params":{"id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.delete", params: {"id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.delete","params":{"id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.list # `album.list` bearer / PAT **List the caller's folders (default first)** Returns every folder with its live-image count. The default folder is pinned first and is immutable (cannot be renamed or deleted). ## Parameters _None._ ## Result Returns `albumListResult`: | Field | Type | Description | |---|---|---| | `albums *` | `array` | | ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.list" } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "albums": [ { "id": "018f3a7c-0000-7000-9a00-0b1c2d3e4f50", "name": "Library", "description": "", "is_default": true, "visibility": "private", "image_count": 128, "created_at": "2026-01-01T00:00:00Z" }, { "id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50", "name": "Iceland 2024", "description": "Ring road, March.", "is_default": false, "visibility": "private", "image_count": 42, "created_at": "2026-02-11T07:03:00Z" } ] } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.list"}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.list", params: {}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.list"}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.move # `album.move` bearer / PAT **Move a photo into a folder** Moves the image into the folder and re-renders it for the folder's crop rules. Returns the updated image. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID to move | | `album_id` | string | yes | Destination folder UUID | ## Result Returns `ImageDTO`: | Field | Type | Description | |---|---|---| | `album_id` | string | | | `created_at *` | string | | | `custom_crops *` | `array` | | | `description *` | string | | | `exif *` | `exifDTO` | | | `exif_visibility` | `visibilityDTO` | | | `format *` | string | | | `height *` | integer | | | `id *` | string | | | `renders *` | `array` | | | `size_bytes *` | integer | | | `status *` | string | | | `title *` | string | | | `width *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.move", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "description": "Long exposure from the Marin Headlands.", "status": "ready", "width": 6000, "height": 4000, "size_bytes": 8452016, "format": "jpg", "exif": { "taken_at": "2026-02-11T06:42:18Z", "camera_make": "FUJIFILM", "camera_model": "X-T5", "lens": "XF16-55mmF2.8" }, "renders": [ { "crop": "thumb", "format": "webp", "width": 256, "height": 256, "size_bytes": 14320, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "pending": false }, { "crop": "large", "format": "webp", "width": 2048, "height": 1365, "size_bytes": 412880, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/large.webp?v=3", "pending": false } ], "custom_crops": [], "exif_visibility": { "gps": false, "camera": true, "datetime": true, "settings": false }, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.move","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.move", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.move","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.reorder # `album.reorder` bearer **Set the manual order of photos within a folder** Assigns the given order to the listed images (all must belong to the folder). Pure metadata — no re-render. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `album_id` | string | yes | Folder UUID | | `ordered_ids` | `array` | yes | Image UUIDs in the desired order; all must belong to the folder | ## Result Returns an empty object `{}` on success. ## Errors | Code | Message | When | |---|---|---| | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.reorder", "params": { "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50", "ordered_ids": [ "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1e" ] } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": {} } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.reorder","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","ordered_ids":["018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1e"]}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.reorder", params: {"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","ordered_ids":["018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1e"]}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.reorder","params":{"album_id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","ordered_ids":["018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1e"]}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # album.update # `album.update` bearer **Rename/re-describe a folder** Partial update: omitted fields are left unchanged. The default folder is immutable. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Folder UUID | | `name` | string | no | New name; omit to leave unchanged | | `description` | string | no | New description; omit to leave unchanged | ## Result Returns `albumDTO`: | Field | Type | Description | |---|---|---| | `created_at *` | string | | | `description *` | string | | | `id *` | string | | | `image_count *` | integer | | | `is_default *` | boolean | | | `name *` | string | | | `visibility *` | string | | ## Errors | Code | Message | When | |---|---|---| | `2001` | `not_found` | No such resource owned by the caller. | | `1003` | `forbidden` | Not the owner, or outside a token's scope. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "album.update", "params": { "id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50", "name": "Iceland — spring 2024" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50", "name": "Iceland 2024", "description": "Ring road, March.", "is_default": false, "visibility": "private", "image_count": 42, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"album.update","params":{"id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","name":"Iceland — spring 2024"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "album.update", params: {"id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","name":"Iceland — spring 2024"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"album.update","params":{"id":"018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50","name":"Iceland — spring 2024"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # auth.login # `auth.login` public **Log in with email and password** Exchanges credentials for an access + refresh token pair. Send the access token as `Authorization: Bearer ` on every non-public call. Login is rate-limited per email+IP; repeated failures are throttled. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `email` | string | yes | Account email address | | `password` | string | yes | Account password | ## Result Returns `tokenPairResult`: | Field | Type | Description | |---|---|---| | `access_expires_at *` | string | RFC3339 expiry of the access token | | `access_token *` | string | Short-lived bearer token (15-minute TTL); send as Authorization: Bearer \ | | `refresh_token *` | string | Long-lived token used with auth.refresh to obtain a new pair | | `user *` | `userDTO` | | ## Errors | Code | Message | When | |---|---|---| | `1001` | `unauthorized` | Missing/invalid credentials or a throttled login. | | `2002` | `validation_failed` | Malformed request; see error.data. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "auth.login", "params": { "email": "ada.lovelace@example.com", "password": "correct-horse-battery-staple" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMThmM2E1Yy0xYzdhLTdlM2IifQ.c2ln", "refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.c2ln", "access_expires_at": "2026-03-14T09:41:53Z", "user": { "id": "018f3a5c-1c7a-7e3b-9c2a-3f4b5a6c7d8e", "email": "ada.lovelace@example.com", "is_admin": false } } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"auth.login","params":{"email":"ada.lovelace@example.com","password":"correct-horse-battery-staple"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "auth.login", params: {"email":"ada.lovelace@example.com","password":"correct-horse-battery-staple"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"auth.login","params":{"email":"ada.lovelace@example.com","password":"correct-horse-battery-staple"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) ``` --- # auth.logoutAll # `auth.logoutAll` bearer **Revoke all of the caller's sessions** Bumps the user's token version, invalidating every issued access and refresh token — including the calling session. JWT session only (not callable with a personal access token). ## Parameters _None._ ## Result Returns an empty object `{}` on success. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "auth.logoutAll" } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": {} } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"auth.logoutAll"}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "auth.logoutAll", params: {}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"auth.logoutAll"}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # auth.refresh # `auth.refresh` public **Exchange a refresh token for a new token pair** Rotates the token pair. Refresh tokens slide on each use up to a 90-day absolute session cap, after which the user must log in again. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `refresh_token` | string | yes | A refresh token from a prior auth.login or auth.refresh | ## Result Returns `tokenPairResult`: | Field | Type | Description | |---|---|---| | `access_expires_at *` | string | RFC3339 expiry of the access token | | `access_token *` | string | Short-lived bearer token (15-minute TTL); send as Authorization: Bearer \ | | `refresh_token *` | string | Long-lived token used with auth.refresh to obtain a new pair | | `user *` | `userDTO` | | ## Errors | Code | Message | When | |---|---|---| | `1001` | `unauthorized` | Missing/invalid credentials or a throttled login. | | `1002` | `token_expired` | The access token expired; refresh and retry. | | `2002` | `validation_failed` | Malformed request; see error.data. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "auth.refresh", "params": { "refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.Zm9vYmFy" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMThmM2E1Yy0xYzdhLTdlM2IifQ.c2ln", "refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.c2ln", "access_expires_at": "2026-03-14T09:41:53Z", "user": { "id": "018f3a5c-1c7a-7e3b-9c2a-3f4b5a6c7d8e", "email": "ada.lovelace@example.com", "is_admin": false } } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"auth.refresh","params":{"refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.Zm9vYmFy"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "auth.refresh", params: {"refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.Zm9vYmFy"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"auth.refresh","params":{"refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.Zm9vYmFy"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) ``` --- # image.archiveToken # `image.archiveToken` bearer / PAT **Mint a short-lived link to download the full archive** Returns a 5-minute, image-scoped URL to a zip of every render plus the original. Ready images only — a pending/failed image would produce a partial archive. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | ## Result Returns `archiveResult`: | Field | Type | Description | |---|---|---| | `expires_at *` | string | | | `url *` | string | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.archiveToken", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "url": "https://peinture.gumeniuk.com/api/v1/images/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/archive?token=eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJhcmNoaXZlIn0.c2ln", "expires_at": "2026-03-14T09:31:53Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.archiveToken","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.archiveToken", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.archiveToken","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.crop.add # `image.crop.add` bearer / PAT **Add a custom crop (max 5 per image)** Adds a per-image crop beyond the shared presets and enqueues its render. code must match ^[a-z0-9][a-z0-9-]{0,31}$ and differ from a preset code; at least one of width/height must be positive (0-15000); mode defaults to fill, format to webp, quality to 80. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | | `code` | string | yes | Crop code, ^[a-z0-9][a-z0-9-]{0,31}$ and not a preset code | | `width` | integer | yes | Target width in px (0-15000); 0 keeps aspect from height | | `height` | integer | yes | Target height in px (0-15000); 0 keeps aspect from width | | `mode` | string | no | Fit mode; defaults to fill | | `format` | string | no | Output format (webp/jpg/png); defaults to webp | | `quality` | integer | no | Encoder quality 1-100; defaults to 80 | ## Result Returns `ImageDTO`: | Field | Type | Description | |---|---|---| | `album_id` | string | | | `created_at *` | string | | | `custom_crops *` | `array` | | | `description *` | string | | | `exif *` | `exifDTO` | | | `exif_visibility` | `visibilityDTO` | | | `format *` | string | | | `height *` | integer | | | `id *` | string | | | `renders *` | `array` | | | `size_bytes *` | integer | | | `status *` | string | | | `title *` | string | | | `width *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | | `4001` | `custom_crop_limit_exceeded` | The image already has five custom crops. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.crop.add", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "code": "square", "width": 1080, "height": 1080, "mode": "fill", "format": "webp", "quality": 82 } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "description": "Long exposure from the Marin Headlands.", "status": "ready", "width": 6000, "height": 4000, "size_bytes": 8452016, "format": "jpg", "exif": { "taken_at": "2026-02-11T06:42:18Z", "camera_make": "FUJIFILM", "camera_model": "X-T5", "lens": "XF16-55mmF2.8" }, "renders": [ { "crop": "thumb", "format": "webp", "width": 256, "height": 256, "size_bytes": 14320, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "pending": false }, { "crop": "large", "format": "webp", "width": 2048, "height": 1365, "size_bytes": 412880, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/large.webp?v=3", "pending": false } ], "custom_crops": [], "exif_visibility": { "gps": false, "camera": true, "datetime": true, "settings": false }, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.crop.add","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","code":"square","width":1080,"height":1080,"mode":"fill","format":"webp","quality":82}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.crop.add", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","code":"square","width":1080,"height":1080,"mode":"fill","format":"webp","quality":82}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.crop.add","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","code":"square","width":1080,"height":1080,"mode":"fill","format":"webp","quality":82}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.crop.delete # `image.crop.delete` bearer / PAT **Delete a custom crop by code** ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | | `code` | string | yes | Crop code to delete | ## Result Returns `ImageDTO`: | Field | Type | Description | |---|---|---| | `album_id` | string | | | `created_at *` | string | | | `custom_crops *` | `array` | | | `description *` | string | | | `exif *` | `exifDTO` | | | `exif_visibility` | `visibilityDTO` | | | `format *` | string | | | `height *` | integer | | | `id *` | string | | | `renders *` | `array` | | | `size_bytes *` | integer | | | `status *` | string | | | `title *` | string | | | `width *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.crop.delete", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "code": "square" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "description": "Long exposure from the Marin Headlands.", "status": "ready", "width": 6000, "height": 4000, "size_bytes": 8452016, "format": "jpg", "exif": { "taken_at": "2026-02-11T06:42:18Z", "camera_make": "FUJIFILM", "camera_model": "X-T5", "lens": "XF16-55mmF2.8" }, "renders": [ { "crop": "thumb", "format": "webp", "width": 256, "height": 256, "size_bytes": 14320, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "pending": false }, { "crop": "large", "format": "webp", "width": 2048, "height": 1365, "size_bytes": 412880, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/large.webp?v=3", "pending": false } ], "custom_crops": [], "exif_visibility": { "gps": false, "camera": true, "datetime": true, "settings": false }, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.crop.delete","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","code":"square"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.crop.delete", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","code":"square"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.crop.delete","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","code":"square"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.delete # `image.delete` bearer / PAT **Delete an image** Soft-deletes the image and asynchronously purges its objects from storage. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | ## Result Returns an empty object `{}` on success. ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.delete", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": {} } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.delete","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.delete", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.delete","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.exif.setVisibility # `image.exif.setVisibility` bearer / PAT **Toggle which EXIF groups are exposed publicly** Sets per-group public visibility (gps, camera, datetime, settings). Only the groups present in the request change; the rest are left as-is. GPS is exposed as a unit (both coordinates or neither). ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | | `groups` | `visibilityGroups` | yes | Per-group public visibility; omit a group to leave it unchanged | ## Result Returns `ImageDTO`: | Field | Type | Description | |---|---|---| | `album_id` | string | | | `created_at *` | string | | | `custom_crops *` | `array` | | | `description *` | string | | | `exif *` | `exifDTO` | | | `exif_visibility` | `visibilityDTO` | | | `format *` | string | | | `height *` | integer | | | `id *` | string | | | `renders *` | `array` | | | `size_bytes *` | integer | | | `status *` | string | | | `title *` | string | | | `width *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.exif.setVisibility", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "groups": { "gps": false, "camera": true } } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "description": "Long exposure from the Marin Headlands.", "status": "ready", "width": 6000, "height": 4000, "size_bytes": 8452016, "format": "jpg", "exif": { "taken_at": "2026-02-11T06:42:18Z", "camera_make": "FUJIFILM", "camera_model": "X-T5", "lens": "XF16-55mmF2.8" }, "renders": [ { "crop": "thumb", "format": "webp", "width": 256, "height": 256, "size_bytes": 14320, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "pending": false }, { "crop": "large", "format": "webp", "width": 2048, "height": 1365, "size_bytes": 412880, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/large.webp?v=3", "pending": false } ], "custom_crops": [], "exif_visibility": { "gps": false, "camera": true, "datetime": true, "settings": false }, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.exif.setVisibility","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","groups":{"gps":false,"camera":true}}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.exif.setVisibility", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","groups":{"gps":false,"camera":true}}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.exif.setVisibility","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","groups":{"gps":false,"camera":true}}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.get # `image.get` bearer / PAT **Get one of the caller's images with full detail** Returns the owner view: full EXIF, render URLs (with ?v= cache-buster and pending flags), custom crops, and the exif_visibility block. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | ## Result Returns `ImageDTO`: | Field | Type | Description | |---|---|---| | `album_id` | string | | | `created_at *` | string | | | `custom_crops *` | `array` | | | `description *` | string | | | `exif *` | `exifDTO` | | | `exif_visibility` | `visibilityDTO` | | | `format *` | string | | | `height *` | integer | | | `id *` | string | | | `renders *` | `array` | | | `size_bytes *` | integer | | | `status *` | string | | | `title *` | string | | | `width *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.get", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "description": "Long exposure from the Marin Headlands.", "status": "ready", "width": 6000, "height": 4000, "size_bytes": 8452016, "format": "jpg", "exif": { "taken_at": "2026-02-11T06:42:18Z", "camera_make": "FUJIFILM", "camera_model": "X-T5", "lens": "XF16-55mmF2.8" }, "renders": [ { "crop": "thumb", "format": "webp", "width": 256, "height": 256, "size_bytes": 14320, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "pending": false }, { "crop": "large", "format": "webp", "width": 2048, "height": 1365, "size_bytes": 412880, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/large.webp?v=3", "pending": false } ], "custom_crops": [], "exif_visibility": { "gps": false, "camera": true, "datetime": true, "settings": false }, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.get","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.get", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.get","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.getPublic # `image.getPublic` public **Get a ready image's public view (no authentication)** The visitor view: EXIF is filtered by the owner's visibility toggles, and custom crops and the exif_visibility block are omitted. Only ready images are returned. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | ## Result Returns `ImageDTO`: | Field | Type | Description | |---|---|---| | `album_id` | string | | | `created_at *` | string | | | `custom_crops *` | `array` | | | `description *` | string | | | `exif *` | `exifDTO` | | | `exif_visibility` | `visibilityDTO` | | | `format *` | string | | | `height *` | integer | | | `id *` | string | | | `renders *` | `array` | | | `size_bytes *` | integer | | | `status *` | string | | | `title *` | string | | | `width *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.getPublic", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "description": "Long exposure from the Marin Headlands.", "status": "ready", "width": 6000, "height": 4000, "size_bytes": 8452016, "format": "jpg", "exif": { "taken_at": "2026-02-11T06:42:18Z", "camera_make": "FUJIFILM", "camera_model": "X-T5", "lens": "XF16-55mmF2.8" }, "renders": [ { "crop": "thumb", "format": "webp", "width": 256, "height": 256, "size_bytes": 14320, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "pending": false }, { "crop": "large", "format": "webp", "width": 2048, "height": 1365, "size_bytes": 412880, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/large.webp?v=3", "pending": false } ], "custom_crops": [], "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.getPublic","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.getPublic", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.getPublic","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) ``` --- # image.list # `image.list` bearer / PAT **List the caller's images (cursor-paginated)** Ordered by capture time (taken_at, falling back to created_at), newest first. Pass the returned next_cursor back as cursor to page; an empty next_cursor means the last page. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `limit` | integer | no | Max items per page (default 50) | | `cursor` | string | no | Opaque cursor from a prior page's next_cursor; omit for the first page | | `album_id` | string | no | Restrict to one folder (UUID); folder view orders by manual position first. Omit for the whole library. Keep it constant across a paginated run | ## Result Returns `listResult`: | Field | Type | Description | |---|---|---| | `items *` | `array` | | | `next_cursor *` | string | | ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.list", "params": { "limit": 50 } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "items": [ { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "status": "ready", "width": 6000, "height": 4000, "thumb_url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "taken_at": "2026-02-11T06:42:18Z", "created_at": "2026-02-11T07:03:00Z" } ], "next_cursor": "018f3a6b-9d1e-7c4a-8b2f-1a2b3c4d5e6f" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.list","params":{"limit":50}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.list", params: {"limit":50}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.list","params":{"limit":50}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.regenerate # `image.regenerate` bearer / PAT **Re-render all sizes for an image** Enqueues a regeneration of every render for the current preset set. Also resets an image whose previous render failed. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | ## Result Returns an empty object `{}` on success. ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.regenerate", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": {} } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.regenerate","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.regenerate", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.regenerate","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # image.update # `image.update` bearer / PAT **Update an image's title and/or description** Partial update: omitted fields are left unchanged. Returns the updated image. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Image UUID | | `title` | string | no | New title; omit to leave unchanged | | `description` | string | no | New description; omit to leave unchanged | ## Result Returns `ImageDTO`: | Field | Type | Description | |---|---|---| | `album_id` | string | | | `created_at *` | string | | | `custom_crops *` | `array` | | | `description *` | string | | | `exif *` | `exifDTO` | | | `exif_visibility` | `visibilityDTO` | | | `format *` | string | | | `height *` | integer | | | `id *` | string | | | `renders *` | `array` | | | `size_bytes *` | integer | | | `status *` | string | | | `title *` | string | | | `width *` | integer | | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "image.update", "params": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at first light" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d", "title": "Golden Gate at dawn", "description": "Long exposure from the Marin Headlands.", "status": "ready", "width": 6000, "height": 4000, "size_bytes": 8452016, "format": "jpg", "exif": { "taken_at": "2026-02-11T06:42:18Z", "camera_make": "FUJIFILM", "camera_model": "X-T5", "lens": "XF16-55mmF2.8" }, "renders": [ { "crop": "thumb", "format": "webp", "width": 256, "height": 256, "size_bytes": 14320, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/thumb.webp?v=3", "pending": false }, { "crop": "large", "format": "webp", "width": 2048, "height": 1365, "size_bytes": 412880, "url": "https://img.peinture.gumeniuk.com/r/018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d/large.webp?v=3", "pending": false } ], "custom_crops": [], "exif_visibility": { "gps": false, "camera": true, "datetime": true, "settings": false }, "created_at": "2026-02-11T07:03:00Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"image.update","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","title":"Golden Gate at first light"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "image.update", params: {"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","title":"Golden Gate at first light"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"image.update","params":{"id":"018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d","title":"Golden Gate at first light"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # rpc.discover # `rpc.discover` public **Return the OpenRPC document describing this API** The service-discovery method from the OpenRPC specification. Returns the machine-readable description of every documented method: its params, result, errors and examples. Callable without authentication. ## Parameters _None._ ## Result Returns a JSON object. --- # token.create # `token.create` bearer **Create a personal access token** Mints a personal access token (PAT) for scripts and other apps. The plaintext secret (prefix `pnt_`) is returned ONCE and only a hash is stored. A PAT is sent like an access token (`Authorization: Bearer pnt_…`), never expires unless you set expires_at, and may call only the image.* namespace plus the upload/archive HTTP routes. JWT session only. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `current_password` | string | yes | The caller's current password (re-authentication) | | `name` | string | yes | A human label for the token | | `expires_at` | string | no | Optional RFC3339 expiry; omit for a token that never expires | ## Result Returns `createTokenResult`: | Field | Type | Description | |---|---|---| | `created_at *` | string | RFC3339 creation timestamp | | `expires_at` | string | RFC3339 expiry, if the token expires | | `hint *` | string | First 10 characters of the secret (pnt_ + 6), to identify it | | `id *` | string | Token UUID | | `last_used_at` | string | RFC3339 timestamp of last use (throttled), if ever used | | `name *` | string | Human label given at creation | | `token *` | string | The plaintext secret (prefix pnt_) — shown only once; store it now | ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "token.create", "params": { "current_password": "correct-horse-battery-staple", "name": "backup-script", "expires_at": "2027-01-01T00:00:00Z" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "token": "pnt_3f9a2c7d8e1b4a6f0c5d9e2a7b3c1f8e", "id": "018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e", "name": "backup-script", "hint": "pnt_3f9a", "expires_at": "2027-01-01T00:00:00Z", "created_at": "2026-03-14T09:26:53Z" } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"token.create","params":{"current_password":"correct-horse-battery-staple","name":"backup-script","expires_at":"2027-01-01T00:00:00Z"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "token.create", params: {"current_password":"correct-horse-battery-staple","name":"backup-script","expires_at":"2027-01-01T00:00:00Z"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"token.create","params":{"current_password":"correct-horse-battery-staple","name":"backup-script","expires_at":"2027-01-01T00:00:00Z"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # token.list # `token.list` bearer **List the caller's personal access tokens** Returns metadata for every active PAT the caller owns (never the hash or plaintext). JWT session only. ## Parameters _None._ ## Result Returns `listTokensResult`: | Field | Type | Description | |---|---|---| | `tokens *` | `array` | | ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "token.list" } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": { "tokens": [ { "id": "018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e", "name": "backup-script", "hint": "pnt_3f9a", "last_used_at": "2026-03-20T18:02:11Z", "created_at": "2026-03-14T09:26:53Z" } ] } } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"token.list"}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "token.list", params: {}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"token.list"}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # token.revoke # `token.revoke` bearer **Revoke a personal access token** Permanently revokes one PAT by id. Revoking a token does not affect the caller's other tokens or password sessions. JWT session only. ## Parameters Passed by name in the `params` object. | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | UUID of the token to revoke | ## Result Returns an empty object `{}` on success. ## Errors | Code | Message | When | |---|---|---| | `2002` | `validation_failed` | Malformed request; see error.data. | | `2001` | `not_found` | No such resource owned by the caller. | See the [error reference](/docs/errors) for the full catalog, including the authentication codes. ## Example Request: ```json { "jsonrpc": "2.0", "id": 1, "method": "token.revoke", "params": { "id": "018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e" } } ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": {} } ``` **curl** ```bash curl -s https://peinture.gumeniuk.com/rpc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"token.revoke","params":{"id":"018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e"}}' ``` **JavaScript** ```js const res = await fetch("https://peinture.gumeniuk.com/rpc", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "token.revoke", params: {"id":"018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e"}, }), }); const { result, error } = await res.json(); ``` **Go** ```go body := []byte(`{"jsonrpc":"2.0","id":1,"method":"token.revoke","params":{"id":"018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e"}}`) req, _ := http.NewRequest("POST", "https://peinture.gumeniuk.com/rpc", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) ``` --- # Terms These terms govern access to the peinture API and its documentation. ## Access The API is for use by the account holder and applications they authorize (via a password session or a personal access token). Do not attempt to access images or accounts you do not own; the API is owner-scoped and enforces this, but circumvention attempts are prohibited. ## Automated & AI-agent access Automated clients, crawlers, and AI agents are **welcome** to read this documentation and its machine-readable artifacts, and to call the API using valid credentials. - The documentation pages are available as markdown twins (append `.md` to any docs URL, or send `Accept: text/markdown`), and are indexed in [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt). - Respect the limits described in [rate limits](/docs/rate-limits); do not attempt to exhaust or degrade the service. - Present a descriptive `User-Agent`. Crawler directives are published in [`/robots.txt`](/robots.txt). ## No warranty The API and documentation are provided "as is", without warranty of any kind. The service may change; consult the [changelog](/docs/changelog) and the versioned specification. ## Contact Questions: i@gumeniuk.com.