{
  "openapi": "3.1.0",
  "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": [
    {
      "url": "https://peinture.gumeniuk.com"
    }
  ],
  "paths": {
    "/api/v1/images": {
      "post": {
        "operationId": "uploadImage",
        "summary": "Upload an image (multipart)",
        "description": "Streams an original into private storage and enqueues its renders. The `file` part is required; the optional `meta` part is a JSON object {title, description}.",
        "tags": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "file": {
                    "description": "The image file (png, jpg, or webp).",
                    "format": "binary",
                    "type": "string"
                  },
                  "meta": {
                    "description": "Optional JSON object: {\"title\":\"…\",\"description\":\"…\"}.",
                    "type": "string"
                  }
                },
                "required": [
                  "file"
                ],
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created image (owner view).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageDTO"
                }
              }
            }
          },
          "400": {
            "description": "Missing, empty, or unsupported image."
          },
          "401": {
            "description": "Missing or invalid bearer token."
          },
          "413": {
            "description": "Upload exceeds the size limit."
          }
        }
      }
    },
    "/api/v1/images/{id}/archive": {
      "get": {
        "operationId": "downloadArchive",
        "summary": "Download a zip of all sizes + the original",
        "description": "The token comes from image.archiveToken (5-minute, owner-scoped). Ready images only.",
        "tags": [
          "image"
        ],
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Image UUID",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "Archive token from image.archiveToken",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The zip archive.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token."
          },
          "404": {
            "description": "No such image."
          },
          "409": {
            "description": "Image is not ready."
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "healthz",
        "summary": "Liveness probe",
        "tags": [
          "ops"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/i/{id}/{file}": {
      "get": {
        "operationId": "serveRender",
        "summary": "Proxy-serve a render through the app",
        "description": "Streams a public render (never an original). `file` is `{crop}.{ext}`, e.g. `large.webp`. What proxy-mode render URLs point at; always available as a fallback.",
        "tags": [
          "image"
        ],
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Image UUID",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "crop and extension, e.g. large.webp",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The render bytes.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No such image or render."
          }
        }
      }
    },
    "/readyz": {
      "get": {
        "operationId": "readyz",
        "summary": "Readiness probe (pings the database)",
        "tags": [
          "ops"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Dependencies reachable."
          },
          "503": {
            "description": "A dependency is unreachable."
          }
        }
      }
    },
    "/rpc/account.setEmail": {
      "post": {
        "operationId": "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": [
          "account"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "account.setEmail",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/setEmailParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "account.setEmail",
                "params": {
                  "current_password": "correct-horse-battery-staple",
                  "email": "ada@analyticalengine.example"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/userResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {
                    "user": {
                      "id": "018f3a5c-1c7a-7e3b-9c2a-3f4b5a6c7d8e",
                      "email": "ada@analyticalengine.example",
                      "is_admin": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/account.setPassword": {
      "post": {
        "operationId": "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": [
          "account"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "account.setPassword",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/setPasswordParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "account.setPassword",
                "params": {
                  "current_password": "correct-horse-battery-staple",
                  "new_password": "Tr0ub4dour\u00263-fresh"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/tokenPairResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.create": {
      "post": {
        "operationId": "album_create",
        "summary": "Create a folder",
        "description": "Creates a private, non-default folder. is_default and visibility are server-set.",
        "tags": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.create",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/albumCreateParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.create",
                "params": {
                  "name": "Iceland 2024",
                  "description": "Ring road, March."
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/albumDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.crop.delete": {
      "post": {
        "operationId": "album_crop_delete",
        "summary": "Delete a folder crop rule (re-renders the folder)",
        "tags": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.crop.delete",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/albumCropDeleteParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.crop.delete",
                "params": {
                  "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50",
                  "code": "hero"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/jobResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {
                    "job_id": 4242
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.crop.list": {
      "post": {
        "operationId": "album_crop_list",
        "summary": "List a folder's crop rules",
        "tags": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.crop.list",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/albumCropListParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.crop.list",
                "params": {
                  "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/cropRuleListResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {
                    "crops": [
                      {
                        "code": "hero",
                        "width": 1200,
                        "height": 630,
                        "mode": "fill",
                        "format": "webp",
                        "quality": 82
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.crop.set": {
      "post": {
        "operationId": "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": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.crop.set",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/albumCropSetParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.crop.set",
                "params": {
                  "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50",
                  "code": "hero",
                  "width": 1200,
                  "height": 630,
                  "mode": "fill",
                  "format": "webp",
                  "quality": 82
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/jobResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {
                    "job_id": 4242
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.delete": {
      "post": {
        "operationId": "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": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.delete",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/idParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.delete",
                "params": {
                  "id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/jobResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {
                    "job_id": 4242
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.list": {
      "post": {
        "operationId": "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": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.list",
                    "type": "string"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.list"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/albumListResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.move": {
      "post": {
        "operationId": "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": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.move",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/albumMoveParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.move",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d",
                  "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/ImageDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.reorder": {
      "post": {
        "operationId": "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": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.reorder",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/albumReorderParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.reorder",
                "params": {
                  "album_id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50",
                  "ordered_ids": [
                    "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d",
                    "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1e"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/emptyResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {}
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/album.update": {
      "post": {
        "operationId": "album_update",
        "summary": "Rename/re-describe a folder",
        "description": "Partial update: omitted fields are left unchanged. The default folder is immutable.",
        "tags": [
          "album"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "album.update",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/albumUpdateParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "album.update",
                "params": {
                  "id": "018f3a80-1c2d-7e00-9a00-0b1c2d3e4f50",
                  "name": "Iceland — spring 2024"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/albumDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/auth.login": {
      "post": {
        "operationId": "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": [
          "auth"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "auth.login",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/loginParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "auth.login",
                "params": {
                  "email": "ada.lovelace@example.com",
                  "password": "correct-horse-battery-staple"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/tokenPairResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/auth.logoutAll": {
      "post": {
        "operationId": "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": [
          "auth"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "auth.logoutAll",
                    "type": "string"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "auth.logoutAll"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/emptyResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {}
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/auth.refresh": {
      "post": {
        "operationId": "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": [
          "auth"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "auth.refresh",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/refreshParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "auth.refresh",
                "params": {
                  "refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXAiOiJyZWZyZXNoIn0.Zm9vYmFy"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/tokenPairResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.archiveToken": {
      "post": {
        "operationId": "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": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.archiveToken",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/idParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.archiveToken",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/archiveResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.crop.add": {
      "post": {
        "operationId": "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": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.crop.add",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/cropAddParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.crop.add",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d",
                  "code": "square",
                  "width": 1080,
                  "height": 1080,
                  "mode": "fill",
                  "format": "webp",
                  "quality": 82
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/ImageDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.crop.delete": {
      "post": {
        "operationId": "image_crop_delete",
        "summary": "Delete a custom crop by code",
        "tags": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.crop.delete",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/cropDeleteParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.crop.delete",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d",
                  "code": "square"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/ImageDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.delete": {
      "post": {
        "operationId": "image_delete",
        "summary": "Delete an image",
        "description": "Soft-deletes the image and asynchronously purges its objects from storage.",
        "tags": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.delete",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/idParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.delete",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/emptyResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {}
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.exif.setVisibility": {
      "post": {
        "operationId": "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": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.exif.setVisibility",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/visibilityParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.exif.setVisibility",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d",
                  "groups": {
                    "gps": false,
                    "camera": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/ImageDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.get": {
      "post": {
        "operationId": "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": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.get",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/idParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.get",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/ImageDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.getPublic": {
      "post": {
        "operationId": "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": [
          "image"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.getPublic",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/idParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.getPublic",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/ImageDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.list": {
      "post": {
        "operationId": "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": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.list",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/listParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.list",
                "params": {
                  "limit": 50
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/listResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.regenerate": {
      "post": {
        "operationId": "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": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.regenerate",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/idParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.regenerate",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/emptyResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {}
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/image.update": {
      "post": {
        "operationId": "image_update",
        "summary": "Update an image's title and/or description",
        "description": "Partial update: omitted fields are left unchanged. Returns the updated image.",
        "tags": [
          "image"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "image.update",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/updateParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "image.update",
                "params": {
                  "id": "018f3a7d-2b8c-7f1a-a4d5-6e7f8a9b0c1d",
                  "title": "Golden Gate at first light"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/ImageDTO"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/rpc.discover": {
      "post": {
        "operationId": "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": [
          "meta"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "rpc.discover",
                    "type": "string"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "type": "object",
                          "description": "The OpenRPC document describing this API."
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/token.create": {
      "post": {
        "operationId": "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": [
          "token"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "token.create",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/createTokenParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "token.create",
                "params": {
                  "current_password": "correct-horse-battery-staple",
                  "name": "backup-script",
                  "expires_at": "2027-01-01T00:00:00Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/createTokenResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/token.list": {
      "post": {
        "operationId": "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": [
          "token"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "token.list",
                    "type": "string"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "token.list"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/listTokensResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "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"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    },
    "/rpc/token.revoke": {
      "post": {
        "operationId": "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": [
          "token"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ]
                  },
                  "jsonrpc": {
                    "const": "2.0",
                    "type": "string"
                  },
                  "method": {
                    "const": "token.revoke",
                    "type": "string"
                  },
                  "params": {
                    "$ref": "#/components/schemas/revokeTokenParams"
                  }
                },
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "type": "object"
              },
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "token.revoke",
                "params": {
                  "id": "018f3a8e-3c9d-7a2b-b5e6-7f8a9b0c1d2e"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response — a result object on success, or an error object on failure (HTTP status is 200 either way).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "integer",
                            "null"
                          ]
                        },
                        "jsonrpc": {
                          "const": "2.0",
                          "type": "string"
                        },
                        "result": {
                          "$ref": "#/components/schemas/emptyResult"
                        }
                      },
                      "required": [
                        "jsonrpc",
                        "result"
                      ],
                      "type": "object"
                    },
                    {
                      "$ref": "#/components/schemas/JsonRpcError"
                    }
                  ]
                },
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {}
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body, or a batch array (not allowed on /rpc/{method})."
          }
        }
      }
    }
  },
  "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"
        ]
      },
      "JsonRpcError": {
        "description": "A JSON-RPC 2.0 error response. See the errors documented per method for the code values.",
        "properties": {
          "error": {
            "properties": {
              "code": {
                "type": "integer"
              },
              "data": {
                "description": "Optional client-visible detail (validation info, limits)."
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ],
            "type": "object"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "jsonrpc": {
            "const": "2.0",
            "type": "string"
          }
        },
        "required": [
          "jsonrpc",
          "error"
        ],
        "type": "object"
      },
      "albumCreateParams": {
        "properties": {
          "name": {
            "type": "string",
            "description": "Folder name (1-200 chars)"
          },
          "description": {
            "type": "string",
            "description": "Optional description"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "name"
        ]
      },
      "albumCropDeleteParams": {
        "properties": {
          "album_id": {
            "type": "string",
            "description": "Folder UUID"
          },
          "code": {
            "type": "string",
            "description": "Crop code to delete"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "album_id",
          "code"
        ]
      },
      "albumCropListParams": {
        "properties": {
          "album_id": {
            "type": "string",
            "description": "Folder UUID"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "album_id"
        ]
      },
      "albumCropSetParams": {
        "properties": {
          "album_id": {
            "type": "string",
            "description": "Folder UUID"
          },
          "code": {
            "type": "string",
            "description": "Crop code, ^[a-z0-9][a-z0-9-]{0,31}$"
          },
          "width": {
            "type": "integer",
            "description": "Target width in px (0-15000)"
          },
          "height": {
            "type": "integer",
            "description": "Target height in px (0-15000)"
          },
          "mode": {
            "type": "string",
            "description": "Fit mode; defaults to fill"
          },
          "format": {
            "type": "string",
            "description": "Output format; defaults to webp"
          },
          "quality": {
            "type": "integer",
            "description": "Encoder quality 1-100; defaults to 80"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "album_id",
          "code",
          "width",
          "height"
        ]
      },
      "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"
        ]
      },
      "albumMoveParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Image UUID to move"
          },
          "album_id": {
            "type": "string",
            "description": "Destination folder UUID"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "album_id"
        ]
      },
      "albumReorderParams": {
        "properties": {
          "album_id": {
            "type": "string",
            "description": "Folder UUID"
          },
          "ordered_ids": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "description": "Image UUIDs in the desired order; all must belong to the folder"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "album_id",
          "ordered_ids"
        ]
      },
      "albumUpdateParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Folder UUID"
          },
          "name": {
            "type": "string",
            "description": "New name; omit to leave unchanged"
          },
          "description": {
            "type": "string",
            "description": "New description; omit to leave unchanged"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id"
        ]
      },
      "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"
        ]
      },
      "createTokenParams": {
        "properties": {
          "current_password": {
            "type": "string",
            "description": "The caller's current password (re-authentication)"
          },
          "name": {
            "type": "string",
            "description": "A human label for the token"
          },
          "expires_at": {
            "type": "string",
            "description": "Optional RFC3339 expiry; omit for a token that never expires"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "current_password",
          "name"
        ]
      },
      "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"
        ]
      },
      "cropAddParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Image UUID"
          },
          "code": {
            "type": "string",
            "description": "Crop code, ^[a-z0-9][a-z0-9-]{0,31}$ and not a preset code"
          },
          "width": {
            "type": "integer",
            "description": "Target width in px (0-15000); 0 keeps aspect from height"
          },
          "height": {
            "type": "integer",
            "description": "Target height in px (0-15000); 0 keeps aspect from width"
          },
          "mode": {
            "type": "string",
            "description": "Fit mode; defaults to fill"
          },
          "format": {
            "type": "string",
            "description": "Output format (webp/jpg/png); defaults to webp"
          },
          "quality": {
            "type": "integer",
            "description": "Encoder quality 1-100; defaults to 80"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "code",
          "width",
          "height"
        ]
      },
      "cropDeleteParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Image UUID"
          },
          "code": {
            "type": "string",
            "description": "Crop code to delete"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "code"
        ]
      },
      "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"
      },
      "idParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Image UUID"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id"
        ]
      },
      "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"
        ]
      },
      "listParams": {
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Max items per page (default 50)"
          },
          "cursor": {
            "type": "string",
            "description": "Opaque cursor from a prior page's next_cursor; omit for the first page"
          },
          "album_id": {
            "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"
          }
        },
        "additionalProperties": false,
        "type": "object"
      },
      "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"
        ]
      },
      "loginParams": {
        "properties": {
          "email": {
            "type": "string",
            "description": "Account email address"
          },
          "password": {
            "type": "string",
            "description": "Account password"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "email",
          "password"
        ]
      },
      "refreshParams": {
        "properties": {
          "refresh_token": {
            "type": "string",
            "description": "A refresh token from a prior auth.login or auth.refresh"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "refresh_token"
        ]
      },
      "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"
        ]
      },
      "revokeTokenParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "UUID of the token to revoke"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id"
        ]
      },
      "setEmailParams": {
        "properties": {
          "current_password": {
            "type": "string",
            "description": "The caller's current password (re-authentication)"
          },
          "email": {
            "type": "string",
            "description": "The new email address"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "current_password",
          "email"
        ]
      },
      "setPasswordParams": {
        "properties": {
          "current_password": {
            "type": "string",
            "description": "The caller's current password (re-authentication)"
          },
          "new_password": {
            "type": "string",
            "description": "The new password to set"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "current_password",
          "new_password"
        ]
      },
      "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"
        ]
      },
      "updateParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Image UUID"
          },
          "title": {
            "type": "string",
            "description": "New title; omit to leave unchanged"
          },
          "description": {
            "type": "string",
            "description": "New description; omit to leave unchanged"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id"
        ]
      },
      "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"
      },
      "visibilityParams": {
        "properties": {
          "id": {
            "type": "string",
            "description": "Image UUID"
          },
          "groups": {
            "$ref": "#/components/schemas/visibilityGroups",
            "description": "Per-group public visibility; omit a group to leave it unchanged"
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "id",
          "groups"
        ]
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "description": "A JWT access token (from auth.login) or a personal access token (pnt_...).",
        "scheme": "bearer",
        "type": "http"
      }
    }
  }
}