{
  "openrpc": "1.3.2",
  "info": {
    "title": "peinture API",
    "version": "1.0.0",
    "description": "Image storage \u0026 processing. Metadata and control is JSON-RPC 2.0 over POST /rpc; raw bytes (upload, archive, image serving) are plain HTTP.",
    "contact": {
      "name": "Stanislav Gumeniuk",
      "email": "i@gumeniuk.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "name": "peinture",
      "url": "https://peinture.gumeniuk.com/rpc"
    }
  ],
  "methods": [
    {
      "name": "account.setEmail",
      "summary": "Change the caller's email address",
      "description": "Verifies the current password and updates the email. No confirmation email is sent. JWT session only.",
      "tags": [
        {
          "name": "account"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "current_password",
          "description": "The caller's current password (re-authentication)",
          "required": true,
          "schema": {
            "type": "string",
            "description": "The caller's current password (re-authentication)"
          }
        },
        {
          "name": "email",
          "description": "The new email address",
          "required": true,
          "schema": {
            "type": "string",
            "description": "The new email address"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/userResult"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "current_password",
              "value": "correct-horse-battery-staple"
            },
            {
              "name": "email",
              "value": "ada@analyticalengine.example"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "user": {
                "id": "018f3a5c-1c7a-7e3b-9c2a-3f4b5a6c7d8e",
                "email": "ada@analyticalengine.example",
                "is_admin": false
              }
            }
          }
        }
      ],
      "x-auth": "bearer"
    },
    {
      "name": "account.setPassword",
      "summary": "Change the caller's password",
      "description": "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.",
      "tags": [
        {
          "name": "account"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "current_password",
          "description": "The caller's current password (re-authentication)",
          "required": true,
          "schema": {
            "type": "string",
            "description": "The caller's current password (re-authentication)"
          }
        },
        {
          "name": "new_password",
          "description": "The new password to set",
          "required": true,
          "schema": {
            "type": "string",
            "description": "The new password to set"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/tokenPairResult"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "current_password",
              "value": "correct-horse-battery-staple"
            },
            {
              "name": "new_password",
              "value": "Tr0ub4dour\u00263-fresh"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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
              }
            }
          }
        }
      ],
      "x-auth": "bearer"
    },
    {
      "name": "album.create",
      "summary": "Create a folder",
      "description": "Creates a private, non-default folder. is_default and visibility are server-set.",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "name",
          "description": "Folder name (1-200 chars)",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Folder name (1-200 chars)"
          }
        },
        {
          "name": "description",
          "description": "Optional description",
          "schema": {
            "type": "string",
            "description": "Optional description"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/albumDTO"
        }
      },
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "name",
              "value": "Iceland 2024"
            },
            {
              "name": "description",
              "value": "Ring road, March."
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ]
    },
    {
      "name": "album.crop.delete",
      "summary": "Delete a folder crop rule (re-renders the folder)",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "album_id",
          "description": "Folder UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Folder UUID"
          }
        },
        {
          "name": "code",
          "description": "Crop code to delete",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Crop code to delete"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/jobResult"
        }
      },
      "errors": [
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "album_id",
              "value": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
            },
            {
              "name": "code",
              "value": "hero"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "job_id": 4242
            }
          }
        }
      ]
    },
    {
      "name": "album.crop.list",
      "summary": "List a folder's crop rules",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "album_id",
          "description": "Folder UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Folder UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/cropRuleListResult"
        }
      },
      "errors": [
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "album_id",
              "value": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "crops": [
                {
                  "code": "hero",
                  "width": 1200,
                  "height": 630,
                  "mode": "fill",
                  "format": "webp",
                  "quality": 82
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "album.crop.set",
      "summary": "Add or update a folder crop rule (re-renders the folder)",
      "description": "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.",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "album_id",
          "description": "Folder UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Folder UUID"
          }
        },
        {
          "name": "code",
          "description": "Crop code, ^[a-z0-9][a-z0-9-]{0,31}$",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Crop code, ^[a-z0-9][a-z0-9-]{0,31}$"
          }
        },
        {
          "name": "width",
          "description": "Target width in px (0-15000)",
          "required": true,
          "schema": {
            "type": "integer",
            "description": "Target width in px (0-15000)"
          }
        },
        {
          "name": "height",
          "description": "Target height in px (0-15000)",
          "required": true,
          "schema": {
            "type": "integer",
            "description": "Target height in px (0-15000)"
          }
        },
        {
          "name": "mode",
          "description": "Fit mode; defaults to fill",
          "schema": {
            "type": "string",
            "description": "Fit mode; defaults to fill"
          }
        },
        {
          "name": "format",
          "description": "Output format; defaults to webp",
          "schema": {
            "type": "string",
            "description": "Output format; defaults to webp"
          }
        },
        {
          "name": "quality",
          "description": "Encoder quality 1-100; defaults to 80",
          "schema": {
            "type": "integer",
            "description": "Encoder quality 1-100; defaults to 80"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/jobResult"
        }
      },
      "errors": [
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "hero",
          "params": [
            {
              "name": "album_id",
              "value": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
            },
            {
              "name": "code",
              "value": "hero"
            },
            {
              "name": "width",
              "value": 1200
            },
            {
              "name": "height",
              "value": 630
            },
            {
              "name": "mode",
              "value": "fill"
            },
            {
              "name": "format",
              "value": "webp"
            },
            {
              "name": "quality",
              "value": 82
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "job_id": 4242
            }
          }
        }
      ]
    },
    {
      "name": "album.delete",
      "summary": "Delete a folder",
      "description": "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.",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/jobResult"
        }
      },
      "errors": [
        {
          "code": 2001,
          "message": "not_found"
        },
        {
          "code": 1003,
          "message": "forbidden"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "job_id": 4242
            }
          }
        }
      ]
    },
    {
      "name": "album.list",
      "summary": "List the caller's folders (default first)",
      "description": "Returns every folder with its live-image count. The default folder is pinned first and is immutable (cannot be renamed or deleted).",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/albumListResult"
        }
      },
      "examples": [
        {
          "name": "basic",
          "params": [],
          "result": {
            "name": "result",
            "value": {
              "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"
                }
              ]
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "album.move",
      "summary": "Move a photo into a folder",
      "description": "Moves the image into the folder and re-renders it for the folder's crop rules. Returns the updated image.",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID to move",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID to move"
          }
        },
        {
          "name": "album_id",
          "description": "Destination folder UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Destination folder UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ImageDTO"
        }
      },
      "errors": [
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            },
            {
              "name": "album_id",
              "value": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "album.reorder",
      "summary": "Set the manual order of photos within a folder",
      "description": "Assigns the given order to the listed images (all must belong to the folder). Pure metadata — no re-render.",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "album_id",
          "description": "Folder UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Folder UUID"
          }
        },
        {
          "name": "ordered_ids",
          "description": "Image UUIDs in the desired order; all must belong to the folder",
          "required": true,
          "schema": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "description": "Image UUIDs in the desired order; all must belong to the folder"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/emptyResult"
        }
      },
      "errors": [
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "album_id",
              "value": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
            },
            {
              "name": "ordered_ids",
              "value": [
                "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d",
                "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1e"
              ]
            }
          ],
          "result": {
            "name": "result",
            "value": {}
          }
        }
      ]
    },
    {
      "name": "album.update",
      "summary": "Rename/re-describe a folder",
      "description": "Partial update: omitted fields are left unchanged. The default folder is immutable.",
      "tags": [
        {
          "name": "album"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Folder UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Folder UUID"
          }
        },
        {
          "name": "name",
          "description": "New name; omit to leave unchanged",
          "schema": {
            "type": "string",
            "description": "New name; omit to leave unchanged"
          }
        },
        {
          "name": "description",
          "description": "New description; omit to leave unchanged",
          "schema": {
            "type": "string",
            "description": "New description; omit to leave unchanged"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/albumDTO"
        }
      },
      "errors": [
        {
          "code": 2001,
          "message": "not_found"
        },
        {
          "code": 1003,
          "message": "forbidden"
        }
      ],
      "examples": [
        {
          "name": "rename",
          "params": [
            {
              "name": "id",
              "value": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
            },
            {
              "name": "name",
              "value": "Iceland — spring 2024"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ]
    },
    {
      "name": "auth.login",
      "summary": "Log in with email and password",
      "description": "Exchanges credentials for an access + refresh token pair. Send the access token as `Authorization: Bearer \u003ctoken\u003e` on every non-public call. Login is rate-limited per email+IP; repeated failures are throttled.",
      "tags": [
        {
          "name": "auth"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "email",
          "description": "Account email address",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Account email address"
          }
        },
        {
          "name": "password",
          "description": "Account password",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Account password"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/tokenPairResult"
        }
      },
      "errors": [
        {
          "code": 1001,
          "message": "unauthorized"
        },
        {
          "code": 2002,
          "message": "validation_failed"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "email",
              "value": "ada.lovelace@example.com"
            },
            {
              "name": "password",
              "value": "correct-horse-battery-staple"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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
              }
            }
          }
        }
      ],
      "x-auth": "public"
    },
    {
      "name": "auth.logoutAll",
      "summary": "Revoke all of the caller's sessions",
      "description": "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).",
      "tags": [
        {
          "name": "auth"
        }
      ],
      "paramStructure": "by-name",
      "params": [],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/emptyResult"
        }
      },
      "examples": [
        {
          "name": "basic",
          "params": [],
          "result": {
            "name": "result",
            "value": {}
          }
        }
      ],
      "x-auth": "bearer"
    },
    {
      "name": "auth.refresh",
      "summary": "Exchange a refresh token for a new token pair",
      "description": "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.",
      "tags": [
        {
          "name": "auth"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "refresh_token",
          "description": "A refresh token from a prior auth.login or auth.refresh",
          "required": true,
          "schema": {
            "type": "string",
            "description": "A refresh token from a prior auth.login or auth.refresh"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/tokenPairResult"
        }
      },
      "errors": [
        {
          "code": 1001,
          "message": "unauthorized"
        },
        {
          "code": 1002,
          "message": "token_expired"
        },
        {
          "code": 2002,
          "message": "validation_failed"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "refresh_token",
              "value": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.Zm9vYmFy"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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
              }
            }
          }
        }
      ],
      "x-auth": "public"
    },
    {
      "name": "image.archiveToken",
      "summary": "Mint a short-lived link to download the full archive",
      "description": "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.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/archiveResult"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.crop.add",
      "summary": "Add a custom crop (max 5 per image)",
      "description": "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.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        },
        {
          "name": "code",
          "description": "Crop code, ^[a-z0-9][a-z0-9-]{0,31}$ and not a preset code",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Crop code, ^[a-z0-9][a-z0-9-]{0,31}$ and not a preset code"
          }
        },
        {
          "name": "width",
          "description": "Target width in px (0-15000); 0 keeps aspect from height",
          "required": true,
          "schema": {
            "type": "integer",
            "description": "Target width in px (0-15000); 0 keeps aspect from height"
          }
        },
        {
          "name": "height",
          "description": "Target height in px (0-15000); 0 keeps aspect from width",
          "required": true,
          "schema": {
            "type": "integer",
            "description": "Target height in px (0-15000); 0 keeps aspect from width"
          }
        },
        {
          "name": "mode",
          "description": "Fit mode; defaults to fill",
          "schema": {
            "type": "string",
            "description": "Fit mode; defaults to fill"
          }
        },
        {
          "name": "format",
          "description": "Output format (webp/jpg/png); defaults to webp",
          "schema": {
            "type": "string",
            "description": "Output format (webp/jpg/png); defaults to webp"
          }
        },
        {
          "name": "quality",
          "description": "Encoder quality 1-100; defaults to 80",
          "schema": {
            "type": "integer",
            "description": "Encoder quality 1-100; defaults to 80"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ImageDTO"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        },
        {
          "code": 4001,
          "message": "custom_crop_limit_exceeded"
        }
      ],
      "examples": [
        {
          "name": "square",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            },
            {
              "name": "code",
              "value": "square"
            },
            {
              "name": "width",
              "value": 1080
            },
            {
              "name": "height",
              "value": 1080
            },
            {
              "name": "mode",
              "value": "fill"
            },
            {
              "name": "format",
              "value": "webp"
            },
            {
              "name": "quality",
              "value": 82
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.crop.delete",
      "summary": "Delete a custom crop by code",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        },
        {
          "name": "code",
          "description": "Crop code to delete",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Crop code to delete"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ImageDTO"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            },
            {
              "name": "code",
              "value": "square"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.delete",
      "summary": "Delete an image",
      "description": "Soft-deletes the image and asynchronously purges its objects from storage.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/emptyResult"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            }
          ],
          "result": {
            "name": "result",
            "value": {}
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.exif.setVisibility",
      "summary": "Toggle which EXIF groups are exposed publicly",
      "description": "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).",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        },
        {
          "name": "groups",
          "description": "Per-group public visibility; omit a group to leave it unchanged",
          "required": true,
          "schema": {
            "$ref": "#/components/schemas/visibilityGroups",
            "description": "Per-group public visibility; omit a group to leave it unchanged"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ImageDTO"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "hide-gps",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            },
            {
              "name": "groups",
              "value": {
                "gps": false,
                "camera": true
              }
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.get",
      "summary": "Get one of the caller's images with full detail",
      "description": "Returns the owner view: full EXIF, render URLs (with ?v= cache-buster and pending flags), custom crops, and the exif_visibility block.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ImageDTO"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.getPublic",
      "summary": "Get a ready image's public view (no authentication)",
      "description": "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.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ImageDTO"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "public"
    },
    {
      "name": "image.list",
      "summary": "List the caller's images (cursor-paginated)",
      "description": "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.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "limit",
          "description": "Max items per page (default 50)",
          "schema": {
            "type": "integer",
            "description": "Max items per page (default 50)"
          }
        },
        {
          "name": "cursor",
          "description": "Opaque cursor from a prior page's next_cursor; omit for the first page",
          "schema": {
            "type": "string",
            "description": "Opaque cursor from a prior page's next_cursor; omit for the first page"
          }
        },
        {
          "name": "album_id",
          "description": "Restrict to one folder (UUID); folder view orders by manual position first. Omit for the whole library. Keep it constant across a paginated run",
          "schema": {
            "type": "string",
            "description": "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": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/listResult"
        }
      },
      "examples": [
        {
          "name": "first-page",
          "params": [
            {
              "name": "limit",
              "value": 50
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.regenerate",
      "summary": "Re-render all sizes for an image",
      "description": "Enqueues a regeneration of every render for the current preset set. Also resets an image whose previous render failed.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/emptyResult"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            }
          ],
          "result": {
            "name": "result",
            "value": {}
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "image.update",
      "summary": "Update an image's title and/or description",
      "description": "Partial update: omitted fields are left unchanged. Returns the updated image.",
      "tags": [
        {
          "name": "image"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "Image UUID",
          "required": true,
          "schema": {
            "type": "string",
            "description": "Image UUID"
          }
        },
        {
          "name": "title",
          "description": "New title; omit to leave unchanged",
          "schema": {
            "type": "string",
            "description": "New title; omit to leave unchanged"
          }
        },
        {
          "name": "description",
          "description": "New description; omit to leave unchanged",
          "schema": {
            "type": "string",
            "description": "New description; omit to leave unchanged"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ImageDTO"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "retitle",
          "params": [
            {
              "name": "id",
              "value": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
            },
            {
              "name": "title",
              "value": "Golden Gate at first light"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "pat-ok"
    },
    {
      "name": "rpc.discover",
      "summary": "Return the OpenRPC document describing this API",
      "description": "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.",
      "tags": [
        {
          "name": "meta"
        }
      ],
      "paramStructure": "by-name",
      "params": [],
      "result": {
        "name": "OpenRPCDocument",
        "required": true,
        "schema": {
          "type": "object",
          "description": "The OpenRPC document describing this API."
        }
      },
      "x-auth": "public"
    },
    {
      "name": "token.create",
      "summary": "Create a personal access token",
      "description": "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.",
      "tags": [
        {
          "name": "token"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "current_password",
          "description": "The caller's current password (re-authentication)",
          "required": true,
          "schema": {
            "type": "string",
            "description": "The caller's current password (re-authentication)"
          }
        },
        {
          "name": "name",
          "description": "A human label for the token",
          "required": true,
          "schema": {
            "type": "string",
            "description": "A human label for the token"
          }
        },
        {
          "name": "expires_at",
          "description": "Optional RFC3339 expiry; omit for a token that never expires",
          "schema": {
            "type": "string",
            "description": "Optional RFC3339 expiry; omit for a token that never expires"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/createTokenResult"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "current_password",
              "value": "correct-horse-battery-staple"
            },
            {
              "name": "name",
              "value": "backup-script"
            },
            {
              "name": "expires_at",
              "value": "2027-01-01T00:00:00Z"
            }
          ],
          "result": {
            "name": "result",
            "value": {
              "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"
            }
          }
        }
      ],
      "x-auth": "bearer"
    },
    {
      "name": "token.list",
      "summary": "List the caller's personal access tokens",
      "description": "Returns metadata for every active PAT the caller owns (never the hash or plaintext). JWT session only.",
      "tags": [
        {
          "name": "token"
        }
      ],
      "paramStructure": "by-name",
      "params": [],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/listTokensResult"
        }
      },
      "examples": [
        {
          "name": "basic",
          "params": [],
          "result": {
            "name": "result",
            "value": {
              "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"
                }
              ]
            }
          }
        }
      ],
      "x-auth": "bearer"
    },
    {
      "name": "token.revoke",
      "summary": "Revoke a personal access token",
      "description": "Permanently revokes one PAT by id. Revoking a token does not affect the caller's other tokens or password sessions. JWT session only.",
      "tags": [
        {
          "name": "token"
        }
      ],
      "paramStructure": "by-name",
      "params": [
        {
          "name": "id",
          "description": "UUID of the token to revoke",
          "required": true,
          "schema": {
            "type": "string",
            "description": "UUID of the token to revoke"
          }
        }
      ],
      "result": {
        "name": "result",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/emptyResult"
        }
      },
      "errors": [
        {
          "code": 2002,
          "message": "validation_failed"
        },
        {
          "code": 2001,
          "message": "not_found"
        }
      ],
      "examples": [
        {
          "name": "basic",
          "params": [
            {
              "name": "id",
              "value": "018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e"
            }
          ],
          "result": {
            "name": "result",
            "value": {}
          }
        }
      ],
      "x-auth": "bearer"
    }
  ],
  "components": {
    "schemas": {
      "ImageDTO": {
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "size_bytes": {
            "type": "integer"
          },
          "format": {
            "type": "string"
          },
          "exif": {
            "$ref": "#/components/schemas/exifDTO"
          },
          "renders": {
            "items": {
              "$ref": "#/components/schemas/renderDTO"
            },
            "type": "array"
          },
          "custom_crops": {
            "items": {
              "$ref": "#/components/schemas/customCropDTO"
            },
            "type": "array"
          },
          "exif_visibility": {
            "$ref": "#/components/schemas/visibilityDTO"
          },
          "album_id": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "title",
          "description",
          "status",
          "width",
          "height",
          "size_bytes",
          "format",
          "exif",
          "renders",
          "custom_crops",
          "created_at"
        ]
      },
      "albumDTO": {
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "is_default": {
            "type": "boolean"
          },
          "visibility": {
            "type": "string"
          },
          "image_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "is_default",
          "visibility",
          "image_count",
          "created_at"
        ]
      },
      "albumListResult": {
        "properties": {
          "albums": {
            "items": {
              "$ref": "#/components/schemas/albumDTO"
            },
            "type": "array"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "albums"
        ]
      },
      "apiTokenDTO": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Token UUID"
          },
          "name": {
            "type": "string",
            "description": "Human label given at creation"
          },
          "hint": {
            "type": "string",
            "description": "First 10 characters of the secret (pnt_ + 6), to identify it"
          },
          "expires_at": {
            "type": "string",
            "description": "RFC3339 expiry, if the token expires"
          },
          "last_used_at": {
            "type": "string",
            "description": "RFC3339 timestamp of last use (throttled), if ever used"
          },
          "created_at": {
            "type": "string",
            "description": "RFC3339 creation timestamp"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "name",
          "hint",
          "created_at"
        ]
      },
      "archiveResult": {
        "properties": {
          "url": {
            "type": "string"
          },
          "expires_at": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "url",
          "expires_at"
        ]
      },
      "createTokenResult": {
        "properties": {
          "token": {
            "type": "string",
            "description": "The plaintext secret (prefix pnt_) — shown only once; store it now"
          },
          "id": {
            "type": "string",
            "description": "Token UUID"
          },
          "name": {
            "type": "string",
            "description": "Human label given at creation"
          },
          "hint": {
            "type": "string",
            "description": "First 10 characters of the secret (pnt_ + 6), to identify it"
          },
          "expires_at": {
            "type": "string",
            "description": "RFC3339 expiry, if the token expires"
          },
          "last_used_at": {
            "type": "string",
            "description": "RFC3339 timestamp of last use (throttled), if ever used"
          },
          "created_at": {
            "type": "string",
            "description": "RFC3339 creation timestamp"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "token",
          "id",
          "name",
          "hint",
          "created_at"
        ]
      },
      "cropRuleDTO": {
        "properties": {
          "code": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "mode": {
            "type": "string"
          },
          "format": {
            "type": "string"
          },
          "quality": {
            "type": "integer"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "code",
          "width",
          "height",
          "mode",
          "format",
          "quality"
        ]
      },
      "cropRuleListResult": {
        "properties": {
          "crops": {
            "items": {
              "$ref": "#/components/schemas/cropRuleDTO"
            },
            "type": "array"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "crops"
        ]
      },
      "customCropDTO": {
        "properties": {
          "code": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "mode": {
            "type": "string"
          },
          "format": {
            "type": "string"
          },
          "quality": {
            "type": "integer"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "code",
          "width",
          "height",
          "mode",
          "format",
          "quality"
        ]
      },
      "emptyResult": {
        "properties": {},
        "additionalProperties": false,
        "type": "object"
      },
      "exifDTO": {
        "properties": {
          "taken_at": {
            "type": "string"
          },
          "camera_make": {
            "type": "string"
          },
          "camera_model": {
            "type": "string"
          },
          "lens": {
            "type": "string"
          },
          "gps_lat": {
            "type": "number"
          },
          "gps_lon": {
            "type": "number"
          }
        },
        "additionalProperties": false,
        "type": "object"
      },
      "imageSummary": {
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "thumb_url": {
            "type": "string"
          },
          "album_id": {
            "type": "string"
          },
          "taken_at": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "title",
          "status",
          "width",
          "height",
          "created_at"
        ]
      },
      "jobResult": {
        "properties": {
          "job_id": {
            "type": "integer"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "job_id"
        ]
      },
      "listResult": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/imageSummary"
            },
            "type": "array"
          },
          "next_cursor": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "items",
          "next_cursor"
        ]
      },
      "listTokensResult": {
        "properties": {
          "tokens": {
            "items": {
              "$ref": "#/components/schemas/apiTokenDTO"
            },
            "type": "array"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "tokens"
        ]
      },
      "renderDTO": {
        "properties": {
          "crop": {
            "type": "string"
          },
          "format": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "size_bytes": {
            "type": "integer"
          },
          "url": {
            "type": "string"
          },
          "pending": {
            "type": "boolean"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "crop",
          "pending"
        ]
      },
      "tokenPairResult": {
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Short-lived bearer token (15-minute TTL); send as Authorization: Bearer \u003ctoken\u003e"
          },
          "refresh_token": {
            "type": "string",
            "description": "Long-lived token used with auth.refresh to obtain a new pair"
          },
          "access_expires_at": {
            "type": "string",
            "description": "RFC3339 expiry of the access token"
          },
          "user": {
            "$ref": "#/components/schemas/userDTO"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "access_token",
          "refresh_token",
          "access_expires_at",
          "user"
        ]
      },
      "userDTO": {
        "properties": {
          "id": {
            "type": "string",
            "description": "User UUID"
          },
          "email": {
            "type": "string",
            "description": "User email address"
          },
          "is_admin": {
            "type": "boolean",
            "description": "Whether the user has the admin role"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "email",
          "is_admin"
        ]
      },
      "userResult": {
        "properties": {
          "user": {
            "$ref": "#/components/schemas/userDTO"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "user"
        ]
      },
      "visibilityDTO": {
        "properties": {
          "gps": {
            "type": "boolean"
          },
          "camera": {
            "type": "boolean"
          },
          "datetime": {
            "type": "boolean"
          },
          "settings": {
            "type": "boolean"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "gps",
          "camera",
          "datetime",
          "settings"
        ]
      },
      "visibilityGroups": {
        "properties": {
          "gps": {
            "type": "boolean",
            "description": "Expose GPS coordinates (as a unit)"
          },
          "camera": {
            "type": "boolean",
            "description": "Expose camera make/model/lens"
          },
          "datetime": {
            "type": "boolean",
            "description": "Expose capture date/time"
          },
          "settings": {
            "type": "boolean",
            "description": "Expose shooting settings"
          }
        },
        "additionalProperties": false,
        "type": "object"
      }
    }
  }
}