{
  "openapi": "3.1.0",
  "info": {
    "title": "FindMe Photo API",
    "version": "1.0.0",
    "description": "The FindMe Photo API lets a photographer (or an AI assistant acting for one) manage event albums programmatically: create albums, upload photos and videos, import from Google Drive, read usage and guest analytics, and get the share link and QR code. Guests open the album's `gallery_url` and find their own photos with a selfie; the guest side is not part of this API.\n\n## Authentication\nEvery request needs a personal API key, created self-serve in the FindMe dashboard (https://findme.photo/photographer, then Connections, then API Keys), sent as:\n\n```\nAuthorization: Bearer fm_live_...\n```\n\nA missing or malformed header returns `401 missing_or_invalid_token`; an unknown or revoked key returns `401 invalid_token`.\n\n## Which plans include API access\nAccess is decided per plan by the `has_ai_assistant` flag on the plan, checked on every request. Today that includes the 30-day **Free+** trial and the paid **Studio**, **Growth** and **Pro** plans. An expired trial does not: those requests return `403 tier_not_eligible` with an `upgrade_url`. Some endpoints need an extra plan feature (analytics, Google Drive import) and return `403 tier_not_eligible` when the plan lacks it.\n\n## Rate limits and monthly quota\nEach API key has a per-minute request limit (sliding one-minute window) and a monthly request quota. Both numbers come from your plan, and some plans have no monthly cap; `GET /me/usage` returns your current values (`api.rate_limit_per_minute`, `api.requests_limit`).\n\n- Over the per-minute limit: `429 rate_limited`, body field `retry_after_seconds`.\n- Over the monthly quota: `429 quota_exceeded`, body fields `retry_after_seconds` and `resets_at`. The quota is counted per API key per calendar month and resets at 00:00 UTC on the 1st.\n- Both 429 responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (Unix seconds) and `Retry-After` (seconds). Successful responses do not carry rate-limit headers.\n- Every request that passes authentication and the per-minute limit counts toward the monthly quota, including requests that then fail validation.\n\n## Response envelope\nSuccess: `{\"data\": ..., \"meta\": {\"request_id\": \"req_...\"}}`. List endpoints add `meta.has_more` and, when there is another page, `meta.next_cursor`; pass it back as `?cursor=`. Every response carries an `X-Request-Id` header.\n\nErrors: `{\"error\": {\"code\", \"message\", \"request_id\", \"docs_url\", \"details\"?, ...}}`. Some errors add fields on the error object, such as `upgrade_url`, `retry_after_seconds`, `resets_at`, `requested`, `plan_max`. Validation failures put `details.issues` (`[{path, message}]`) on a `400 invalid_request`.\n\n| code | HTTP |\n|---|---|\n| missing_or_invalid_token, invalid_token | 401 |\n| upgrade_required | 402 |\n| tier_not_eligible, forbidden | 403 |\n| not_found | 404 |\n| conflict | 409 |\n| gone | 410 |\n| invalid_request | 400 |\n| rate_limited, quota_exceeded | 429 |\n| drive_not_connected | 428 |\n| internal_error | 500 (502 when an upstream service is unreachable) |\n\n## MCP server for AI assistants\nThe same operations are available to AI assistants through an MCP server at https://findme.photo/api/mcp (Streamable HTTP). OAuth discovery: https://findme.photo/.well-known/oauth-protected-resource.",
    "termsOfService": "https://findme.photo/terms",
    "contact": {
      "name": "FindMe Photo",
      "email": "info@findme.photo",
      "url": "https://findme.photo/developers"
    }
  },
  "externalDocs": {
    "description": "Developer and AI-agent docs: access, plans, MCP server, errors",
    "url": "https://findme.photo/developers"
  },
  "servers": [
    {
      "url": "https://findme.photo/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Who the key belongs to, plan limits and usage."
    },
    {
      "name": "Events",
      "description": "Albums (events): create, read, update, delete, restore, QR code, analytics."
    },
    {
      "name": "Photos",
      "description": "Upload (presigned S3 PUT flow), list and delete photos and videos."
    },
    {
      "name": "Drive imports",
      "description": "Import photos from a Google Drive folder and track progress."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Get the authenticated photographer",
        "description": "Returns who the API key belongs to: user id, email, display name, plan (internal `tier` code and `tier_display_name`), subscription status and period end. Call it to check a key works. Also returns the Lightroom plugin update channel: `plugin.update_available` is true only when the caller reported a plugin version (via `X-FindMe-Plugin-Version` or a `findme-lightroom/x.y.z` User-Agent) older than `plugin.latest_version`.",
        "parameters": [
          {
            "name": "X-FindMe-Plugin-Version",
            "in": "header",
            "required": false,
            "description": "Lightroom plugin version (e.g. `0.1.17`). Echoed back as `plugin.current_version`. Other clients omit it.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+\\.\\d+\\.\\d+"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The authenticated photographer.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Me"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/me/account": {
      "get": {
        "operationId": "getAccount",
        "tags": [
          "Account"
        ],
        "summary": "Get account and Google Drive connection summary",
        "description": "Call once at the start of a session to confirm which FindMe account, plan and Google Drive connection are in play. `accounts_match` is false when the FindMe email and the connected Drive Google email differ (Drive imports then only see folders that Drive account can access), and null when Drive is not connected. `hint` is a ready-to-show sentence, or null when nothing needs attention. `drive.tier_allows_drive` says whether the plan includes Drive imports.",
        "responses": {
          "200": {
            "description": "Account summary.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Account"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/me/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Account"
        ],
        "summary": "Get current usage against plan limits",
        "description": "Returns the current calendar month (UTC), API requests used this month, the monthly request limit (null = unlimited) and per-minute rate limit, storage used vs. the plan limit in bytes, and active albums vs. the plan album limit (null = unlimited). `api.requests_used` is the sum over all of the account's API keys; the monthly quota itself is enforced per key. Active albums excludes deleted and expired albums.",
        "responses": {
          "200": {
            "description": "Usage for the current period.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Usage"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/me/tier_limits": {
      "get": {
        "operationId": "getTierLimits",
        "tags": [
          "Account"
        ],
        "summary": "Get what the caller's plan allows",
        "description": "Returns the caller's plan limits and feature flags. `allowed_album_qualities` lists every album quality the plan may use (all options up to `max_album_quality`); use it to pick `album_quality` before `createEvent`. The row is chosen by the account's region (`in` for India, otherwise `us`).",
        "responses": {
          "200": {
            "description": "Plan limits.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TierLimits"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events": {
      "get": {
        "operationId": "listEvents",
        "tags": [
          "Events"
        ],
        "summary": "List albums",
        "description": "Lists the caller's albums (events), newest first, excluding deleted ones. Cursor-paginated: when `meta.has_more` is true, pass `meta.next_cursor` as `cursor` to get the next page. `quality_locked` is true once the album has any photo, after which `album_quality` cannot change.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from `meta.next_cursor` of the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "required": false,
            "description": "Only albums created strictly after this ISO 8601 date-time (offset allowed).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of albums.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ListMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createEvent",
        "tags": [
          "Events"
        ],
        "summary": "Create an album",
        "description": "Creates an album (event) and returns it with its `gallery_url` (the link guests open) and `qr_url`. Defaults when omitted: `album_quality` 3000px, downloads enabled, not collaborative. The album count limit and the album-quality ceiling of the plan are enforced: exceeding either returns `402 upgrade_required` with `upgrade_url` (plus `current_count`/`limit`, or `requested`/`plan_max`). Choose `album_quality` carefully: it is locked once the first photo is uploaded. Not idempotent: each successful call creates a new album.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEventRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created album.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Event"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`conflict`: access code collision. Retry the request.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "get": {
        "operationId": "getEvent",
        "tags": [
          "Events"
        ],
        "summary": "Get an album with stats",
        "description": "Returns one album you own, with `stats`: photo and video counts, storage used in bytes (counted at uploaded size), and, only when the plan includes analytics, `guest_visits` and `selfie_searches` totals. Deleted albums return 404.",
        "responses": {
          "200": {
            "description": "The album.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EventDetail"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateEvent",
        "tags": [
          "Events"
        ],
        "summary": "Update an album",
        "description": "Partially updates an album you own; send at least one field. Unknown fields are rejected. Send `null` for `event_date`, `description` or `tag` to clear them. Changing `access_code` changes the guest link; a taken code returns `409 conflict`. `album_quality` can only change while the album has no photos (otherwise `409 conflict` with `details.current_quality` and `details.photo_count`) and only to a quality the plan allows (otherwise `402 upgrade_required` with `requested` and `plan_max`). The response does not include `stats`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateEventRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated album (without `stats`).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EventDetail"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteEvent",
        "tags": [
          "Events"
        ],
        "summary": "Delete an album (recoverable for 7 days)",
        "description": "Soft-deletes an album you own. It disappears from lists and lookups immediately and can be brought back with `restoreEvent` within 7 days. Returns 204 with no body.",
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/qr": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "get": {
        "operationId": "getEventQrCode",
        "tags": [
          "Events"
        ],
        "summary": "Get a PNG QR code for the album's guest link",
        "description": "Returns a square PNG QR code that opens the album's guest link (the same host as `gallery_url`, with `?src=qr` appended). Requires the same Bearer key as every other endpoint, so it cannot be hot-linked directly in a public page.",
        "parameters": [
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Image width in pixels.",
            "schema": {
              "type": "integer",
              "minimum": 128,
              "maximum": 2048,
              "default": 512
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "description": "`private, max-age=300`",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/png"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/restore": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "post": {
        "operationId": "restoreEvent",
        "tags": [
          "Events"
        ],
        "summary": "Restore a deleted album",
        "description": "Restores an album you deleted, within 7 days of deletion. No request body. Returns `409 conflict` if the album is not deleted, and `410 gone` (with `deleted_at` and `window_days`) once the 7-day window has passed.",
        "responses": {
          "200": {
            "description": "The restored album.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/RestoredEvent"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "410": {
            "description": "`gone`: the album is past the 7-day recovery window. The error object includes `deleted_at` and `window_days`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/analytics": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "get": {
        "operationId": "getEventAnalytics",
        "tags": [
          "Events"
        ],
        "summary": "Get guest analytics for an album",
        "description": "Returns guest visits, unique visitors, photo downloads and selfie searches for an album, as totals and per day, over a date window. `from` defaults to the album's creation date and `to` to today (UTC); both are inclusive. Requires a plan that includes analytics, otherwise `403 tier_not_eligible`. The `unique_visitors` total is the sum of the daily figures.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start, YYYY-MM-DD (inclusive).",
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end, YYYY-MM-DD (inclusive).",
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics for the window.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EventAnalytics"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/photos": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "get": {
        "operationId": "listPhotos",
        "tags": [
          "Photos"
        ],
        "summary": "List photos and videos in an album",
        "description": "Lists an album's photos and videos, newest first, cursor-paginated like `listEvents`. Without `status`, the list hides failed uploads and unfinished upload slots older than 15 minutes, but keeps recent `pending` slots, so it shows what is already in the album (use it to skip files before uploading). Pass `status=pending` or `status=error` to see the hidden rows.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from `meta.next_cursor` of the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only rows with this processing status.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "completed",
                "error"
              ]
            }
          },
          {
            "name": "media_type",
            "in": "query",
            "required": false,
            "description": "Only photos or only videos.",
            "schema": {
              "type": "string",
              "enum": [
                "photo",
                "video"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of photos.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Photo"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ListMeta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createPhotoUploads",
        "tags": [
          "Photos"
        ],
        "summary": "Step 1 of upload: get presigned upload URLs",
        "description": "Reserves one upload slot per file (up to 50 per call) and returns a presigned S3 URL for each. The full upload flow:\n\n1. **This call.** Send `filename`, `content_type` and `size_bytes` for each file. Accepted types: JPEG, PNG, WebP photos (up to 50 MB each) and MP4, QuickTime, WebM videos (up to 1 GB each); any other type or size rejects the whole batch with `400 invalid_request` and `details.rejected`. The plan's storage limit (and video storage limit, on plans that have one) is checked against the declared sizes; going over returns `429 quota_exceeded` with the byte counts in `details`.\n2. **PUT the bytes.** For each slot, `PUT` the raw file bytes to `presigned_url` with the same `Content-Type` you declared, before `expires_at` (15 minutes). This request goes straight to S3: do not send your FindMe Authorization header. Resize photos to the album quality first: the long edge must not exceed 800, 3000 or 4000 px for those qualities, or 8000 px for `15mb` (the response repeats the album's `album_quality`).\n3. **Confirm** the uploaded slots with `confirmPhotoUploads`. Until then the slot stays `pending` and the photo is not in the album.\n4. **Release** any slot you will not fill (cancelled export, failed PUT) with `releasePhotoUploads`.\n\n`batch_id` is informational; no other endpoint takes it. Not idempotent: every call reserves new slots.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePhotoUploadsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload slots with presigned URLs.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PhotoUploadBatch"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "description": "`rate_limited` or `quota_exceeded` from the request gate (see TooManyRequests), or `quota_exceeded` because the batch would exceed the plan's storage limit (`details.storage_used_bytes`, `details.storage_limit_bytes`, `details.batch_bytes`) or video storage limit (`details.video_storage_used_bytes`, `details.video_storage_limit_bytes`, `details.batch_video_bytes`). The storage case carries no rate-limit headers.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/photos/confirm": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "post": {
        "operationId": "confirmPhotoUploads",
        "tags": [
          "Photos"
        ],
        "summary": "Step 3 of upload: confirm uploaded files",
        "description": "Confirms slots whose bytes you have PUT to S3 (up to 50 per call). For each id the server checks the file exists in S3, records its real size, and then:\n\n- photos move to `indexing` and face indexing starts in the background (asynchronous; the photo becomes findable by selfie once indexing finishes);\n- videos move straight to `completed`.\n\nResults come back per photo in `confirmed[]` with HTTP 200 even when some items fail; check each `status`/`reason`. Failure reasons: `not_found`, `not_owned`, `already_processed` (status is then the current one), `s3_object_missing` (the PUT never landed), `upload_exceeds_size_limit` (over 100 MB for a photo or 1 GB for a video; the file is deleted), `video_storage_limit_exceeded`, `video_storage_check_failed`, `size_exceeds_album_quality: ...`, `db_update_failed: ...`.\n\nOptional `dimensions`: send the rendered `width`/`height` per photo when you resized before upload. They are stored and checked against the album quality (long edge over the cap by more than 2 px fails with `size_exceeds_album_quality`). Without them the server reads the dimensions from the stored JPEG or PNG and applies no quality check. `original_size_bytes` (the file size before you resized it) is what the plan's storage meter counts, never less than the stored size.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmPhotoUploadsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-photo confirmation results.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PhotoConfirmBatch"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/photos/release": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "post": {
        "operationId": "releasePhotoUploads",
        "tags": [
          "Photos"
        ],
        "summary": "Step 4 of upload: release unused upload slots",
        "description": "Gives back upload slots from `createPhotoUploads` that you will not fill (cancelled export, failed PUT), so they do not linger as blank entries. Only slots still `pending` in this album, reserved by you, are removed; any other id (already confirmed, unknown, another album) is left untouched and returned in `skipped`. A confirmed photo is never deleted here; use `deletePhoto`. Any file that did land in S3 for a released slot is deleted in the background.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReleasePhotoUploadsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Which ids were released and which were skipped.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PhotoReleaseResult"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/photos/{photoId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        },
        {
          "name": "photoId",
          "in": "path",
          "required": true,
          "description": "Photo or video id.",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "delete": {
        "operationId": "deletePhoto",
        "tags": [
          "Photos"
        ],
        "summary": "Delete a photo or video",
        "description": "Permanently deletes one photo or video from an album you own. There is no recovery window. The stored files are removed from S3 in the background. Returns 204 with no body.",
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/events/{id}/drive_imports": {
      "parameters": [
        {
          "$ref": "#/components/parameters/EventId"
        }
      ],
      "post": {
        "operationId": "startDriveImport",
        "tags": [
          "Drive imports"
        ],
        "summary": "Import photos from a Google Drive folder",
        "description": "Starts a background import of the images in a Google Drive folder into the album, and returns an `import_id` to poll with `getDriveImport` (also given as `poll_url`). Requirements: the plan includes Google Drive imports (`403 tier_not_eligible` otherwise) and Google Drive is connected on the FindMe account (`428 drive_not_connected` otherwise, with `connect_url`). Works for albums you own or collaborate on.\n\nNote for AI assistants: FindMe uses Google's drive.file permission, which only lets FindMe read files the photographer picked in the FindMe web app, so a folder ID from chat may find no files. Prefer sending the photographer to Import from Drive on the album in the web app.\n\nBehaviour: HEIC files are skipped (`skipped_heic`); files whose name is already in the album are skipped as duplicates (`skipped_duplicates`); at most 50 files are imported per call (Google limit) and the album's photo limit can lower that further, in which case `limit_warning` explains what was left out. If every file is already in the album, the call succeeds with `status: nothing_to_import`, `import_id: null` and `poll_url: null`.\n\nErrors: `428 drive_not_connected`; `402 upgrade_required` when the storage limit or the album photo limit is reached (`upgrade_url`); `400 invalid_request` (bad body, or no supported image files in the folder); `403 forbidden` (album not found or no access); `500 internal_error`; `502 internal_error` when the import service could not be reached.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartDriveImportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import started.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/DriveImportStarted"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: the body failed validation (the error object has `reason`), or the folder has no supported image files.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`upgrade_required`: storage limit reached or the album is full. Has `upgrade_url`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`tier_not_eligible`: the plan does not include API access or Google Drive imports. `forbidden`: album not found or no access.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "428": {
            "description": "`drive_not_connected`: Google Drive is not connected to this FindMe account. Has `connect_url`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "`internal_error`: the import service could not be reached or returned a non-JSON response.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/drive_imports/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Drive import id (`import_id` from `startDriveImport`).",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getDriveImport",
        "tags": [
          "Drive imports"
        ],
        "summary": "Get Drive import progress",
        "description": "Returns the status and progress of a Google Drive import you started (from this API or the web app). Poll until `status` is `completed`, `failed`, `cancelled` or `token_expired`. `progress.percent_complete` is (processed + failed) / total_files, and is 100 once completed. `progress.skipped` counts files skipped as duplicates at start.",
        "responses": {
          "200": {
            "description": "Import status.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/DriveImport"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fm_live_*",
        "description": "Personal API key, created in the FindMe dashboard (https://findme.photo/photographer, then Connections, then API Keys). Format: `fm_live_` followed by at least 24 URL-safe characters."
      }
    },
    "parameters": {
      "EventId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Album (event) id.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "Request id for support and log correlation; equals `meta.request_id` / `error.request_id`.",
        "schema": {
          "type": "string"
        }
      },
      "X-RateLimit-Limit": {
        "description": "Per-minute request limit of the plan.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left in the current one-minute window.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Unix time (seconds) when the one-minute window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`invalid_request`: invalid id, query parameter, cursor or body. Validation failures list `details.issues`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`missing_or_invalid_token` (no or malformed `Authorization: Bearer fm_live_...` header) or `invalid_token` (unknown or revoked key).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "`upgrade_required`: the plan does not allow this (album limit or album quality). Includes `upgrade_url`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "`tier_not_eligible`: the plan does not include API access (includes `upgrade_url`), or does not include the feature this endpoint needs.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found`: no such resource, or it is not yours, or (for albums) it is deleted.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "`conflict`: the request conflicts with the current state.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "`rate_limited` (per-minute limit; `retry_after_seconds`) or `quota_exceeded` (monthly quota; `retry_after_seconds`, `resets_at`).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "`internal_error`: something failed on our side. Safe to retry reads; include `request_id` when reporting.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "enum": [
          "missing_or_invalid_token",
          "invalid_token",
          "tier_not_eligible",
          "upgrade_required",
          "rate_limited",
          "quota_exceeded",
          "invalid_request",
          "not_found",
          "forbidden",
          "conflict",
          "gone",
          "drive_not_connected",
          "internal_error"
        ],
        "description": "Machine-readable error code."
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "request_id",
              "docs_url"
            ],
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation."
              },
              "request_id": {
                "type": "string",
                "pattern": "^req_[0-9a-f]{16}$",
                "description": "Same value as the `X-Request-Id` header."
              },
              "docs_url": {
                "type": "string",
                "format": "uri",
                "description": "Where the endpoints and error codes are documented."
              },
              "details": {
                "type": [
                  "object",
                  "string"
                ],
                "description": "Extra context. On validation failures: `{\"issues\": [{\"path\", \"message\"}]}`; on rejected upload batches: `{\"rejected\": [{\"filename\", \"reason\"}]}`; on storage-limit errors: byte counts. A string when passed through from the Drive import service."
              },
              "upgrade_url": {
                "type": "string",
                "format": "uri",
                "description": "Present on `tier_not_eligible` from the request gate and on `upgrade_required`."
              },
              "retry_after_seconds": {
                "type": "integer",
                "description": "Present on `rate_limited` and request-gate `quota_exceeded`."
              },
              "resets_at": {
                "type": "string",
                "format": "date-time",
                "description": "Present on request-gate `quota_exceeded`: when the monthly quota resets."
              },
              "current_count": {
                "type": "integer",
                "description": "Present on `upgrade_required` for the album limit."
              },
              "limit": {
                "type": "integer",
                "description": "Present on `upgrade_required` for the album limit."
              },
              "requested": {
                "type": "string",
                "enum": [
                  "800px",
                  "3000px",
                  "4000px",
                  "15mb"
                ],
                "description": "Present on `upgrade_required` for album quality."
              },
              "plan_max": {
                "type": "string",
                "enum": [
                  "800px",
                  "3000px",
                  "4000px",
                  "15mb"
                ],
                "description": "Present on `upgrade_required` for album quality."
              },
              "deleted_at": {
                "type": "string",
                "format": "date-time",
                "description": "Present on `gone` from `restoreEvent`."
              },
              "window_days": {
                "type": "integer",
                "description": "Present on `gone` from `restoreEvent`."
              },
              "reason": {
                "type": "string",
                "description": "Present on `invalid_request` from `startDriveImport`."
              }
            },
            "additionalProperties": true
          }
        }
      },
      "Meta": {
        "type": "object",
        "required": [
          "request_id"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "pattern": "^req_[0-9a-f]{16}$"
          }
        }
      },
      "ListMeta": {
        "type": "object",
        "required": [
          "request_id",
          "has_more"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "pattern": "^req_[0-9a-f]{16}$"
          },
          "has_more": {
            "type": "boolean",
            "description": "True when another page exists."
          },
          "next_cursor": {
            "type": "string",
            "description": "Present only when `has_more` is true. Pass as `cursor` to get the next page."
          }
        }
      },
      "AlbumQuality": {
        "type": "string",
        "enum": [
          "800px",
          "3000px",
          "4000px",
          "15mb"
        ],
        "description": "Stored photo resolution for the album, by long edge: 800px, 3000px, 4000px, or `15mb` (up to 8000 px). Fixed once the album has photos."
      },
      "Event": {
        "type": "object",
        "description": "An album as returned by `listEvents` and `createEvent`.",
        "required": [
          "id",
          "name",
          "access_code",
          "event_date",
          "description",
          "tag",
          "album_quality",
          "enable_downloads",
          "is_collaborative",
          "photo_count",
          "quality_locked",
          "gallery_url",
          "qr_url",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "access_code": {
            "type": "string",
            "description": "Album code; part of `gallery_url`."
          },
          "event_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "tag": {
            "type": [
              "string",
              "null"
            ]
          },
          "album_quality": {
            "$ref": "#/components/schemas/AlbumQuality"
          },
          "enable_downloads": {
            "type": "boolean"
          },
          "is_collaborative": {
            "type": "boolean"
          },
          "photo_count": {
            "type": "integer",
            "description": "Number of photo and video rows in the album, any status. Always 0 on `createEvent`."
          },
          "quality_locked": {
            "type": "boolean",
            "description": "True when `photo_count` > 0; `album_quality` can no longer change."
          },
          "gallery_url": {
            "type": "string",
            "format": "uri",
            "description": "The link guests open. Uses the photographer's branded findme.photo subdomain when the plan includes one and a username is set."
          },
          "qr_url": {
            "type": "string",
            "format": "uri",
            "description": "URL of `getEventQrCode` for this album (needs the Bearer key)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EventStats": {
        "type": "object",
        "required": [
          "photo_count",
          "video_count",
          "storage_bytes"
        ],
        "properties": {
          "photo_count": {
            "type": "integer",
            "description": "Photos in the album (any status)."
          },
          "video_count": {
            "type": "integer",
            "description": "Videos in the album (any status)."
          },
          "storage_bytes": {
            "type": "integer",
            "description": "Storage the album uses, counted at uploaded size."
          },
          "guest_visits": {
            "type": "integer",
            "description": "Total guest visits. Only when the plan includes analytics."
          },
          "selfie_searches": {
            "type": "integer",
            "description": "Total selfie searches. Only when the plan includes analytics."
          }
        }
      },
      "EventDetail": {
        "type": "object",
        "description": "An album as returned by `getEvent` (with `stats`) and `updateEvent` (without).",
        "required": [
          "id",
          "name",
          "access_code",
          "event_date",
          "description",
          "tag",
          "album_quality",
          "enable_downloads",
          "is_collaborative",
          "gallery_url",
          "qr_url",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "access_code": {
            "type": "string"
          },
          "event_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "tag": {
            "type": [
              "string",
              "null"
            ]
          },
          "album_quality": {
            "$ref": "#/components/schemas/AlbumQuality"
          },
          "enable_downloads": {
            "type": "boolean"
          },
          "is_collaborative": {
            "type": "boolean"
          },
          "gallery_url": {
            "type": "string",
            "format": "uri"
          },
          "qr_url": {
            "type": "string",
            "format": "uri"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "stats": {
            "$ref": "#/components/schemas/EventStats"
          }
        }
      },
      "RestoredEvent": {
        "type": "object",
        "required": [
          "id",
          "name",
          "access_code",
          "event_date",
          "description",
          "tag",
          "gallery_url",
          "qr_url",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "access_code": {
            "type": "string"
          },
          "event_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "tag": {
            "type": [
              "string",
              "null"
            ]
          },
          "gallery_url": {
            "type": "string",
            "format": "uri",
            "description": "Guest link on https://findme.photo (not the branded subdomain)."
          },
          "qr_url": {
            "type": "string",
            "format": "uri"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateEventRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Album name (trimmed)."
          },
          "event_date": {
            "type": "string",
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "YYYY-MM-DD."
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "tag": {
            "type": "string",
            "maxLength": 80
          },
          "album_quality": {
            "$ref": "#/components/schemas/AlbumQuality",
            "default": "3000px"
          },
          "enable_downloads": {
            "type": "boolean",
            "default": true
          },
          "is_collaborative": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "UpdateEventRequest": {
        "type": "object",
        "minProperties": 1,
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "event_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "YYYY-MM-DD, or null to clear."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "tag": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "access_code": {
            "type": "string",
            "pattern": "^[A-Z0-9]{4,12}$",
            "description": "4-12 uppercase letters or digits. Changes the guest link."
          },
          "album_quality": {
            "$ref": "#/components/schemas/AlbumQuality",
            "description": "Only while the album has no photos, and only up to the plan maximum."
          },
          "enable_downloads": {
            "type": "boolean"
          },
          "is_collaborative": {
            "type": "boolean"
          }
        }
      },
      "EventAnalytics": {
        "type": "object",
        "required": [
          "event_id",
          "window",
          "guest_visits",
          "unique_visitors",
          "photos_downloaded",
          "selfie_searches",
          "daily"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "window": {
            "type": "object",
            "required": [
              "from",
              "to"
            ],
            "properties": {
              "from": {
                "type": "string",
                "format": "date-time",
                "description": "`{from}T00:00:00Z`"
              },
              "to": {
                "type": "string",
                "format": "date-time",
                "description": "`{to}T23:59:59Z`"
              }
            }
          },
          "guest_visits": {
            "type": "integer"
          },
          "unique_visitors": {
            "type": "integer",
            "description": "Sum of daily unique visitors."
          },
          "photos_downloaded": {
            "type": "integer"
          },
          "selfie_searches": {
            "type": "integer"
          },
          "daily": {
            "type": "array",
            "description": "Days in the window that have data. Order is not guaranteed.",
            "items": {
              "type": "object",
              "required": [
                "date",
                "views",
                "unique_visitors",
                "downloads",
                "selfie_searches"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "views": {
                  "type": "integer"
                },
                "unique_visitors": {
                  "type": "integer"
                },
                "downloads": {
                  "type": "integer"
                },
                "selfie_searches": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "Photo": {
        "type": "object",
        "required": [
          "id",
          "event_id",
          "filename",
          "media_type",
          "size_bytes",
          "status",
          "width",
          "height",
          "taken_at",
          "thumbnail_url",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "filename": {
            "type": [
              "string",
              "null"
            ],
            "description": "Original filename."
          },
          "media_type": {
            "type": "string",
            "enum": [
              "photo",
              "video"
            ]
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Stored size in bytes (declared size until confirmed)."
          },
          "status": {
            "type": "string",
            "description": "Processing status. Through this API: `pending` (slot reserved, not confirmed), `indexing` (photo confirmed, face indexing running), `completed`, `error`. Other upload paths may write other values."
          },
          "width": {
            "type": [
              "integer",
              "null"
            ]
          },
          "height": {
            "type": [
              "integer",
              "null"
            ]
          },
          "taken_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "thumbnail_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Public poster image for a video (the same file the guest gallery shows). Null for photos."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreatePhotoUploadsRequest": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "object",
              "required": [
                "filename",
                "content_type",
                "size_bytes"
              ],
              "properties": {
                "filename": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 255
                },
                "content_type": {
                  "type": "string",
                  "enum": [
                    "image/jpeg",
                    "image/png",
                    "image/webp",
                    "video/mp4",
                    "video/quicktime",
                    "video/webm"
                  ],
                  "description": "MIME type. Use the same value as the Content-Type of the S3 PUT."
                },
                "size_bytes": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 1073741824,
                  "description": "File size in bytes. Max 52428800 (50 MB) for photos, 1073741824 (1 GB) for videos."
                }
              }
            }
          }
        }
      },
      "PhotoUploadBatch": {
        "type": "object",
        "required": [
          "batch_id",
          "uploads",
          "album_quality"
        ],
        "properties": {
          "batch_id": {
            "type": "string",
            "pattern": "^bat_[0-9a-f]{16}$",
            "description": "Informational only."
          },
          "uploads": {
            "type": "array",
            "description": "One slot per requested file, in request order.",
            "items": {
              "type": "object",
              "required": [
                "photo_id",
                "filename",
                "media_type",
                "presigned_url",
                "s3_key",
                "expires_at"
              ],
              "properties": {
                "photo_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "Pass to `confirmPhotoUploads` after the PUT, or to `releasePhotoUploads` if unused."
                },
                "filename": {
                  "type": "string"
                },
                "media_type": {
                  "type": "string",
                  "enum": [
                    "photo",
                    "video"
                  ]
                },
                "presigned_url": {
                  "type": "string",
                  "format": "uri",
                  "description": "S3 URL to PUT the raw bytes to, with the declared Content-Type and no Authorization header."
                },
                "s3_key": {
                  "type": "string"
                },
                "expires_at": {
                  "type": "string",
                  "format": "date-time",
                  "description": "The presigned URL stops working 15 minutes after issue."
                }
              }
            }
          },
          "album_quality": {
            "$ref": "#/components/schemas/AlbumQuality"
          }
        }
      },
      "ConfirmPhotoUploadsRequest": {
        "type": "object",
        "required": [
          "photo_ids"
        ],
        "properties": {
          "photo_ids": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "dimensions": {
            "type": "array",
            "maxItems": 50,
            "description": "Optional rendered dimensions per photo, for uploaders that resize before upload.",
            "items": {
              "type": "object",
              "required": [
                "photo_id",
                "width",
                "height"
              ],
              "properties": {
                "photo_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "width": {
                  "type": "integer",
                  "minimum": 1
                },
                "height": {
                  "type": "integer",
                  "minimum": 1
                },
                "original_long_edge": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "Accepted; not currently used."
                },
                "size_bytes": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "Accepted; the server uses the size measured in S3 instead."
                },
                "original_size_bytes": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "File size before the uploader resized it. Photos only."
                }
              }
            }
          }
        }
      },
      "PhotoConfirmBatch": {
        "type": "object",
        "required": [
          "confirmed"
        ],
        "properties": {
          "confirmed": {
            "type": "array",
            "description": "One result per requested photo id.",
            "items": {
              "type": "object",
              "required": [
                "photo_id",
                "status"
              ],
              "properties": {
                "photo_id": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "description": "`indexing` (photo accepted, face indexing started), `completed` (video accepted), `error`, or with `already_processed` the row's current status."
                },
                "reason": {
                  "type": "string",
                  "description": "Present when the item was not accepted. See the operation description for values."
                }
              }
            }
          }
        }
      },
      "ReleasePhotoUploadsRequest": {
        "type": "object",
        "required": [
          "photo_ids"
        ],
        "properties": {
          "photo_ids": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "PhotoReleaseResult": {
        "type": "object",
        "required": [
          "released",
          "skipped"
        ],
        "properties": {
          "released": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Slots removed."
          },
          "skipped": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ids left untouched (not a pending slot of yours in this album)."
          }
        }
      },
      "StartDriveImportRequest": {
        "type": "object",
        "required": [
          "folder_id"
        ],
        "properties": {
          "folder_id": {
            "type": "string",
            "minLength": 1,
            "description": "Google Drive folder id."
          },
          "folder_name": {
            "type": "string",
            "maxLength": 200,
            "description": "Display name for the import."
          }
        }
      },
      "DriveImportStarted": {
        "type": "object",
        "required": [
          "import_id",
          "event_id",
          "total_files",
          "skipped_heic",
          "skipped_duplicates",
          "limit_warning",
          "status",
          "poll_url"
        ],
        "properties": {
          "import_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Null when `status` is `nothing_to_import`."
          },
          "event_id": {
            "type": "string"
          },
          "total_files": {
            "type": "integer",
            "description": "Files queued for import."
          },
          "skipped_heic": {
            "type": "integer",
            "description": "HEIC files skipped."
          },
          "limit_warning": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when the 50-file cap or the album photo limit left files out."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "nothing_to_import"
            ]
          },
          "poll_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Site-relative path of `getDriveImport` for this import, e.g. `/api/v1/drive_imports/{import_id}`. Null when `status` is `nothing_to_import`."
          },
          "skipped_duplicates": {
            "type": "integer",
            "description": "Files skipped because a file with the same name is already in the album."
          }
        }
      },
      "DriveImport": {
        "type": "object",
        "required": [
          "id",
          "event_id",
          "folder_id",
          "folder_name",
          "status",
          "progress",
          "started_at",
          "last_batch_at",
          "completed_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "folder_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "folder_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed",
              "cancelled",
              "token_expired"
            ]
          },
          "progress": {
            "type": "object",
            "required": [
              "total_files",
              "processed",
              "failed",
              "skipped",
              "new_persons",
              "percent_complete",
              "file_status_counts"
            ],
            "properties": {
              "total_files": {
                "type": "integer"
              },
              "processed": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "skipped": {
                "type": "integer",
                "description": "Duplicates skipped at start (not part of total_files)."
              },
              "new_persons": {
                "type": "integer"
              },
              "percent_complete": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "One decimal place."
              },
              "file_status_counts": {
                "type": "object",
                "description": "Count of files per file status. Keys present only for statuses that occur.",
                "propertyNames": {
                  "enum": [
                    "pending",
                    "downloading",
                    "uploading",
                    "indexing",
                    "completed",
                    "skipped",
                    "failed"
                  ]
                },
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_batch_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "user_id",
          "email",
          "display_name",
          "tier",
          "tier_display_name",
          "subscription_status",
          "current_period_end",
          "plugin"
        ],
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full name, else username."
          },
          "tier": {
            "type": "string",
            "description": "Internal plan code: `freeplus` (Free+), `basic` / `lite` (Studio), `premium` (Growth), `pro` (Pro)."
          },
          "tier_display_name": {
            "type": "string"
          },
          "subscription_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "current_period_end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "plugin": {
            "type": "object",
            "description": "Lightroom plugin update channel.",
            "required": [
              "latest_version",
              "download_url",
              "current_version",
              "update_available"
            ],
            "properties": {
              "latest_version": {
                "type": "string"
              },
              "download_url": {
                "type": "string",
                "format": "uri"
              },
              "current_version": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Version the caller reported; null for other clients."
              },
              "update_available": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "findme",
          "drive",
          "accounts_match",
          "hint"
        ],
        "properties": {
          "findme": {
            "type": "object",
            "required": [
              "user_id",
              "email",
              "display_name",
              "tier",
              "tier_display_name",
              "events_count"
            ],
            "properties": {
              "user_id": {
                "type": "string",
                "format": "uuid"
              },
              "email": {
                "type": "string"
              },
              "display_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "tier": {
                "type": "string",
                "description": "Internal plan code (see `getMe`)."
              },
              "tier_display_name": {
                "type": "string"
              },
              "events_count": {
                "type": "integer",
                "description": "Albums not deleted."
              }
            }
          },
          "drive": {
            "type": "object",
            "required": [
              "connected",
              "tier_allows_drive"
            ],
            "properties": {
              "connected": {
                "type": "boolean"
              },
              "google_email": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Only when connected."
              },
              "connected_at": {
                "type": "string",
                "format": "date-time",
                "description": "Only when connected."
              },
              "last_used_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Only when connected."
              },
              "tier_allows_drive": {
                "type": "boolean",
                "description": "Whether the plan includes Google Drive imports."
              }
            }
          },
          "accounts_match": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the FindMe email equals the Drive Google email (case-insensitive); null when Drive is not connected."
          },
          "hint": {
            "type": [
              "string",
              "null"
            ],
            "description": "A sentence to show the photographer, or null."
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "period",
          "api",
          "storage",
          "events"
        ],
        "properties": {
          "period": {
            "type": "object",
            "required": [
              "start",
              "end"
            ],
            "properties": {
              "start": {
                "type": "string",
                "format": "date",
                "description": "First day of the current UTC month."
              },
              "end": {
                "type": "string",
                "format": "date",
                "description": "Last day of the current UTC month."
              }
            }
          },
          "api": {
            "type": "object",
            "required": [
              "requests_used",
              "requests_limit",
              "rate_limit_per_minute"
            ],
            "properties": {
              "requests_used": {
                "type": "integer",
                "description": "Requests this month, summed over all of the account's API keys."
              },
              "requests_limit": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Monthly request quota per API key; null = unlimited."
              },
              "rate_limit_per_minute": {
                "type": "integer"
              }
            }
          },
          "storage": {
            "type": "object",
            "required": [
              "used_bytes",
              "limit_bytes"
            ],
            "properties": {
              "used_bytes": {
                "type": "integer"
              },
              "limit_bytes": {
                "type": "integer"
              }
            }
          },
          "events": {
            "type": "object",
            "required": [
              "active",
              "limit"
            ],
            "properties": {
              "active": {
                "type": "integer",
                "description": "Albums neither deleted nor expired."
              },
              "limit": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Plan album limit; null = unlimited."
              }
            }
          }
        }
      },
      "TierLimits": {
        "type": "object",
        "required": [
          "tier",
          "region",
          "display_name",
          "max_albums",
          "max_photos_per_album",
          "max_storage_gb",
          "retention_months",
          "max_album_quality",
          "allowed_album_qualities",
          "max_video_duration_seconds",
          "max_videos_per_album",
          "max_video_storage_gb",
          "has_analytics",
          "has_drive_sync",
          "has_color_scheme",
          "has_branding",
          "has_custom_domain",
          "has_whitelabel",
          "has_ai_assistant",
          "commission_rate_pct",
          "currency",
          "monthly_price",
          "annual_price"
        ],
        "properties": {
          "tier": {
            "type": "string",
            "description": "Internal plan code."
          },
          "region": {
            "type": "string",
            "enum": [
              "us",
              "in"
            ]
          },
          "display_name": {
            "type": "string"
          },
          "max_albums": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null = unlimited."
          },
          "max_photos_per_album": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null = unlimited."
          },
          "max_storage_gb": {
            "type": "integer"
          },
          "retention_months": {
            "type": [
              "integer",
              "null"
            ]
          },
          "max_album_quality": {
            "$ref": "#/components/schemas/AlbumQuality"
          },
          "allowed_album_qualities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AlbumQuality"
            },
            "description": "Every quality up to `max_album_quality`."
          },
          "max_video_duration_seconds": {
            "type": "integer"
          },
          "max_videos_per_album": {
            "type": "integer"
          },
          "max_video_storage_gb": {
            "type": [
              "number",
              "null"
            ],
            "description": "Account video storage cap in GB; null = the plan uses the count/duration limits instead."
          },
          "has_analytics": {
            "type": "boolean"
          },
          "has_drive_sync": {
            "type": "boolean"
          },
          "has_color_scheme": {
            "type": "boolean"
          },
          "has_branding": {
            "type": "boolean"
          },
          "has_custom_domain": {
            "type": "boolean"
          },
          "has_whitelabel": {
            "type": "boolean"
          },
          "has_ai_assistant": {
            "type": "boolean",
            "description": "Whether the plan includes API and AI-assistant access."
          },
          "commission_rate_pct": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Commission on store sales, as a fraction (0-1)."
          },
          "currency": {
            "type": "string"
          },
          "monthly_price": {
            "type": "number"
          },
          "annual_price": {
            "type": "number"
          }
        }
      }
    }
  }
}
