{
  "openapi": "3.0.0",
  "paths": {
    "/api/v1/auth/github": {
      "get": {
        "description": "Sets a short-lived state cookie and redirects the browser to GitHub. `redirect` is the dashboard path to land on afterwards; anything that is not a same-site path becomes `/`. 404 while `AUTH_GITHUB=off`.",
        "operationId": "auth.github",
        "parameters": [
          {
            "name": "redirect",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Off to GitHub.",
            "headers": {
              "Location": {
                "description": "Where the browser is sent next.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "summary": "Start signing in with GitHub",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/github/callback": {
      "get": {
        "description": "Where GitHub sends the browser back. Checks the state, exchanges the code, reads the verified email and the organisation memberships, and either sets the session cookie and redirects to the requested path, or redirects to `/login?error=<code>&provider=github`. The codes are `oauth_state`, `oauth_denied`, `oauth_no_email`, `oauth_not_member` and `oauth_failed`.",
        "operationId": "auth.githubCallback",
        "parameters": [
          {
            "name": "code",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "error",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Signed in, or turned away — the `Location` says which.",
            "headers": {
              "Location": {
                "description": "Where the browser is sent next.",
                "schema": {
                  "type": "string"
                }
              },
              "Set-Cookie": {
                "description": "The session cookie: `HttpOnly`, `SameSite=Lax`, and `Secure` when the API is reached over https. A browser stores it without help; anything else should use an API key instead of replaying this.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "summary": "Finish signing in with GitHub",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/google": {
      "get": {
        "description": "Sets a short-lived state cookie and redirects the browser to Google. `redirect` is the dashboard path to land on afterwards; anything that is not a same-site path becomes `/`. 404 while `AUTH_GOOGLE=off`.",
        "operationId": "auth.google",
        "parameters": [
          {
            "name": "redirect",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Off to Google.",
            "headers": {
              "Location": {
                "description": "Where the browser is sent next.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "summary": "Start signing in with Google",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/google/callback": {
      "get": {
        "description": "Where Google sends the browser back. Checks the state, exchanges the code, reads the verified email and the Workspace domain, and either sets the session cookie and redirects to the requested path, or redirects to `/login?error=<code>&provider=google`. The codes are `oauth_state`, `oauth_denied`, `oauth_no_email`, `oauth_not_member` and `oauth_failed`.",
        "operationId": "auth.googleCallback",
        "parameters": [
          {
            "name": "code",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "error",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Signed in, or turned away — the `Location` says which.",
            "headers": {
              "Location": {
                "description": "Where the browser is sent next.",
                "schema": {
                  "type": "string"
                }
              },
              "Set-Cookie": {
                "description": "The session cookie: `HttpOnly`, `SameSite=Lax`, and `Secure` when the API is reached over https. A browser stores it without help; anything else should use an API key instead of replaying this.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "summary": "Finish signing in with Google",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "description": "Sets the session cookie. A wrong password and an address with no account get the same 401, on purpose — the response must not say which addresses exist.",
        "operationId": "auth.login",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The person who signed in.",
            "headers": {
              "Set-Cookie": {
                "description": "The session cookie: `HttpOnly`, `SameSite=Lax`, and `Secure` when the API is reached over https. A browser stores it without help; anything else should use an API key instead of replaying this.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "summary": "Sign in",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/logout": {
      "post": {
        "description": "Destroys the session server-side and expires the cookie. A caller holding a personal access token or an API key has no session to end: it answers 204 and does nothing, because a credential that outlives this call is the honest answer and revoking one is a different route.",
        "operationId": "auth.logout",
        "parameters": [],
        "responses": {
          "204": {
            "description": "Signed out.",
            "headers": {
              "Set-Cookie": {
                "description": "The session cookie: `HttpOnly`, `SameSite=Lax`, and `Secure` when the API is reached over https. A browser stores it without help; anything else should use an API key instead of replaying this.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Sign out",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/me": {
      "get": {
        "description": "Answers for whichever credential was sent, and `kind` says which. A person — the dashboard’s session cookie (`via: \"session\"`, whose idle timer this also refreshes) or a personal access token (`via: \"token\"`) — or a `cli` API key, named with the project and environment it may reach. A `delivery` key is refused, as it is everywhere on the management API.",
        "operationId": "auth.me",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The person or the key the credential belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Who this credential is",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/options": {
      "get": {
        "description": "Whether the password sign-up form is open and which providers are configured. Public and cheap: it is what the sign-in page draws itself from.",
        "operationId": "auth.options",
        "parameters": [],
        "responses": {
          "200": {
            "description": "What the sign-in page may offer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthOptionsView"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "summary": "The ways in",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/register": {
      "post": {
        "description": "Rate limited per address. Registering does not sign you in: call `login` next. Answers 403 while `AUTH_REGISTRATION=closed`.",
        "operationId": "auth.register",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
                  },
                  "password": {
                    "type": "string",
                    "minLength": 12
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "email",
                  "password",
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The account that was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserView"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "summary": "Create an account",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/tokens": {
      "get": {
        "description": "Your own tokens, newest first, revoked ones included — a revoked token is the evidence of what a leaked credential could have touched. Session only: a token cannot manage tokens.",
        "operationId": "auth.tokens",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Every token you have minted, without secrets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PersonalTokenView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "List your personal access tokens",
        "tags": [
          "auth"
        ]
      },
      "post": {
        "description": "Session only, and the only response that ever carries the secret. The token authenticates as you, over HTTP basic as `tokenId:secret`, with every right you have in every project — for as long as it lives, which is why an expiry is worth choosing. It cannot mint further tokens.",
        "operationId": "auth.createToken",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  },
                  "expiresIn": {
                    "nullable": true,
                    "type": "string",
                    "enum": [
                      "30d",
                      "90d",
                      "365d"
                    ]
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new token, with its secret. Store it now — it is never shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MintedPersonalTokenView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Mint a personal access token",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/auth/tokens/{id}": {
      "delete": {
        "description": "Session only, and idempotent: revoking an already-revoked token succeeds. The row stays, so the token remains in the list with a `revokedAt`. Somebody else’s token is a 404, exactly as an id that never existed is.",
        "operationId": "auth.revokeToken",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The token’s id, as `GET /auth/tokens` reports it — never its `tokenId`.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The token no longer authenticates anything."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Revoke a personal access token",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/v1/locales": {
      "get": {
        "description": "Every locale `POST /projects` and `PATCH /projects/{project}` accept in `locales` and `defaultLocale`, alphabetised by tag, with an English name, the language’s own name for itself and its writing direction. Any session or key may read it. The list is compiled into the API and changes only with a release, so the answer is cached for an hour and a client holding the `ETag` spends a 304 to learn nothing has changed.",
        "operationId": "locales.list",
        "parameters": [
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "One or more ETags this client already holds, or `*`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The catalogue.",
            "headers": {
              "ETag": {
                "description": "Strong, over the exact bytes served; changes only with a release.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=3600`: the same list for everyone, for an hour.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocaleCatalogue"
                }
              }
            }
          },
          "304": {
            "description": "The copy the caller already holds is current. The `ETag` and `Cache-Control` are the same as on a 200, so a cache keeps holding it for the same hour.",
            "headers": {
              "ETag": {
                "description": "Strong, over the exact bytes served; changes only with a release.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=3600`: the same list for everyone, for an hour.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List the locales a project may use",
        "tags": [
          "locales"
        ]
      }
    },
    "/api/v1/projects": {
      "post": {
        "description": "The caller becomes its owner, and the project’s environments are created alongside it in one transaction. A session only: a key is issued for one project and cannot make another. `locales` and `defaultLocale` must come from `GET /locales`; a tag outside it is a 422 on `locales.N` or `defaultLocale`. `environments` is a list of `{ key, name? }` — the key rules are the ones `POST /environments` enforces, a repeated key is a 422 on `environments.N.key`, and a missing `name` is the key with its first letter capitalised. Omit the list and the project gets one environment, `prod` (“Prod”). `defaultEnvironment` names the one that carries `isDefault` and defaults to the first in the list; naming one that is not listed is a 422 on `defaultEnvironment`. `settings` is opaque to the API and capped at 64 KB serialised; a larger one is a 422 on `settings`. One key inside it is reserved: `settings.search` must be `{ \"enabled\": boolean }` when present, and controls whether the delivery API answers searches for this project. It defaults to **on**. Turning it off deletes the project’s search index; turning it back on rebuilds it in the background.",
        "operationId": "projects.create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "slug": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 64,
                    "pattern": "^[a-z0-9][a-z0-9-]*$"
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "defaultLocale": {
                    "type": "string",
                    "pattern": "^[a-z]{2,3}-(?:[A-Z]{2}|\\d{3})$"
                  },
                  "locales": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "pattern": "^[a-z]{2,3}-(?:[A-Z]{2}|\\d{3})$"
                    }
                  },
                  "environments": {
                    "minItems": 1,
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 2,
                          "maxLength": 64,
                          "pattern": "^[a-z0-9][a-z0-9-]*$"
                        },
                        "name": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 200
                        }
                      },
                      "required": [
                        "key"
                      ]
                    }
                  },
                  "defaultEnvironment": {
                    "type": "string"
                  },
                  "settings": {
                    "type": "object",
                    "additionalProperties": {}
                  }
                },
                "required": [
                  "slug",
                  "name",
                  "defaultLocale",
                  "locales"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The project that was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Create a project",
        "tags": [
          "projects"
        ]
      },
      "get": {
        "description": "Only the caller’s own: membership is the only way to see a project. `role` is the caller’s own membership — `null` for an API key, which holds scopes and no role — so a client can decide what to offer without a second request.",
        "operationId": "projects.list",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Every project the caller belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProjectView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "List your projects",
        "tags": [
          "projects"
        ]
      }
    },
    "/api/v1/projects/{project}": {
      "get": {
        "description": "A project the caller is not a member of answers 404, not 403: slugs are global and guessable, and a 403 would confirm which ones exist. `role` is the caller’s own membership — `null` for an API key, which holds scopes and no role — so a client can decide what to offer without a second request.",
        "operationId": "projects.get",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Read a project",
        "tags": [
          "projects"
        ]
      },
      "patch": {
        "description": "Owners only, and never the slug: the slug is the project’s URL, and renaming it would break every link, key and cached response pointing at the old one. A locale being *added* must come from `GET /locales` (422 on `locales.N`); one the project already holds is kept as it is. A locale being *removed* must hold no live document in any environment, or the update is refused with 409 `in_use` naming them. The default locale cannot be removed without moving it first. **Moving the default locale is refused with 409 `in_use` while any binding would move with it**, and `usedBy` names the rows a binding is read from — a component (`_schemas/<name>`), a folder’s `_index`, or a key’s default-locale row — because those are the rows that would have to say the same thing at the new locale. Every binding in this API is read from the row at the default locale, so moving it silently unbound every document in the project at once, and an unbound body is stored and served exactly as it was sent. A project that holds a component therefore cannot change its default locale, since a schema exists only in the default locale and cannot be prepared at the new one: that is a migration rather than a form field. A project with no bindings moves freely. `settings` is opaque to the API and capped at 64 KB serialised; a larger one is a 422 on `settings`. One key inside it is reserved: `settings.search` must be `{ \"enabled\": boolean }` when present, and controls whether the delivery API answers searches for this project. It defaults to **on**. Turning it off deletes the project’s search index; turning it back on rebuilds it in the background. A `settings` write replaces the whole bag, so a `search` that is to be kept must be sent with it.",
        "operationId": "projects.update",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "defaultLocale": {
                    "type": "string",
                    "pattern": "^[a-z]{2,3}-(?:[A-Z]{2}|\\d{3})$"
                  },
                  "locales": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "pattern": "^[a-z]{2,3}-(?:[A-Z]{2}|\\d{3})$"
                    }
                  },
                  "settings": {
                    "type": "object",
                    "additionalProperties": {}
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "A locale being removed still has documents, or a default locale being moved still has bindings read from it (`in_use` — fix those first); nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocaleInUse"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Update a project",
        "tags": [
          "projects"
        ]
      }
    },
    "/api/v1/projects/{project}/assets": {
      "post": {
        "description": "One multipart file part named `file`, an optional text part `alt`, an optional text part `visibility` (`public` by default), and an optional text part `folder` naming the folder to upload into (absent uploads to the project root). The type is sniffed from the bytes and must be on the allow-list; a raster image is stored after one pass that applies EXIF orientation and strips every metadata block. The same bytes already in the project answer 200 with the existing asset — nothing about it changes, including its folder — unless the upload named the other `visibility`, which is a 409 `visibility_mismatch` carrying that asset. Over the cap is 413; over the project’s quota is 409 `quota_exceeded`; too many uploads is 429 with `Retry-After`.",
        "operationId": "assets.upload",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Exactly one file part. Its type is sniffed; the declared one is ignored."
                  },
                  "alt": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Alternative text for an image. Optional."
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private"
                    ],
                    "default": "public",
                    "description": "Who may read the bytes. A private file is served only against a signed, expiring URL. Optional."
                  },
                  "folder": {
                    "type": "string",
                    "description": "The id of the folder to upload into. Absent uploads to the project root."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "These bytes were already in the project; this is that asset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetView"
                }
              }
            }
          },
          "201": {
            "description": "The asset that was created — or, on 200, the one that already held these bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Either the project has no room left for this file (`quota_exceeded`), or the same bytes are already in it with the other visibility and the upload named the one it wanted (`visibility_mismatch` — `asset` is that file). Nothing was stored either way; the body says which. One key rather than two, because a route documents one body per status.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/QuotaExceeded"
                    },
                    {
                      "$ref": "#/components/schemas/VisibilityMismatch"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than the server accepts. The limit is `JSON_BODY_LIMIT`, 10MB by default, or `ASSET_MAX_BYTES` on an upload; split the request — or shrink the file — and send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The asset store cannot be reached, or the image transform queue is full. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorageUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Upload a file",
        "tags": [
          "assets"
        ]
      },
      "get": {
        "description": "Newest first, paged with an opaque `cursor`; `q` searches names, `mime` filters by type or family and may repeat, and `folder` narrows to one folder’s id or `root`. Absent, `folder` returns files at every level. Every page carries the project’s usage against its quota.",
        "operationId": "assets.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "q",
            "required": false,
            "in": "query",
            "description": "A case-insensitive substring of the name.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "mime",
            "required": false,
            "in": "query",
            "description": "A full type (`image/png`) or a family (`image/*`); repeat it to OR several.",
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "1 to 100, 50 by default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "required": false,
            "in": "query",
            "description": "The `nextCursor` of the previous page. Opaque.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visibility",
            "required": false,
            "in": "query",
            "description": "`public` or `private`. Absent returns both.",
            "schema": {
              "type": "string",
              "enum": [
                "public",
                "private"
              ]
            }
          },
          {
            "name": "folder",
            "required": false,
            "in": "query",
            "description": "A folder id, or `root`. Absent returns every file in the project.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of assets, and the project’s usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetPage"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List a project’s files",
        "tags": [
          "assets"
        ]
      }
    },
    "/api/v1/projects/{project}/assets/{id}": {
      "get": {
        "operationId": "assets.get",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The asset id: 26 lowercase base32 characters.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z2-7]{26}$",
              "example": "b7k2m9x4q1w8e5r3t6y2u1i8o4"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The asset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Read one file’s metadata",
        "tags": [
          "assets"
        ]
      },
      "patch": {
        "description": "The four mutable fields; none is versioned, and a change reaches every page that embeds the asset on its next read without a publish. `alt: null` clears it; `folderId: null` moves the file back to the project root.",
        "operationId": "assets.patch",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The asset id: 26 lowercase base32 characters.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z2-7]{26}$",
              "example": "b7k2m9x4q1w8e5r3t6y2u1i8o4"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "alt": {
                    "nullable": true,
                    "type": "string",
                    "maxLength": 500
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private"
                    ]
                  },
                  "folderId": {
                    "nullable": true,
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The asset as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Rename a file, change its alt text, its visibility, or its folder",
        "tags": [
          "assets"
        ]
      },
      "delete": {
        "description": "Hard: the bytes and the row. Refused with 409 `in_use` while any live document in any environment names the asset — the referrers, with their environment, are in the body, and the references route lists them in full.",
        "operationId": "assets.delete",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The asset id: 26 lowercase base32 characters.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z2-7]{26}$",
              "example": "b7k2m9x4q1w8e5r3t6y2u1i8o4"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The asset and its bytes are gone."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Live content still names this file (`in_use` — remove it from those documents first); nothing was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetInUse"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The asset store cannot be reached, or the image transform queue is full. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorageUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Delete a file",
        "tags": [
          "assets"
        ]
      }
    },
    "/api/v1/projects/{project}/assets/{id}/references": {
      "get": {
        "description": "Every live document in every environment whose current version points at the asset, once per place, capped at 200 and sorted by environment, key, locale and path.",
        "operationId": "assets.references",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The asset id: 26 lowercase base32 characters.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z2-7]{26}$",
              "example": "b7k2m9x4q1w8e5r3t6y2u1i8o4"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Where the file is used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetReferencesView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List the documents that use a file",
        "tags": [
          "assets"
        ]
      }
    },
    "/api/v1/projects/{project}/environments": {
      "get": {
        "operationId": "environments.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every environment in the project, the default included.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EnvironmentView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List a project’s environments",
        "tags": [
          "environments"
        ]
      },
      "post": {
        "description": "Owners only. An environment is part of the project’s shape rather than its content, and every one of them is another copy for editors to keep in step. Never the default — that is decided when the project is created. `cloneFrom` names another environment of this project to start from: every document that is **published** there is copied in as version 1, already published here, and `includeDrafts` copies the draft head too — as a second version where it differs, and as the only version for a document that was never published. `includeDrafts` without `cloneFrom` is refused. A clone copies content and nothing else: `previewUrl` is per environment and is never copied, a translation’s review is not carried over, and the schema binding, references, assets and preview of every row are derived again from the body that landed. The environment appears only once the copy is complete, and a clone that fails leaves nothing behind. It shares the promotion rate limit, and it is recorded in this environment’s promotion trail as a `clone`.",
        "operationId": "environments.create",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "key": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 64,
                    "pattern": "^[a-z0-9][a-z0-9-]*$"
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "cloneFrom": {
                    "type": "string"
                  },
                  "includeDrafts": {
                    "default": false,
                    "type": "boolean"
                  }
                },
                "required": [
                  "key",
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The environment that was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnvironmentView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Create an environment",
        "tags": [
          "environments"
        ]
      }
    },
    "/api/v1/projects/{project}/environments/{env}": {
      "get": {
        "description": "One environment, with how many live documents and unrevoked keys it holds — what a client names before asking whether to delete it. `documents` counts what the document listing counts: live rows in every locale, folder `_index` metadata excluded. Any member of the project may read it, as with the list; a key bound to one environment may only read that one.",
        "operationId": "environments.get",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The environment and its counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnvironmentDetailView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Read an environment",
        "tags": [
          "environments"
        ]
      },
      "patch": {
        "description": "Owners only. The name, the website preview URL, or both; a field left out is left as it is, and `previewUrl: null` clears it. The key and `isDefault` cannot be changed — the key is what published content is addressed by. `previewUrl` is held to the same address rule as a webhook URL, with the same sentences: `https://` unless the installation sets `WEBHOOK_ALLOW_HTTP`, no username or password, at most 2048 characters, and a host that resolves to a private address is refused unless the installation sets `WEBHOOK_ALLOW_PRIVATE_NETWORKS`. Quillet never requests the URL itself — the editor’s browser does — so the check is defence in depth rather than egress control.",
        "operationId": "environments.update",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "previewUrl": {
                    "nullable": true,
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The environment as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnvironmentView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Update an environment",
        "tags": [
          "environments"
        ]
      },
      "delete": {
        "description": "Owners only. Removes the environment and, in the same transaction, every document and version in it, and revokes every key bound to it — a revoked key stays in the key list with a `revokedAt` and, its environment being gone, `env: null` and `envDeleted: true` — which is what tells it apart from a key that was never bound to an environment. After the commit every cached delivery answer for the environment is retired. The project’s default environment cannot be deleted: 422 on `env`. The counts say what went: `documents` is every row removed — deleted-but-recoverable ones and folder `_index` metadata included — so it may exceed what `GET …/environments/{env}` counted.",
        "operationId": "environments.delete",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnvironmentDeletion"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Delete an environment",
        "tags": [
          "environments"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/compare": {
      "post": {
        "description": "A read-only report of how this environment stands against another, folder by folder and locale by locale. The environment in the path is the **target** — the one a later promotion would write into — and `from` names the source. Needs only `content:read`: running it twice answers the same report.",
        "operationId": "promotion.compare",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string"
                  },
                  "scope": {
                    "minItems": 1,
                    "maxItems": 50,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "prefix": {
                          "type": "string"
                        },
                        "locales": {
                          "minItems": 1,
                          "maxItems": 64,
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "prefix"
                      ]
                    }
                  },
                  "limit": {
                    "default": 200,
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 500
                  },
                  "cursor": {
                    "default": null,
                    "nullable": true,
                    "type": "string"
                  }
                },
                "required": [
                  "from",
                  "scope"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One page of the report, in (key, locale) order, with a summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompareResponse"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Compare two environments",
        "tags": [
          "promotion"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/documents": {
      "get": {
        "description": "Drafts included, bodies excluded: a page of a hundred documents would otherwise carry a hundred bodies to draw a tree. Page with `cursor`, which is opaque and encodes both the key and the locale it stopped at. Folder `_index` documents are not content and appear only under `includeIndex=true`. `preview` is the value of the component’s `x-quillet.preview` field in this document’s current body, trimmed and cut to 120 characters, or `null`. It is derived on every save from the schema version the save was measured against: changing a component’s preview field relabels an instance on its next save, not on the change. The dashboard labels an instance `preview ?? title ?? key`. `_singletons/<name>` holds the one instance of a singleton component — a component whose body carries `x-quillet.singleton: true`. Its `effectiveSchema` is that component whatever the row or the folder says — and whether or not the component still carries the flag, because the key is the binding and the flag decides how many instances there may be rather than what one is bound to — and `schemaSource` is `singleton`; a `schema` on it is refused, and creating the component’s instance anywhere else is too. Unlike `_schemas/`, the folder is served and listed by delivery.",
        "operationId": "documents.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "prefix",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "md",
                "text"
              ]
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeIndex",
            "required": false,
            "in": "query",
            "description": "When `true`, folder `_index` documents are listed as rows of their own. A backup needs this to find a folder that holds nothing else; a tree does not.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of documents.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentPage"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List documents",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/documents/{key}": {
      "get": {
        "description": "The draft head, body included — what the dashboard edits. `preview` is the value of the component’s `x-quillet.preview` field in this document’s current body, trimmed and cut to 120 characters, or `null`. It is derived on every save from the schema version the save was measured against: changing a component’s preview field relabels an instance on its next save, not on the change. The dashboard labels an instance `preview ?? title ?? key`. `_singletons/<name>` holds the one instance of a singleton component — a component whose body carries `x-quillet.singleton: true`. Its `effectiveSchema` is that component whatever the row or the folder says — and whether or not the component still carries the flag, because the key is the binding and the flag decides how many instances there may be rather than what one is bound to — and `schemaSource` is `singleton`; a `schema` on it is refused, and creating the component’s instance anywhere else is too. Unlike `_schemas/`, the folder is served and listed by delivery.",
        "operationId": "documents.get",
        "parameters": [
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The document, with its content.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Read a document",
        "tags": [
          "documents"
        ]
      },
      "put": {
        "description": "Appends a version and moves the draft head. It never publishes: what delivery serves only changes when `publish` says so. Sending content equal to the head is a no-op rather than an empty version, and a `message` sent with it is discarded along with it: a commit message describes a version, and a no-op save has no version for it to sit on. Metadata in the same request is still applied. Send `If-Match: W/\"<contentHash>\"` to make the save conditional on the version you were editing. A translation is measured against the body a reader will get: this locale’s fields merged over the default locale’s current draft, which is how delivery resolves it. So an empty translation of a page with a required field is accepted, and one that sets that field to the wrong type is not. A schema binds a *key* rather than one locale: a translation that sends no `schema:` is bound by the default locale’s row — sanitised, validated and reference-checked against it, on save and on publish alike — and a folder’s `defaultSchema` fills the gap only where no row for the key names one. `schema: null` clears this row’s own binding, and is refused with 422 on `schema` where nothing else is left to bind the document: a write always carries a body, and an unbound body is stored and served exactly as it was sent, so unbinding and supplying content in one call is one request from sanitised to raw on the delivery API. It stays legal wherever nothing is unbound by it — a document the folder’s default still binds, and one that never had a binding. Delete the document to write a body without one. Saving a folder’s `_index` in the default locale *without* a `defaultSchema` is the same removal made from the other side, and is refused the same way the deletion of that `_index` is: 409 `in_use` with the message `This folder binds documents to a component.` and `usedBy` naming the live documents its `defaultSchema` binds. It is decided on the settings as parsed, so a `defaultSchema` of the wrong type or of a name no key could hold is a removal like any other — those fields are dropped rather than refused. Naming a *different* component is not a removal: the documents move to it and none of them is left bound to nothing. An `_index` in any other locale binds nothing and is never refused for this. A field marked `x-quillet.translatable: false` is shared across locales: it exists in the default locale only, and a body for another locale that holds one is refused at that path rather than having it dropped — so a push never silently loses what a file says. Every merge takes the default locale’s value there, and it is not listed in `_meta.fallbacks`: it is not untranslated, it is shared. Inside a list or a block list the flag is inert, because an array is merged whole. `_component` is reserved. When the API compiles a schema it adds `properties._component: { \"const\": \"<the schema’s own name>\" }` to the compiled copy — first, so the generated interface leads with it; never in `required`, so a body may omit it; on every schema it loads, not only the one being compiled. Stored bodies are untouched and `additionalProperties: false` is unaffected, because the property is in `properties`. A body that carries it must name the schema it is being checked against. That is what makes a block list’s `oneOf` decidable where two components have the same shape, and what gives the generated types a discriminant. An embedded component’s body may carry it; a *group* — an inline object with `properties` of its own — may not, because a group is not a component and has no name, which is how a reader tells the two apart. A `_component` on a group is refused whether or not the group closes itself. In a block list that names two or more components it is **required**: which component a block is decides whose rules that block is under, so a block that leaves it out is refused with \"Say which component this block is.\" at the block’s own path. A list that names one component keeps it optional — every block in it is that component — so a list that *gains* a second component needs `_component` added to the blocks already stored under it: until then those bodies are refused by this route and by `publish`, one message per block, and nothing about them is changed or lost. A rich text field — `x-quillet.widget: \"richtext\"` — is sanitised before the body is validated or hashed: the tags `p br strong em u s a h1–h6 ul ol li blockquote pre code hr`, `href` and `title` on a link, and the schemes `http https mailto tel`. Everything else is dropped and its text kept, `maxLength` is measured afterwards, and sanitising is idempotent — so the answer carries the stored body and a pull after a push converges. A link field holds `{ label?, href }` or `{ label?, ref }`, exactly one of the two; a `ref` names a live json document, joins `refs[]` and is served exactly as written. A `{ \"$asset\": \"<id>\" }` pointer is extracted into the document’s `assets[]` exactly as a `$ref` is: distinct and sorted, never settable, empty for a schema document. That list is what an asset’s `…/references` reads and what refuses its deletion with 409 `in_use`, and both are asked of the **project** rather than of one environment — an asset is project-scoped and the documents pointing at it are not, so a pointer in `staging` keeps the file alive for `main` and each referrer is named with its `env`. An id outside the grammar is 422 `content.<path>`: \"Not an asset id.\", wherever it sits, because it can never resolve; a pointer inside a schema document is \"A schema cannot hold an asset.\" Where the schema marks the slot — `x-quillet.widget` `image`, `video` or `file` — the target is checked, one sentence per way it can be wrong: `No asset \"…\".`, `\"logo.pdf\" is not an image.` or `is not a video.` for the family a widget insists on, `\"logo.pdf\" is not one of: image/png, image/svg+xml.` for an `accept`, and `\"hero.png\" is 30 MB; this field allows at most 10 MB.` for a `maxBytes`. A pointer in a slot the schema never marked, or in a document with no schema, is extracted and guarded but not checked — the permissive rule an unmarked `$ref` gets. Which is why a schema document — a body under `_schemas/` — is refused when it uses a shape those rules cannot be applied through: `anyOf`, `allOf`, `not`, `if`/`then`/`else`, `contains`, `dependentSchemas`, `dependentRequired`, `propertyNames`, `patternProperties`, `unevaluatedProperties`, `unevaluatedItems`, an anchor keyword, an `additionalProperties` other than `false`, or a `$ref` to a pointer (`#/…`). Each of them can put a field where the walk that finds rich text, links, references and groups cannot report it, so the shape is refused rather than stored and half-enforced; one schema points at another by name, and §3.7 already calls all of these uneditable as a form. `preview` is the value of the component’s `x-quillet.preview` field in this document’s current body, trimmed and cut to 120 characters, or `null`. It is derived on every save from the schema version the save was measured against: changing a component’s preview field relabels an instance on its next save, not on the change. The dashboard labels an instance `preview ?? title ?? key`. `_singletons/<name>` holds the one instance of a singleton component — a component whose body carries `x-quillet.singleton: true`. Its `effectiveSchema` is that component whatever the row or the folder says — and whether or not the component still carries the flag, because the key is the binding and the flag decides how many instances there may be rather than what one is bound to — and `schemaSource` is `singleton`; a `schema` on it is refused, and creating the component’s instance anywhere else is too — including through a folder, so an `_index` in an ordinary folder may not name a singleton as its `defaultSchema`. The flag itself is refused in the other direction for the same reason: a component cannot become a singleton while documents are instances of it or an `_index` defaults to it, so neither value is ever left contradicting the other. Unlike `_schemas/`, the folder is served and listed by delivery.",
        "operationId": "documents.put",
        "parameters": [
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "description": "The `contentHash` of the version the editor was holding, quoted as `W/\"<contentHash>\"`. When present and no longer the head, the save is refused with 409 `conflict` and nothing is written — a document that does not exist is never the head, so any header at all refuses one, and a deleted document does not exist: its last hash will not revive it. A header this API could not have issued (`*`, a list, anything but a quoted 64-character lowercase-hex hash) is a 422 `validation_failed` on `_form` instead, because it is a request to fix rather than a race to retry. Absent, the save overwrites whatever is there, which is what a CLI push and every client written before this header existed do.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "json",
                      "md",
                      "text"
                    ]
                  },
                  "content": {},
                  "title": {
                    "nullable": true,
                    "type": "string",
                    "maxLength": 200
                  },
                  "order": {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991
                  },
                  "tags": {
                    "maxItems": 32,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "maxLength": 64,
                      "pattern": "^[a-z0-9][a-z0-9-]*$"
                    }
                  },
                  "searchable": {
                    "nullable": true,
                    "type": "boolean"
                  },
                  "schema": {
                    "nullable": true,
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]*$"
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 500
                  }
                },
                "required": [
                  "type",
                  "content"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The document as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Either a lost race (`conflict` — retry as you are) or a document that live content still references (`in_use` — remove those references first); the body says which.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Conflict"
                    },
                    {
                      "$ref": "#/components/schemas/InUse"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than the server accepts. The limit is `JSON_BODY_LIMIT`, 10MB by default, or `ASSET_MAX_BYTES` on an upload; split the request — or shrink the file — and send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Create or update a document",
        "tags": [
          "documents"
        ]
      },
      "delete": {
        "description": "Soft: the document stops being served, its history stays, and a later write to the same key brings it back where it left off. Refused with 409 `in_use` while live content still references the key — the references route says what. Deleting a component (`_schemas/<name>`) is refused with 409 `in_use` and the message `This component is in use.` while anything is bound to it — a live json document by its own `schema`, by the default locale’s row for its key, by a folder’s `defaultSchema` or by being its singleton, or another component that embeds or references it; `usedBy` names the first 50, sorted by key then locale. Deleting the default locale’s row of a key does not unbind the key: its component is read from that row whether or not the row is deleted, so every translation stays bound to it, including one written after the delete. The remaining translations are given that component explicitly as well, in the same transaction; a key with more than 100 of them is refused with 409 `in_use` instead. Deleting a folder’s `_index` in the default locale is refused the same way, with the message `This folder binds documents to a component.`, while its `defaultSchema` is what binds live documents under it — `usedBy` names them. No operation may leave a document whose stored body was written under a binding without that binding: an unbound body is stored and served exactly as it was sent. Delete those documents, or bind them directly, and the folder goes. An `_index` in any other locale binds nothing and is never refused for this. The folder’s `syncSource` is decided before the document is looked for, so an actor the folder refuses hears the same 403 whether or not the key exists: the 404 is not an existence oracle for a folder the caller may not read.",
        "operationId": "documents.delete",
        "parameters": [
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The document is no longer served."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Either a lost race (`conflict` — retry as you are) or a document that live content still references (`in_use` — remove those references first); the body says which.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Conflict"
                    },
                    {
                      "$ref": "#/components/schemas/InUse"
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Delete a document",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/preview-tokens": {
      "post": {
        "description": "A pass to one document, in one locale, of this environment, valid for fifteen minutes, and the address on the customer’s own site to open it at — the environment’s `previewUrl` with `token`, `key` and `locale` appended, keeping any query it already carried. **Editors and owners only, and people only:** an API key is refused however it is scoped, because a token that opens unpublished work belongs to somebody accountable for it; a personal access token is that person and is accepted. Thirty a minute per credential. The token is not a credential for this API — the delivery read it opens still needs the site’s delivery key — and it is not stored, so it cannot be revoked one at a time. Set the environment’s preview URL first, or this answers 422 on `_form`.",
        "operationId": "environments.previewToken",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "The document to open, exactly as the editor addresses it."
                  },
                  "locale": {
                    "description": "The locale to open. Defaults to the project’s default.",
                    "type": "string"
                  }
                },
                "required": [
                  "key"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The token, when it expires, and the address to open.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreviewTokenView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Mint a website preview token",
        "tags": [
          "environments"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/promote": {
      "post": {
        "description": "Copies the named documents from `from` into this environment in one transaction: the source’s current head becomes a new draft here, a key the source no longer has is deleted here, and `publish: true` makes them live in the same transaction — which needs `content:publish` as well, and measures every body against this environment’s **published** schemas. Every entry is guarded by the target head the caller last saw. Nothing is applied on any refusal.",
        "operationId": "promotion.promote",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string"
                  },
                  "scope": {
                    "minItems": 1,
                    "maxItems": 50,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "prefix": {
                          "type": "string"
                        },
                        "locales": {
                          "minItems": 1,
                          "maxItems": 64,
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "prefix"
                      ]
                    }
                  },
                  "entries": {
                    "minItems": 1,
                    "maxItems": 500,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "locale": {
                          "type": "string"
                        },
                        "baseHash": {
                          "nullable": true,
                          "type": "string",
                          "pattern": "^[0-9a-f]{64}$"
                        },
                        "sourceHash": {
                          "nullable": true,
                          "type": "string",
                          "pattern": "^[0-9a-f]{64}$"
                        }
                      },
                      "required": [
                        "key"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "publish": {
                    "default": false,
                    "type": "boolean"
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 500
                  }
                },
                "required": [
                  "from",
                  "entries"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What each entry did, in the order it was asked for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PromoteResponse"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Refused: either the actor may not use this route at all, or it named documents in folders it may not change — the second names every one.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ApiError"
                    },
                    {
                      "$ref": "#/components/schemas/ForbiddenBatch"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "A lost race (`conflict` — retry as you are), a stale base (`base_mismatch` — pull first), or a deletion of something still referenced (`in_use` — nothing was applied); the body says which. One key rather than three, because a route documents one body per status and these three share 409.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Conflict"
                    },
                    {
                      "$ref": "#/components/schemas/BaseMismatch"
                    },
                    {
                      "$ref": "#/components/schemas/InUse"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than the server accepts. The limit is `JSON_BODY_LIMIT`, 10MB by default, or `ASSET_MAX_BYTES` on an upload; split the request — or shrink the file — and send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Promote documents from another environment",
        "tags": [
          "promotion"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/promotions": {
      "get": {
        "description": "Every promotion and clone that committed **into** this environment, newest first. Summaries only: the entries are on the one-record route, so ten records cost ten small rows.",
        "operationId": "promotion.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "How many records to return: 1 to 50. Defaults to 10.",
            "schema": {
              "default": 10,
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "cursor",
            "required": false,
            "in": "query",
            "description": "The `nextCursor` of the previous page — the last record’s id. Pass it back unchanged; the page after it is the records older than it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the trail, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PromotionPage"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List the promotions into an environment",
        "tags": [
          "promotion"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/promotions/{id}": {
      "get": {
        "description": "One record with what each of its entries did, in the order it was asked for.",
        "operationId": "promotion.get",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The promotion’s id, as `GET …/promotions` reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The record, with its entries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PromotionView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Read one promotion",
        "tags": [
          "promotion"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/publish": {
      "post": {
        "description": "Moves the pointer delivery reads, for up to 200 documents in one transaction, at one instant — so \"what was live at T\" has a single answer for the whole set. Needs `content:publish`, which a key can hold without ever being allowed to change a byte of content. 200 rather than 201: nothing was created. A document named twice is one target: the response carries one entry per distinct `key`+`locale`, so it can be shorter than the request. Bad input is reported two ways on purpose — a bad key is a 422 keyed by `key` when the grammar rejects it and by the key itself (`landing/typo`) when it names nothing, while a body a schema refuses is keyed by the entry that sent it (`documents.3.content.title`), because that one is about a position in the batch. A document whose current version has gone missing is refused with a 500 rather than published as an empty page: that is corruption, and the key is named in the log. A translation is measured against its resolved body — this locale’s fields over the default locale’s *published* ones — so publishing a translation over an unpublished default locale is refused rather than serving a hole. Publishing the pair in one call is one decision and is accepted. The check is one-directional: only the documents of the batch are measured, so publishing a change to the default locale alone is not re-checked against the translations already live that inherit from it. A rich text field is sanitised here as well as on save, against the schema that will be serving it: a field that became `richtext` after a body was stored has never met the sanitiser, so the version being published is rewritten and re-hashed before it goes live — and its `preview` re-derived from the rewritten body, so no listing labels a row out of markup the stored version no longer holds. Sanitising is idempotent, so a body that is already clean is left exactly as it is. A link’s `ref` is checked here too, for the same liveness a typed reference is: publishing a document whose link points at a draft is refused unless the target is in the same call. An entry may carry `version`, the version whose summary the caller read: it is a precondition rather than a choice of what to publish, and a draft that has moved past it is refused with 409 `conflict` naming both versions, inside the transaction and before any write. A caller that names no version publishes whatever is current, exactly as before.",
        "operationId": "publishing.publish",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "documents": {
                    "minItems": 1,
                    "maxItems": 200,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "locale": {
                          "type": "string"
                        },
                        "version": {
                          "type": "integer",
                          "minimum": 0,
                          "exclusiveMinimum": true,
                          "maximum": 9007199254740991
                        }
                      },
                      "required": [
                        "key"
                      ]
                    }
                  }
                },
                "required": [
                  "documents"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per document asked for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResponse"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another writer changed the same document first. Read again and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conflict"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Publish documents",
        "tags": [
          "publishing"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/publish-impact/{key}": {
      "get": {
        "description": "What `publish` would do to this one document, without doing it: every check the publish runs is run inside a transaction that is then rolled back, so `problems` is exactly the 422 a publish would answer with, keyed without the `documents.N.` entry prefix, and empty when the publish would go through. `affects` lists the live documents whose *published* body inlines this key, two hops deep — the distance the delivery cache forgets on a publish. Publishing a translation reaches every reader of that locale: an inlined reference is resolved in the reader’s locale, so a referrer live only in the default locale is reached too, and each affected document is listed once under the reader’s locale. Publishing the default locale reaches every locale, and `fallbackLocales` names the ones with no live translation of their own, which are served this version whole. Under `publish-impact/` rather than `documents/{key}/impact`, for the reason `versions/` is: a suffix after a wildcard key is ambiguous with a legal key. Refusals that are not about the content — a key that names nothing, a folder a sync source owns — are answered as the publish answers them. Needs `content:publish`, not `content:read`: reaching the answer runs the publish inside a transaction and can lock a document whose rich text body the publish would rewrite, so asking what a publish would do carries the publish’s own cost and its own authority.",
        "operationId": "publishing.impact",
        "parameters": [
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What the publish would change, and why it would be refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishImpactView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Preview what publishing a document would change",
        "tags": [
          "publishing"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/references/{key}": {
      "get": {
        "description": "Every live document in the environment whose current version points at this key, once per place it does. This is what the dashboard’s “Used by” panel reads, and what makes a refused delete actionable. A key that holds nothing still answers: the question is about the referrers. Under `references/` rather than `documents/{key}/references`, for the reason `versions/` is: a suffix after a wildcard key is ambiguous with the perfectly legal key `docs/references`. A link’s `ref` is reported here too, at the link’s own path. `preview` is the value of the component’s `x-quillet.preview` field in the *referrer’s* current body, trimmed and cut to 120 characters, or `null`. It is derived on every save from the schema version the save was measured against: changing a component’s preview field relabels an instance on its next save, not on the change. The dashboard labels an instance `preview ?? title ?? key`.",
        "operationId": "documents.references",
        "parameters": [
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every place the document is referenced from, capped at 200.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReferencesView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List what references a document",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/schemas": {
      "get": {
        "description": "One entry per schema document under `_schemas/`, in name order. `version` is the draft head and `publishedVersion` is what delivery serves — the gap between them is the \"unpublished changes\" an author acts on — and each is `null` when there is no such version. `documents` counts the json documents the schema is in force for, folder-inherited bindings included; a binding that lives in an `_index` body cannot be counted by a query, so it is found by the same scan the validate action uses, over the first 2000 json keys of the environment in key order. A larger environment under-reports the counts rather than costing a full walk for a listing. `_component` is reserved. When the API compiles a schema it adds `properties._component: { \"const\": \"<the schema’s own name>\" }` to the compiled copy — first, so the generated interface leads with it; never in `required`, so a body may omit it; on every schema it loads, not only the one being compiled. Stored bodies are untouched and `additionalProperties: false` is unaffected, because the property is in `properties`. A body that carries it must name the schema it is being checked against. That is what makes a block list’s `oneOf` decidable where two components have the same shape, and what gives the generated types a discriminant. An embedded component’s body may carry it; a *group* — an inline object with `properties` of its own — may not, because a group is not a component and has no name, which is how a reader tells the two apart. `title` is the draft body’s root `title`, the document’s `title` where the body has none, and `null` where neither does; `fields` counts the root properties; `embeds` names the components this one reaches by bare `$ref`, direct only and sorted, which is what makes a dependency visible before a delete is refused; `singleton` is `x-quillet.singleton`, and `page` is `x-quillet.page` — a component whose instances are pages, carrying SEO fields and a list of blocks.",
        "operationId": "schemas.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every schema of the environment, with its versions and usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SchemasView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List the schemas of an environment",
        "tags": [
          "schemas"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/schemas/{name}/validate": {
      "post": {
        "description": "Reports, never writes. Changing a schema does not retro-check the documents under it — only a save refuses a body — so this is how an author sees what a change would cost before anybody tries to publish it. At most 500 documents, in key order. A folder-inherited binding cannot be expressed as a query, so the documents are found by scanning the first 2000 json keys of the environment and resolving each one’s folder settings: an environment larger than that may not be reported in full. A translation is reported against its resolved body, exactly as a save and a publish measure it, so an untranslated field is not a failure. Each entry carries the document’s `preview` — its stored label, derived on its last save — so this report doubles as the list of what exists.",
        "operationId": "schemas.validate",
        "parameters": [
          {
            "name": "name",
            "required": true,
            "in": "path",
            "description": "A schema’s name — the segment after `_schemas/`, never the whole key.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "hero"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "mode",
            "required": false,
            "in": "query",
            "description": "Which version of the schema *and* of each document to read. `current` reports on the drafts an editor is working on; `published` reports on what delivery is serving. Defaults to `current`.",
            "schema": {
              "type": "string",
              "enum": [
                "current",
                "published"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One entry per document the schema is in force for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateResponse"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Check every document against a schema",
        "tags": [
          "schemas"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/search/reindex": {
      "post": {
        "description": "Walks every published document in the environment, writes the entries it should have, and deletes every entry the pass did not write. **Idempotent**: running it twice writes the same rows and answers the same counts. It is the repair for the one thing the index cannot fix by itself — a crash between a publish and the index write leaves a page out, or leaves a withdrawn page in, and there is no TTL behind it. Owners only, and **five an hour per environment** rather than per credential: the cost belongs to the environment, so two owners cannot buy ten passes between them. With `settings.search.enabled` false this answers 422 — rebuilding an index that is about to be dropped is not a thing to let somebody wait for.",
        "operationId": "search.reindex",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "How many documents were considered — distinct keys, not translations — and how many entries the index now holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchReindexView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Rebuild the search index for an environment",
        "tags": [
          "search"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/sync/bulk": {
      "post": {
        "description": "Up to 500 documents and deletions together, in one transaction. A push produces drafts and never publishes. An optional `scope` makes the server refuse any entry outside it, so a mis-configured checkout cannot push past its own config. Any entry may carry `baseHash` — the head the lock file recorded, or `null` for \"must not exist yet\"; one stale base refuses the whole batch with 409 `base_mismatch`. On a deletion the same three states read as: a hash deletes only that head, `null` asserts there is nothing to delete and so is a no-op answering `not-found`, and leaving it out deletes whatever is there. A deletion of a document live content still references refuses the whole batch with 409 `in_use`. A document named twice in the same half of a call is refused with 422 on the second entry: two entries for one document are two answers to one question. Note the transport: the body ceiling is `JSON_BODY_LIMIT` — 10MB by default — and a batch over it comes back as a 413 rather than being truncated. A write that brings a deleted key back reports `updated` even when the bytes are unchanged and no version is appended: the key is serving again, and `unchanged` would have a lock file record nothing about a change the push had just made. A schema binds a *key* rather than one locale: a translation that sends no `schema:` is bound by the default locale’s row — sanitised, validated and reference-checked against it, on save and on publish alike — and a folder’s `defaultSchema` fills the gap only where no row for the key names one. `schema: null` clears this row’s own binding, and is refused with 422 on `schema` where nothing else is left to bind the document: a write always carries a body, and an unbound body is stored and served exactly as it was sent, so unbinding and supplying content in one call is one request from sanitised to raw on the delivery API. It stays legal wherever nothing is unbound by it — a document the folder’s default still binds, and one that never had a binding. Delete the document to write a body without one. Deleting that row does not unbind the key: its component is read from that row whether or not the row is deleted, so every translation stays bound to it, including one written after the delete. The remaining translations are given that component explicitly as well, in the same transaction; a key with more than 100 of them refuses the batch with 409 `in_use` instead. A field marked `x-quillet.translatable: false` is shared across locales: it exists in the default locale only, and a body for another locale that holds one is refused at that path rather than having it dropped — so a push never silently loses what a file says. Every merge takes the default locale’s value there, and it is not listed in `_meta.fallbacks`: it is not untranslated, it is shared. Inside a list or a block list the flag is inert, because an array is merged whole. A rich text field — `x-quillet.widget: \"richtext\"` — is sanitised before the body is validated or hashed: the tags `p br strong em u s a h1–h6 ul ol li blockquote pre code hr`, `href` and `title` on a link, and the schemes `http https mailto tel`. Everything else is dropped and its text kept, `maxLength` is measured afterwards, and sanitising is idempotent — so a pull after a push converges. A link field holds `{ label?, href }` or `{ label?, ref }`, exactly one of the two; a `ref` names a live json document, joins `refs[]` and is served exactly as written. A `{ \"$asset\": \"<id>\" }` pointer is extracted into the document’s `assets[]` exactly as a `$ref` is: distinct and sorted, never settable, empty for a schema document. That list is what an asset’s `…/references` reads and what refuses its deletion with 409 `in_use`, and both are asked of the **project** rather than of one environment — an asset is project-scoped and the documents pointing at it are not, so a pointer in `staging` keeps the file alive for `main` and each referrer is named with its `env`. An id outside the grammar is 422 `content.<path>`: \"Not an asset id.\", wherever it sits, because it can never resolve; a pointer inside a schema document is \"A schema cannot hold an asset.\" Where the schema marks the slot — `x-quillet.widget` `image`, `video` or `file` — the target is checked, one sentence per way it can be wrong: `No asset \"…\".`, `\"logo.pdf\" is not an image.` or `is not a video.` for the family a widget insists on, `\"logo.pdf\" is not one of: image/png, image/svg+xml.` for an `accept`, and `\"hero.png\" is 30 MB; this field allows at most 10 MB.` for a `maxBytes`. A pointer in a slot the schema never marked, or in a document with no schema, is extracted and guarded but not checked — the permissive rule an unmarked `$ref` gets. A block of a list that names two or more components must say which one it is, with `_component`: which component a block is decides whose rules it is under. A list naming one component leaves it optional. So a list that gains a second component needs `_component` added to the blocks already stored under it — until then a push carrying them is refused, one message per block, and nothing stored is changed or lost. Every write here also derives the document’s `preview` — the value of the component’s `x-quillet.preview` field in the body being written, trimmed and cut to 120 characters — from the schema version the write was measured against. It is what labels an instance in a listing, a picker and a validate report, so a push relabels what it writes and nothing else. `_singletons/` is an ordinary content prefix here — `_singletons/<name>` holds the one instance of the component `<name>`, is bound to it by its key whether or not that component still carries the flag, and is covered by the whole-environment scope or by naming the folder. Pushing a second instance of a singleton component, at any other key, is refused at that entry, and so is an `_index` in an ordinary folder naming one as its `defaultSchema` — as is the component itself, if the flag goes on while documents are instances of it or an `_index` defaults to it. Deleting a component (`_schemas/<name>`) is refused with 409 `in_use` and the message `This component is in use.` while anything is bound to it — a live json document by its own `schema`, by the default locale’s row for its key, by a folder’s `defaultSchema` or by being its singleton, or another component that embeds or references it; `usedBy` names the first 50, sorted by key then locale. Documents and components deleted in the same batch do not count, so a push may stop using a component and remove it at once. Deleting a folder’s `_index` in the default locale is refused the same way, with the message `This folder binds documents to a component.`, while its `defaultSchema` is what binds live documents under it — `usedBy` names them, and documents deleted in the same batch do not count. Pushing that same `_index` *without* a `defaultSchema` is refused identically, because it takes the same binding away — the writes of a batch are applied before its deletions, so emptying a folder and deleting the documents it binds are two pushes. No operation may leave a document whose stored body was written under a binding without that binding: an unbound body is stored and served exactly as it was sent. An `_index` in any other locale binds nothing and is never refused for this.",
        "operationId": "sync.bulk",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "minItems": 1,
                    "maxItems": 50,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "prefix": {
                          "type": "string"
                        },
                        "locales": {
                          "minItems": 1,
                          "maxItems": 64,
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "prefix"
                      ]
                    }
                  },
                  "documents": {
                    "maxItems": 500,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "json",
                            "md",
                            "text"
                          ]
                        },
                        "content": {},
                        "title": {
                          "nullable": true,
                          "type": "string",
                          "maxLength": 200
                        },
                        "order": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "tags": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "maxLength": 64,
                            "pattern": "^[a-z0-9][a-z0-9-]*$"
                          }
                        },
                        "searchable": {
                          "nullable": true,
                          "type": "boolean"
                        },
                        "schema": {
                          "nullable": true,
                          "type": "string",
                          "pattern": "^[a-z0-9][a-z0-9-]*$"
                        },
                        "message": {
                          "type": "string",
                          "maxLength": 500
                        },
                        "key": {
                          "type": "string"
                        },
                        "locale": {
                          "type": "string"
                        },
                        "baseHash": {
                          "nullable": true,
                          "type": "string",
                          "pattern": "^[0-9a-f]{64}$"
                        }
                      },
                      "required": [
                        "type",
                        "content",
                        "key"
                      ]
                    }
                  },
                  "deletions": {
                    "default": [],
                    "maxItems": 500,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "locale": {
                          "type": "string"
                        },
                        "baseHash": {
                          "nullable": true,
                          "type": "string",
                          "pattern": "^[0-9a-f]{64}$"
                        }
                      },
                      "required": [
                        "key"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per document and per deletion asked for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResponse"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Refused: either the actor may not use this route at all, or it named documents in folders it may not change — the second names every one.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ApiError"
                    },
                    {
                      "$ref": "#/components/schemas/ForbiddenBatch"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "A lost race (`conflict` — retry as you are), a stale base (`base_mismatch` — pull first), or a deletion of something still referenced (`in_use` — nothing was applied); the body says which. One key rather than three, because a route documents one body per status and these three share 409.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Conflict"
                    },
                    {
                      "$ref": "#/components/schemas/BaseMismatch"
                    },
                    {
                      "$ref": "#/components/schemas/InUse"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than the server accepts. The limit is `JSON_BODY_LIMIT`, 10MB by default, or `ASSET_MAX_BYTES` on an upload; split the request — or shrink the file — and send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Push a working copy",
        "tags": [
          "sync"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/sync/diff": {
      "post": {
        "description": "Send the scope this checkout manages — folders, each optionally narrowed to some locales — and the hash of every file in it; the answer says which way each document has moved. Read-only, so it needs only `content:read` — a CI job checking whether a branch is in sync should not need permission to change anything.",
        "operationId": "sync.diff",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "minItems": 1,
                    "maxItems": 50,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "prefix": {
                          "type": "string"
                        },
                        "locales": {
                          "minItems": 1,
                          "maxItems": 64,
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "prefix"
                      ]
                    }
                  },
                  "documents": {
                    "maxItems": 5000,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "locale": {
                          "type": "string"
                        },
                        "hash": {
                          "type": "string",
                          "pattern": "^[0-9a-f]{64}$"
                        },
                        "lockHash": {
                          "type": "string",
                          "pattern": "^[0-9a-f]{64}$"
                        },
                        "lockVersion": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        }
                      },
                      "required": [
                        "key",
                        "hash"
                      ]
                    }
                  }
                },
                "required": [
                  "scope",
                  "documents"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per document, whichever side holds it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiffResponse"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Compare a working copy",
        "tags": [
          "sync"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/translations/{key}": {
      "get": {
        "description": "Every project locale, in the project’s order, and whether this key exists in it and what it still inherits — measured on the *current* versions with the same merge a save and a publish use, so this, the editor’s inherited-fields panel and a publish’s refusal cannot disagree. Fields the component shares across locales are never counted as missing. Under `translations/` rather than `documents/{key}/translations`, for the reason `versions/` is. 404 when the key has no document in any locale.",
        "operationId": "documents.translations",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One entry per project locale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranslationsView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "How complete each locale of a document is",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/translations/{key}/review": {
      "post": {
        "description": "Completeness says what a translation still inherits; it says nothing about whether the source moved on since somebody last looked. This records which source version, and which version of the translation itself, an editor actually read — verified inside the write’s own transaction, so a save landing in between refuses the review with a 409 rather than recording a state nobody looked at. The source locale is not reviewable: it *is* what translations are reviewed against. Nothing is published and no version is appended; the whole translations view comes back so the caller need not read it again. Measured on drafts, like the completeness beside it.",
        "operationId": "documents.review",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          },
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sourceVersion": {
                    "type": "integer",
                    "minimum": 0,
                    "exclusiveMinimum": true,
                    "maximum": 9007199254740991,
                    "description": "The source’s current version number, as the translations view gave it."
                  },
                  "version": {
                    "type": "integer",
                    "minimum": 0,
                    "exclusiveMinimum": true,
                    "maximum": 9007199254740991,
                    "description": "This translation’s current version number."
                  }
                },
                "required": [
                  "sourceVersion",
                  "version"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every project locale, with the review recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranslationsView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another writer changed the same document first. Read again and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conflict"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Record that a translation was reviewed against its source",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/types": {
      "get": {
        "description": "One `.d.ts` for the whole environment: an interface per schema, named after it in PascalCase, a `Ref<T>` for the reference properties, and a `Schemas` map from each schema’s name to its type. Every component is one named interface, and an embedded component is that interface by name. Each one leads with `_component?: '<name>'`, from the `const` the API compiles the schema with. A block list naming two or more components requires that marker on every block, so that one is typed as an intersection per member — `((Hero & {_component: 'hero'}) | (Cta & {_component: 'cta'}))[]` — which a site can `switch` on, rule a name out of, and have checked for a case it forgot. `richtext` and `code` fields are `string`, as are `date`, `date-time`, `uri` and `email`; an image, video or file field is `Asset`; a link field is the declared `Link`. `order`, `preview`, `singleton`, `translatable`, `language`, `default`, `minItems` and `maxItems` are annotations and rules this file ignores — a consumer that wants the defaults applied reads `quillet/schemas/*.json`, which is the one place they live. A singleton has no type of its own: the site reads `_singletons/settings` as `Settings`. Published versions by default — what a build against delivery should be typed by — and the drafts with `draft=1`. The `ETag` is taken from the versions the file was generated from, so a client that already has the file spends a 304 to find out nothing has changed. Never stored: the answer follows the drafts, and there is no shared cache this belongs in.",
        "operationId": "schemas.types",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "draft",
            "required": false,
            "in": "query",
            "description": "Generate from the schemas as they are being edited rather than from what is published. Left out, the file describes exactly what delivery is serving.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "One or more ETags this client already holds, or `*`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A TypeScript module declaring a type for every schema.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "Weak, and over the schema versions the file was generated from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`no-store`, always: the management API serves drafts.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "The copy the caller already holds is current. The `ETag` is the one to send back next time, and `Cache-Control` is the same `no-store` as on a 200 — a 304 that dropped it would invite a cache to hold an answer that follows drafts.",
            "headers": {
              "ETag": {
                "description": "Weak, and over the schema versions the file was generated from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`no-store`, always: the management API serves drafts.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Generate TypeScript types for every schema",
        "tags": [
          "schemas"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/unpublish": {
      "post": {
        "description": "Clears the pointer, so delivery stops serving these documents. The content and its history are untouched, and publishing again restores them. A document named twice is one target: the response carries one entry per distinct `key`+`locale`, so it can be shorter than the request. Bad input is reported two ways on purpose — a bad key is a 422 keyed by `key` when the grammar rejects it and by the key itself (`landing/typo`) when it names nothing, while a body a schema refuses is keyed by the entry that sent it (`documents.3.content.title`), because that one is about a position in the batch. Nothing is measured on the way down: unpublishing a default locale can take the base out from under translations that stay live and inherit from it, and those will serve a hole where the fallback was. Unpublish the locales of a page together when that matters.",
        "operationId": "publishing.unpublish",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "documents": {
                    "minItems": 1,
                    "maxItems": 200,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "locale": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "key"
                      ]
                    }
                  }
                },
                "required": [
                  "documents"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per document asked for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResponse"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another writer changed the same document first. Read again and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conflict"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Unpublish documents",
        "tags": [
          "publishing"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/versions/{key}": {
      "get": {
        "description": "Under `versions/` rather than `documents/{key}/versions`, because a suffix would be ambiguous with the perfectly legal key `docs/versions`. Newest first, paged with `cursor` — opaque, and keyed on the version number — and without bodies unless `bodies=1`: a document edited daily for a year would otherwise answer a year of bodies to draw a list of dates. A deleted document still has a history; the versions are what a restore would be made of.",
        "operationId": "documents.versions",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          },
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "How many versions to return: 1 to 100. Defaults to 50.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "required": false,
            "in": "query",
            "description": "The `nextCursor` of the previous page. Opaque.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bodies",
            "required": false,
            "in": "query",
            "description": "`1` includes each version’s body. Omitted by default.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the document’s history, in this locale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionPage"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Read a document’s history",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/v1/projects/{project}/envs/{env}/work-queue": {
      "get": {
        "description": "One view at a time — rows with unpublished changes, locales with no translation, partial translations, and translations whose source has moved on since they were reviewed — filtered on the server rather than on a page a client already has. Keys are scanned in order in bounded batches: a request looks at a limited number of distinct keys, says how many in `scanned`, and hands back where the *scan* stopped, so a view that matches almost nothing still ends. Keep following `nextCursor` until it is null; a page with no items and a cursor is normal. Every row carries completeness, review and publication as separate facts. Measured on drafts, like the translations view it is computed with. Folder `_index` documents and `_schemas/` components never appear: neither is content an editor finishes.",
        "operationId": "documents.workQueue",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "view",
            "required": true,
            "in": "query",
            "description": "Which kind of unfinished work to list.",
            "schema": {
              "type": "string",
              "enum": [
                "unpublished",
                "missing",
                "partial",
                "needs-review"
              ]
            }
          },
          {
            "name": "prefix",
            "required": false,
            "in": "query",
            "description": "A folder path ending in a slash, such as `docs/guides/`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "One of the project’s locales. Left out, every locale is listed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schema",
            "required": false,
            "in": "query",
            "description": "The component in force for the key — the content type. Left out, every type is listed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "How many rows to return: 1 to 200. Defaults to 100.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "required": false,
            "in": "query",
            "description": "The `nextCursor` of the previous page. Opaque.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of unfinished work.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkQueuePage"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Find unfinished work across an environment",
        "tags": [
          "documents"
        ]
      }
    },
    "/api/v1/projects/{project}/folders": {
      "get": {
        "description": "Every folder, flat, each carrying its `parentId`. Not paged: a project’s folders are made by hand and number in the dozens.",
        "operationId": "folders.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every folder of the project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FolderList"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "List a project’s folders",
        "tags": [
          "folders"
        ]
      },
      "post": {
        "description": "At the root, or nested under `parentId`. Refused past 5 levels deep, and when a sibling already holds the name.",
        "operationId": "folders.create",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parentId": {
                    "nullable": true,
                    "type": "string"
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The folder that was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FolderView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Create a folder",
        "tags": [
          "folders"
        ]
      }
    },
    "/api/v1/projects/{project}/folders/{folderId}": {
      "patch": {
        "description": "A rename and a move in one write, checked against the parent it will have once the call completes. Refused if the move would nest a folder inside itself, inside its own descendant, or past 5 levels deep.",
        "operationId": "folders.patch",
        "parameters": [
          {
            "name": "folderId",
            "required": true,
            "in": "path",
            "description": "The folder’s id, as `GET /folders` reports it.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parentId": {
                    "nullable": true,
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The folder as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FolderView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Rename a folder, move it, or both",
        "tags": [
          "folders"
        ]
      },
      "delete": {
        "description": "Refused with 409 `in_use` while the folder holds any file or subfolder — nothing recurses, so a folder is only ever deleted once it is empty.",
        "operationId": "folders.delete",
        "parameters": [
          {
            "name": "folderId",
            "required": true,
            "in": "path",
            "description": "The folder’s id, as `GET /folders` reports it.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The folder is gone."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Either a lost race (`conflict` — retry as you are) or a document that live content still references (`in_use` — remove those references first); the body says which.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Conflict"
                    },
                    {
                      "$ref": "#/components/schemas/InUse"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Delete a folder",
        "tags": [
          "folders"
        ]
      }
    },
    "/api/v1/projects/{project}/import": {
      "post": {
        "description": "The archive of `quillet backup` minus its asset bytes: `manifest.json`, `environments.json`, `folders.json`, `assets/index.json`, and the archived documents of each target environment keyed by that environment’s key. Nothing is written into the project — the answer says which asset bytes to upload next, and the commit is a separate call. Everything is checked here, so that the commit has nothing left to discover: the format must be 1; every named environment must exist and hold no documents at all, deleted ones included (409 `not_empty`); every key must be a legal key and every locale one of the project’s (422 on `locales`, naming the missing ones); every version history must run consecutively upwards with each `contentHash` recomputed and matching; a `published` pointer must name a version the archive carries; every asset id must be an asset id, and an id this project already holds with different bytes — or bytes it already holds under another id — is 409 `asset_conflict`; the bytes still to arrive must fit the project’s quota (409 `quota_exceeded`); and every `{\"$asset\": …}` and `{\"$ref\": …}` in every version of every body must resolve, the first against the archive or the project, the second against the same environment’s archive. A staged import lives one hour, and its body limit is `IMPORT_MAX_BYTES` — 256MB by default — rather than `JSON_BODY_LIMIT`; `Content-Encoding: gzip` is decoded before it is measured.",
        "operationId": "import.stage",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "manifest": {
                    "type": "object",
                    "properties": {
                      "format": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      "cli": {
                        "type": "string"
                      },
                      "project": {
                        "type": "string"
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time",
                        "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                      }
                    },
                    "required": [
                      "format"
                    ]
                  },
                  "environments": {
                    "default": [],
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "key"
                      ]
                    }
                  },
                  "folders": {
                    "default": [],
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string",
                          "minLength": 1
                        },
                        "parentId": {
                          "default": null,
                          "nullable": true,
                          "type": "string"
                        }
                      },
                      "required": [
                        "id",
                        "name"
                      ]
                    }
                  },
                  "assets": {
                    "default": [],
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string",
                          "minLength": 1
                        },
                        "filename": {
                          "nullable": true,
                          "type": "string"
                        },
                        "mime": {
                          "type": "string",
                          "minLength": 1
                        },
                        "size": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 9007199254740991
                        },
                        "sha256": {
                          "type": "string"
                        },
                        "width": {
                          "default": null,
                          "nullable": true,
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "height": {
                          "default": null,
                          "nullable": true,
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "animated": {
                          "default": false,
                          "type": "boolean"
                        },
                        "alt": {
                          "default": null,
                          "nullable": true,
                          "type": "string"
                        },
                        "visibility": {
                          "type": "string",
                          "enum": [
                            "public",
                            "private"
                          ]
                        },
                        "folderId": {
                          "default": null,
                          "nullable": true,
                          "type": "string"
                        },
                        "createdBy": {
                          "type": "string",
                          "minLength": 1
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time",
                          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time",
                          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "mime",
                        "size",
                        "sha256",
                        "visibility",
                        "createdBy",
                        "createdAt"
                      ]
                    }
                  },
                  "documents": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "document": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "locale": {
                                "type": "string"
                              },
                              "type": {
                                "type": "string",
                                "enum": [
                                  "json",
                                  "md",
                                  "text"
                                ]
                              },
                              "title": {
                                "default": null,
                                "nullable": true,
                                "type": "string"
                              },
                              "order": {
                                "default": 0,
                                "type": "number"
                              },
                              "tags": {
                                "default": [],
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "searchable": {
                                "default": null,
                                "nullable": true,
                                "type": "boolean"
                              },
                              "searchableSource": {
                                "type": "string",
                                "enum": [
                                  "document",
                                  "folder"
                                ]
                              },
                              "schema": {
                                "default": null,
                                "nullable": true,
                                "type": "string"
                              },
                              "createdBy": {
                                "type": "string",
                                "minLength": 1
                              },
                              "createdAt": {
                                "type": "string",
                                "format": "date-time",
                                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                              },
                              "updatedAt": {
                                "type": "string",
                                "format": "date-time",
                                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                              }
                            },
                            "required": [
                              "key",
                              "locale",
                              "type",
                              "createdBy",
                              "createdAt",
                              "updatedAt"
                            ]
                          },
                          "published": {
                            "default": null,
                            "nullable": true,
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 9007199254740991
                          },
                          "versions": {
                            "minItems": 1,
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "number": {
                                  "type": "integer",
                                  "minimum": 1,
                                  "maximum": 9007199254740991
                                },
                                "contentHash": {
                                  "type": "string"
                                },
                                "message": {
                                  "default": null,
                                  "nullable": true,
                                  "type": "string"
                                },
                                "createdBy": {
                                  "type": "string",
                                  "minLength": 1
                                },
                                "createdAt": {
                                  "type": "string",
                                  "format": "date-time",
                                  "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                                },
                                "publishedAt": {
                                  "default": null,
                                  "nullable": true,
                                  "type": "string",
                                  "format": "date-time",
                                  "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                                },
                                "unpublishedAt": {
                                  "default": null,
                                  "nullable": true,
                                  "type": "string",
                                  "format": "date-time",
                                  "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                                },
                                "body": {},
                                "content": {}
                              },
                              "required": [
                                "number",
                                "contentHash",
                                "createdBy",
                                "createdAt"
                              ]
                            }
                          }
                        },
                        "required": [
                          "document",
                          "versions"
                        ]
                      }
                    }
                  }
                },
                "required": [
                  "manifest",
                  "documents"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The staged import, and which asset bytes it still needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportStagedView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "A target environment already holds documents (`not_empty`), the project cannot take an archived asset under its archived id (`asset_conflict`), or the archive does not fit the project’s quota (`quota_exceeded`); the body says which. Nothing was staged either way.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/NotEmpty"
                    },
                    {
                      "$ref": "#/components/schemas/AssetConflict"
                    },
                    {
                      "$ref": "#/components/schemas/QuotaExceeded"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than the server accepts. The limit is `JSON_BODY_LIMIT`, 10MB by default, or `ASSET_MAX_BYTES` on an upload; split the request — or shrink the file — and send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Stage an archive",
        "tags": [
          "import"
        ]
      }
    },
    "/api/v1/projects/{project}/import/{importId}": {
      "get": {
        "description": "What has arrived and what has not: the asset ids still missing are what a resumed upload sends, and `committed` names the environments already written. A committed or aborted import is deleted with its staged rows, so a 404 here after a successful commit is the ordinary answer rather than a problem.",
        "operationId": "import.get",
        "parameters": [
          {
            "name": "importId",
            "required": true,
            "in": "path",
            "description": "The staged import’s id, as `POST …/import` answered it.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Where the import has got to.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportStateView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Read a staged import",
        "tags": [
          "import"
        ]
      },
      "delete": {
        "description": "Deletes the staged rows and every byte this import put in the store. A file the project was already holding is never touched — those bytes belong to a row that exists and that this import did not write. Refused with 409 while a commit of this project is in flight, because the rows and objects this would delete are the ones that commit is working from; retry when it has finished. An import nobody aborts is swept once no commit of the project is running, usually within the hour.",
        "operationId": "import.abort",
        "parameters": [
          {
            "name": "importId",
            "required": true,
            "in": "path",
            "description": "The staged import’s id, as `POST …/import` answered it.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The staged import and its bytes are gone."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "A commit of this project is in flight — this import’s own, or a sibling’s — and the rows and objects an abort would delete are the ones that commit is working from. Nothing was deleted. Retry once it has finished; if the process behind it died, abort that import instead and this one will go through.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conflict"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Abort a staged import",
        "tags": [
          "import"
        ]
      }
    },
    "/api/v1/projects/{project}/import/{importId}/assets/{id}": {
      "put": {
        "description": "The raw bytes of `assets/<id>`, with a `Content-Length` — no multipart envelope, because the archive’s index already says what this file is. The bytes are hashed as they stream and refused unless they match the archive’s `sha256`; their length must be the archive’s `size`; the type is sniffed and must be on the allow-list and be the type the archive recorded. Over `ASSET_MAX_BYTES` is 413, and no `Content-Length` is 422 — a chunked body is the one shape this route cannot bound before reading it. Idempotent: re-sending a file that has already arrived, or one this project was already holding, is a 200 that stores nothing. Image dimensions are not re-probed and EXIF is not stripped a second time — the archived `width`, `height` and `animated` are restored as they were, and a second strip pass would change the very bytes the `sha256` is checked against.",
        "operationId": "import.bytes",
        "parameters": [
          {
            "name": "importId",
            "required": true,
            "in": "path",
            "description": "The staged import’s id, as `POST …/import` answered it.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The asset id: 26 lowercase base32 characters.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z2-7]{26}$",
              "example": "b7k2m9x4q1w8e5r3t6y2u1i8o4"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary",
                "description": "The file, exactly as `assets/<id>` holds it."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The import, with this asset marked received.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportStateView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than the server accepts. The limit is `JSON_BODY_LIMIT`, 10MB by default, or `ASSET_MAX_BYTES` on an upload; split the request — or shrink the file — and send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayloadTooLarge"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The asset store cannot be reached, or the image transform queue is full. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorageUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Upload one archived file’s bytes",
        "tags": [
          "import"
        ]
      }
    },
    "/api/v1/projects/{project}/import/{importId}/commit": {
      "post": {
        "description": "One transaction per environment, in the `sync/bulk` shape: the asset rows (with the ids, the folders and the timestamps the archive gave them), then the `_schemas/` documents, then every other document with every version, every message, every author and every timestamp the archive carried — and the publish pointer at whichever version was live, which is the whole reason this route exists. `environments` narrows it; absent, every staged environment not yet written is committed. Bodies are **not** re-validated against the schemas: a restore puts back what was valid when it was written. What the schemas now in force refuse is reported under `warnings`, worded exactly as the validate action words it. Refused with 409 `assets_missing` while any needed byte has not arrived. If one environment’s transaction fails the others still commit: the answer is 207, `failed` names it, and the import stays staged so that environment alone can be retried. A commit that leaves nothing staged deletes the import. An id a row now holds with different bytes is 409 `asset_conflict`, as staging would have said had the row existed then: the archive’s pointers would resolve to a file that is not the one they meant.",
        "operationId": "import.commit",
        "parameters": [
          {
            "name": "importId",
            "required": true,
            "in": "path",
            "description": "The staged import’s id, as `POST …/import` answered it.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "environments": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was written, and what the schemas now in force make of it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportCommitView"
                }
              }
            }
          },
          "207": {
            "description": "Some environments committed and at least one did not; `failed` names each with its reason, and the import is still there to retry with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportCommitView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The commit cannot run yet: some asset bytes have not arrived (`assets_missing`), or a commit of this project is already in flight (`conflict`). An id a row now holds with different bytes is `asset_conflict`, which waiting does not clear: it is what staging would have said had the row existed then. Nothing was written. One key rather than three, because a route documents one body per status.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AssetsMissing"
                    },
                    {
                      "$ref": "#/components/schemas/Conflict"
                    },
                    {
                      "$ref": "#/components/schemas/AssetConflict"
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "basic": []
          }
        ],
        "summary": "Write a staged archive into the project",
        "tags": [
          "import"
        ]
      }
    },
    "/api/v1/projects/{project}/keys": {
      "get": {
        "description": "Owners only. Listing hands out access as surely as minting does: the names and last-used times of a project’s keys are a map of its automation.",
        "operationId": "keys.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every key of the project, revoked ones included, without secrets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/KeyView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "List a project’s API keys",
        "tags": [
          "keys"
        ]
      },
      "post": {
        "description": "Owners only, and the only response that ever carries the secret. A `delivery` key needs an `env` and is confined to `/d/v1`; a `cli` key works against the management API — across every environment, or bound to the one named in `env`.",
        "operationId": "keys.create",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "cli",
                      "delivery"
                    ]
                  },
                  "scopes": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "content:read",
                        "content:write",
                        "content:publish",
                        "assets:private"
                      ]
                    }
                  },
                  "env": {
                    "type": "string"
                  },
                  "expiresAt": {
                    "type": "string"
                  }
                },
                "required": [
                  "name",
                  "kind",
                  "scopes"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key, with its secret. Store it now — it is never shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MintedKeyView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Mint an API key",
        "tags": [
          "keys"
        ]
      }
    },
    "/api/v1/projects/{project}/keys/{keyId}": {
      "delete": {
        "description": "Owners only, and idempotent: revoking an already-revoked key succeeds. The row stays, so the key remains in the list with a `revokedAt`.",
        "operationId": "keys.revoke",
        "parameters": [
          {
            "name": "keyId",
            "required": true,
            "in": "path",
            "description": "The API key’s public `keyId` — the half of the credential that is safe to store and to name in a URL, not the database `id`.",
            "schema": {
              "type": "string",
              "pattern": "^qk_",
              "example": "qk_9f2cA1b3D4e5F6g7"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The key no longer authenticates anything."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Revoke an API key",
        "tags": [
          "keys"
        ]
      }
    },
    "/api/v1/projects/{project}/members": {
      "get": {
        "description": "Editors and above, and people only. They collaborate with the team, so they may see it; a viewer has no reason to enumerate it, and an API key — which satisfies `editor` by holding `content:write` — must not be a way to read everybody’s email address.",
        "operationId": "members.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Everyone who works on the project.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MemberView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "List a project’s members",
        "tags": [
          "members"
        ]
      },
      "post": {
        "description": "Owners only: adding somebody hands out access to everything in the project. The person must already have an account.",
        "operationId": "members.add",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "editor",
                      "owner"
                    ]
                  }
                },
                "required": [
                  "email",
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The member that was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Add a member",
        "tags": [
          "members"
        ]
      }
    },
    "/api/v1/projects/{project}/members/{userId}": {
      "delete": {
        "description": "Owners only. A project always keeps at least one owner.",
        "operationId": "members.remove",
        "parameters": [
          {
            "name": "userId",
            "required": true,
            "in": "path",
            "description": "The member’s user id, as `GET /members` reports it.",
            "schema": {
              "type": "string",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The person is no longer a member."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Remove a member",
        "tags": [
          "members"
        ]
      }
    },
    "/api/v1/projects/{project}/webhooks": {
      "get": {
        "description": "Owners only, and never an API key: a subscription hands the shape of the project’s activity to an outside URL, and the list of where a project’s events go is a map of its integrations. Every subscription of the project, oldest first — at most 20, so no paging.",
        "operationId": "webhooks.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every webhook of the project, without secrets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WebhookView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "List a project’s webhooks",
        "tags": [
          "webhooks"
        ]
      },
      "post": {
        "description": "Owners only, and never an API key: a subscription hands the shape of the project’s activity to an outside URL, and the list of where a project’s events go is a map of its integrations. The only response, with rotate-secret, that carries the secret. The URL must be https:// (http:// only where the deployment allows it), carry no username or password, and resolve to a public address — a private one is refused here and again at delivery. `envs` names environments by key; leave it out, or send null, for all of them. At most 20 per project.",
        "operationId": "webhooks.create",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  },
                  "url": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2048
                  },
                  "events": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "content.published",
                        "content.unpublished",
                        "content.deleted",
                        "schema.published",
                        "asset.created",
                        "asset.updated",
                        "asset.deleted",
                        "environment.created",
                        "environment.deleted"
                      ]
                    }
                  },
                  "envs": {
                    "nullable": true,
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "active": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "name",
                  "url",
                  "events"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new webhook, with its secret. Store it now — it is never shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedWebhookView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Create a webhook",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/v1/projects/{project}/webhooks/{id}": {
      "get": {
        "description": "Owners only, and never an API key: a subscription hands the shape of the project’s activity to an outside URL, and the list of where a project’s events go is a map of its integrations. One subscription as the list shows it — its URL, its events, its environments, when it last delivered and why it is paused — and never its secret, which is shown only when it is created or rotated.",
        "operationId": "webhooks.get",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The webhook, without its secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Read a webhook",
        "tags": [
          "webhooks"
        ]
      },
      "patch": {
        "description": "Every field optional; an unknown field is a 422 on that field. A new `url` is checked as on creation and the secret stays. `active: false` cancels the deliveries still waiting; `active: true` on a paused webhook clears the pause and the failure count — redeliver what it missed from its deliveries.",
        "operationId": "webhooks.update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  },
                  "url": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2048
                  },
                  "events": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "content.published",
                        "content.unpublished",
                        "content.deleted",
                        "schema.published",
                        "asset.created",
                        "asset.updated",
                        "asset.deleted",
                        "environment.created",
                        "environment.deleted"
                      ]
                    }
                  },
                  "envs": {
                    "nullable": true,
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "active": {
                    "type": "boolean"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The webhook as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Edit a webhook",
        "tags": [
          "webhooks"
        ]
      },
      "delete": {
        "description": "Hard, and cascading: its deliveries go with it. A second delete is 404 — unlike a key’s revoke, a webhook is gone.",
        "operationId": "webhooks.delete",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The webhook and its log are gone."
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Delete a webhook",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/v1/projects/{project}/webhooks/{id}/deliveries": {
      "get": {
        "description": "The last 30 days, newest first, keyset on the delivery id. One `status` at a time, or none. The id carries the millisecond a delivery was written rather than a sequence, so two written in the same instant have no order between them.",
        "operationId": "webhooks.listDeliveries",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "description": "One status to show. Absent shows every delivery.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "delivering",
                "delivered",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "How many deliveries to return: 1 to 100. Defaults to 50.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "required": false,
            "in": "query",
            "description": "The last `id` of the previous page. Opaque: send back what you were given.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of deliveries and where to resume.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryPage"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "List a webhook’s deliveries",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/v1/projects/{project}/webhooks/{id}/deliveries/{deliveryId}": {
      "get": {
        "description": "With its stored payload, every attempt, and the first 4 KB of the last response — text, never rendered.",
        "operationId": "webhooks.getDelivery",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "deliveryId",
            "required": true,
            "in": "path",
            "description": "The delivery’s id — a ULID, the same one the envelope and `Quillet-Delivery` carry.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$",
              "example": "01J7QZ3M8T5V9X2K4N6P8R0S2T"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The delivery in full.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryDetailView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Read a delivery",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/v1/projects/{project}/webhooks/{id}/deliveries/{deliveryId}/redeliver": {
      "post": {
        "description": "A new delivery with a new id and the original’s payload, `redeliveredFrom` naming it — any status may be redelivered. Refused while the webhook is off; 404 for a delivery the 30-day retention has removed.",
        "operationId": "webhooks.redeliver",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "deliveryId",
            "required": true,
            "in": "path",
            "description": "The delivery’s id — a ULID, the same one the envelope and `Quillet-Delivery` carry.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$",
              "example": "01J7QZ3M8T5V9X2K4N6P8R0S2T"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "The new delivery, queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Send a delivery again",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/v1/projects/{project}/webhooks/{id}/rotate-secret": {
      "post": {
        "description": "A new secret, shown once. The old one signs alongside it for 24 hours as a second `v1` in `Quillet-Signature`, then is dropped; rotating again within that time replaces the old one at once.",
        "operationId": "webhooks.rotateSecret",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The webhook with its new secret. Store it now — it is never shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedWebhookView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Rotate a webhook’s secret",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/v1/projects/{project}/webhooks/{id}/test": {
      "post": {
        "description": "Writes a `ping` delivery and sends it now, through the same code every delivery uses, answering the finished row — `delivered` with the code, or `failed` with the sentence. Takes as long as the attempt, up to the timeout. Refused while the webhook is off, and past ten a minute per webhook. A ping updates the last delivery and never the failure count.",
        "operationId": "webhooks.test",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The webhook’s id, as the list reports it.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{24}$",
              "example": "66f1c0b8a1b2c3d4e5f60718"
            }
          },
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The ping, as it finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryView"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "summary": "Send a test event",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/d/v1/{project}/assets/{id}": {
      "get": {
        "description": "Public and immutable for a `public` file: an `<img>` sends no key, and the id is the only thing between the bytes and a reader who does not know it. A `private` file additionally requires the `exp` and `sig` this API mints; without them, or with them tampered or expired, the answer is the same 404 an unknown id gets. A `delivery` key of the project holding `assets:private`, sent as HTTP basic auth, opens a private file as a token does; the answer is `private, max-age=300`. `?w=` and `?f=` ask for a scaled, re-encoded copy of a raster image; both are ignored on an SVG, an animated GIF or a non-image. `HEAD` answers the same headers.",
        "operationId": "delivery.asset",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "The asset id: 26 lowercase base32 characters.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z2-7]{26}$",
              "example": "b7k2m9x4q1w8e5r3t6y2u1i8o4"
            }
          },
          {
            "name": "w",
            "required": false,
            "in": "query",
            "description": "One of the configured widths (`ASSET_IMAGE_WIDTHS`). At or above the image’s own width serves the image’s width — never an upscale. A private asset’s `url` already carries `?exp=…&sig=…`; append this with `&`, not a second `?`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "f",
            "required": false,
            "in": "query",
            "description": "One of `webp`, `avif`, `jpeg`, `png`. Absent keeps the original’s format; a GIF becomes `png`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "exp",
            "required": false,
            "in": "query",
            "description": "Minted by the API as part of a private asset’s `url`. Required to read a private file; ignored on a public one. Never construct this yourself.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sig",
            "required": false,
            "in": "query",
            "description": "Minted by the API as part of a private asset’s `url`. Required to read a private file; ignored on a public one. Never construct this yourself.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "The strong ETag this client holds, or `*`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Range",
            "in": "header",
            "description": "One `bytes=` range, honoured on an original.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The bytes, under the sniffed content type.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "Strong: the sha256 of the bytes served, with a `-w{width}-{format}` suffix on a variant.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable` on a public asset: the bytes at this URL never change. On a private one, `private, max-age=<seconds left on the token>`, or `private, max-age=300` when a key opened it — either way a shared cache must not hold it.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "description": "`inline` for images, PDFs, video and audio; `attachment` for everything else. Carries the asset’s name.",
                "schema": {
                  "type": "string"
                }
              },
              "Accept-Ranges": {
                "description": "`bytes`. One `Range` is honoured on an original; a variant ignores it.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Content-Type-Options": {
                "description": "`nosniff`: the sniffed type is the only type.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "206": {
            "description": "The requested range, with `Content-Range`.",
            "headers": {
              "ETag": {
                "description": "Strong: the sha256 of the bytes served, with a `-w{width}-{format}` suffix on a variant.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable` on a public asset: the bytes at this URL never change. On a private one, `private, max-age=<seconds left on the token>`, or `private, max-age=300` when a key opened it — either way a shared cache must not hold it.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "description": "`inline` for images, PDFs, video and audio; `attachment` for everything else. Carries the asset’s name.",
                "schema": {
                  "type": "string"
                }
              },
              "Accept-Ranges": {
                "description": "`bytes`. One `Range` is honoured on an original; a variant ignores it.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Content-Type-Options": {
                "description": "`nosniff`: the sniffed type is the only type.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "`bytes {start}-{end}/{size}`, measured from the stored object.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Length": {
                "description": "The length of the part served, not of the whole object.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "The copy the caller holds is the bytes. `ETag` and `Cache-Control` are repeated.",
            "headers": {
              "ETag": {
                "description": "Strong: the sha256 of the bytes served, with a `-w{width}-{format}` suffix on a variant.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable` on a public asset: the bytes at this URL never change. On a private one, `private, max-age=<seconds left on the token>`, or `private, max-age=300` when a key opened it — either way a shared cache must not hold it.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "description": "`inline` for images, PDFs, video and audio; `attachment` for everything else. Carries the asset’s name.",
                "schema": {
                  "type": "string"
                }
              },
              "Accept-Ranges": {
                "description": "`bytes`. One `Range` is honoured on an original; a variant ignores it.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Content-Type-Options": {
                "description": "`nosniff`: the sniffed type is the only type.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "416": {
            "description": "The range starts past the end. `Content-Range: bytes */<size>`, and none of the headers a 200 carries.",
            "headers": {
              "Content-Range": {
                "description": "`bytes */{size}` — the size the range was refused against.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`no-store`: a refusal is never stored.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Content-Type-Options": {
                "description": "`nosniff`, which survives every failure on this route.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The asset store cannot be reached, or the image transform queue is full. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorageUnavailable"
                }
              }
            }
          }
        },
        "summary": "Read a file’s bytes",
        "tags": [
          "delivery"
        ]
      }
    },
    "/d/v1/{project}/{env}/content/{key}": {
      "get": {
        "description": "The requested locale’s content, with the project default filling any gap, and `_meta` recording the difference. Every `{ \"$ref\": … }` in the body is replaced by the document it names — `refs=` levels deep, each resolved into the same locale in its own right, `null` where the target is not published — and `_meta.refs` lists what was reached. Every answer carries a weak `ETag` taken from the resolved body, references included, so a conditional request costs a 304. A folder’s `_index` is not content and is never served here. A field marked `x-quillet.translatable: false` is shared across locales: every merge takes the default locale’s value there, and it is not listed in `_meta.fallbacks` — it is not untranslated, it is shared. Inside a list or a block list the flag is inert, because an array is merged whole. Every `{ \"$asset\": … }` — at any depth, whatever `refs=` says — becomes the asset’s `InlinedAsset` (`{ id, url, name, mime, size, width, height, alt, variants }`) or `null`, and `_meta.assets` lists each. `preview=<token>` answers the **draft** instead of the published version: the document’s current version merged with the default locale’s current version by the same per-field fallback, references inlined from *their* drafts, and `_meta.preview: true`. A preview is never cached anywhere — `Cache-Control: no-store`, no `ETag`, no 304 — and is refused with 401 if the token does not name this project, this environment, this key and this locale. `preview` and `at` together is a 422.",
        "operationId": "delivery.content",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "key",
            "required": true,
            "in": "path",
            "description": "The document key: a slash-separated path, substituted into the URL as written. Lowercase letters, digits, hyphens and underscores per segment.",
            "style": "simple",
            "allowReserved": true,
            "schema": {
              "type": "string",
              "example": "docs/guides/intro"
            }
          },
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "The locale to resolve into. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "at",
            "required": false,
            "in": "query",
            "description": "An ISO 8601 instant: what was live then. Never cached.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "preview",
            "required": false,
            "in": "query",
            "description": "A preview token from `POST …/envs/{env}/preview-tokens`: answers the draft instead of the published version. Never cached, and not combinable with `at`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refs",
            "required": false,
            "in": "query",
            "description": "How many levels of `{ \"$ref\": … }` to inline: 0 to 5, 3 by default. 0 leaves every reference as it is written.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "One or more ETags this client already holds, or `*`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The document, resolved into the requested locale.",
            "headers": {
              "ETag": {
                "description": "Weak, and taken from the resolved answer — not from where it came from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Short in the browser and longer at the edge, because the edge is the copy a publish can invalidate. `no-store` for a pinned `?at=` read, and for a `?preview=` read, which also carries no `ETag`.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "`Authorization`, always: two projects’ keys address the same URL shape, and a shared cache that ignored the credential would hand one project’s content to another’s reader.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolvedDocument"
                }
              }
            }
          },
          "304": {
            "description": "The copy the caller already holds is current. The `ETag` is the one to send back next time, and `Cache-Control` and `Vary` are the same as on a 200 — a 304 that dropped them would have a shared cache forget how long to hold the copy it is revalidating.",
            "headers": {
              "ETag": {
                "description": "Weak, and taken from the resolved answer — not from where it came from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Short in the browser and longer at the edge, because the edge is the copy a publish can invalidate. `no-store` for a pinned `?at=` read, and for a `?preview=` read, which also carries no `ETag`.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "`Authorization`, always: two projects’ keys address the same URL shape, and a shared cache that ignored the credential would hand one project’s content to another’s reader.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "basic": []
          }
        ],
        "summary": "Read a published document",
        "tags": [
          "delivery"
        ]
      }
    },
    "/d/v1/{project}/{env}/list": {
      "get": {
        "description": "The navigation under a prefix: the prefix’s own `_index` as `folder`, the folders below it as their published `_index` describes them, and the items in it. Each of the three is null or empty rather than absent when there is nothing to say. Bodies are deliberately absent. The reserved folder `_singletons/` is listed like any other — `_singletons/<name>` is the one instance of the component `<name>` and reports it as its `schema`, which is how a site reads its settings — while `_schemas/` never appears. `preview=<token>` answers the **drafts**: every document under the prefix that has a current version, `publishedAt: null` for those never published, and the folders drawn from the current `_index` documents. A listing is not one document, so the token is checked for this project and this environment only. It is never cached — `Cache-Control: no-store`, no `ETag`, no 304 — and `preview` and `at` together is a 422.",
        "operationId": "delivery.list",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "prefix",
            "required": false,
            "in": "query",
            "description": "A slash-terminated folder path, or omitted for the whole environment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "The locale to resolve into. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "at",
            "required": false,
            "in": "query",
            "description": "An ISO 8601 instant: what was live then. Never cached.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "preview",
            "required": false,
            "in": "query",
            "description": "A preview token from `POST …/envs/{env}/preview-tokens`: answers the draft instead of the published version. Never cached, and not combinable with `at`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "required": false,
            "in": "query",
            "description": "Comma-separated. An item must carry every one of them.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "depth",
            "required": false,
            "in": "query",
            "description": "How many levels below the prefix to reach: 1 to 10.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "One or more ETags this client already holds, or `*`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What is published under the prefix.",
            "headers": {
              "ETag": {
                "description": "Weak, and taken from the resolved answer — not from where it came from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Short in the browser and longer at the edge, because the edge is the copy a publish can invalidate. `no-store` for a pinned `?at=` read, and for a `?preview=` read, which also carries no `ETag`.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "`Authorization`, always: two projects’ keys address the same URL shape, and a shared cache that ignored the credential would hand one project’s content to another’s reader.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryListing"
                }
              }
            }
          },
          "304": {
            "description": "The copy the caller already holds is current. The `ETag` is the one to send back next time, and `Cache-Control` and `Vary` are the same as on a 200 — a 304 that dropped them would have a shared cache forget how long to hold the copy it is revalidating.",
            "headers": {
              "ETag": {
                "description": "Weak, and taken from the resolved answer — not from where it came from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Short in the browser and longer at the edge, because the edge is the copy a publish can invalidate. `no-store` for a pinned `?at=` read, and for a `?preview=` read, which also carries no `ETag`.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "`Authorization`, always: two projects’ keys address the same URL shape, and a shared cache that ignored the credential would hand one project’s content to another’s reader.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "basic": []
          }
        ],
        "summary": "List published content under a folder",
        "tags": [
          "delivery"
        ]
      }
    },
    "/d/v1/{project}/{env}/search": {
      "get": {
        "description": "The published pages that answer `q`, best first, each naming the document, the heading the answer was found under and that heading’s `anchor` — so a site can link straight to the place rather than to the page. `snippet` is plain text: the site marks the terms itself. Only **published** content is ever searched, in one environment. A locale with nothing published for a key is answered from the project’s default locale, and that hit carries `fallbackLocale` — but only where the key has *nothing* in the requested locale, because a hit whose snippet is in a language the reader will not be served is a lie. `prefix` and `tags` filter exactly as they do on `list`. `q` may use Mongo’s own grammar (`\"a phrase\"`, `-excluded`); there is no fuzzy matching and no prefix matching. Paging reaches the first 200 results and no further: `total` is the size of that window and is exact for everything reachable. `score` is comparable within one answer and meaningless across two. `preview` and `at` are refused with a 422 — there is no draft in this index and it stores one state, the present. A project that has turned search off answers **404**: the route does not exist for it.",
        "operationId": "delivery.search",
        "parameters": [
          {
            "name": "project",
            "required": true,
            "in": "path",
            "description": "The project’s slug, as it was created — never its id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "marketing-site"
            }
          },
          {
            "name": "env",
            "required": true,
            "in": "path",
            "description": "The environment’s key within the project.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]*$",
              "example": "prod"
            }
          },
          {
            "name": "q",
            "required": true,
            "in": "query",
            "description": "What to search for. Mongo’s own grammar works inside it — `\"a phrase\"` and `-excluded` — and it is never read as a regular expression. 1 to 200 characters.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "locale",
            "required": false,
            "in": "query",
            "description": "The locale to search in. Defaults to the project’s default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "prefix",
            "required": false,
            "in": "query",
            "description": "A slash-terminated folder path: only documents under it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "required": false,
            "in": "query",
            "description": "Comma-separated. A document must carry every one of them.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "preview",
            "required": false,
            "in": "query",
            "description": "Refused: search covers published content only.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "at",
            "required": false,
            "in": "query",
            "description": "Refused: the index stores one state, the present.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "How many results to answer: 1 to 50, 10 by default.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Where to start, 0-based. `offset + limit` may not exceed 200.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "description": "One or more ETags this client already holds, or `*`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The pages that answer the question, and where in each.",
            "headers": {
              "ETag": {
                "description": "Weak, and taken from the resolved answer — not from where it came from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Short in the browser and longer at the edge, because the edge is the copy a publish can invalidate. `no-store` for a pinned `?at=` read, and for a `?preview=` read, which also carries no `ETag`.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "`Authorization`, always: two projects’ keys address the same URL shape, and a shared cache that ignored the credential would hand one project’s content to another’s reader.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResult"
                }
              }
            }
          },
          "304": {
            "description": "The copy the caller already holds is current. The `ETag` is the one to send back next time, and `Cache-Control` and `Vary` are the same as on a 200 — a 304 that dropped them would have a shared cache forget how long to hold the copy it is revalidating.",
            "headers": {
              "ETag": {
                "description": "Weak, and taken from the resolved answer — not from where it came from.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "Short in the browser and longer at the edge, because the edge is the copy a publish can invalidate. `no-store` for a pinned `?at=` read, and for a `?preview=` read, which also carries no `ETag`.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "`Authorization`, always: two projects’ keys address the same URL shape, and a shared cache that ignored the credential would hand one project’s content to another’s reader.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No credential was sent, or the one that was is not recognised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The credential is real but is not allowed to do this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or none this credential is allowed to know about. On the public bytes route there is no credential at all, so it is also the answer to an id that is not one, to an asset in another project and to a project that does not exist — one answer, because telling them apart would tell a stranger which ids are worth guessing. It carries `Cache-Control: no-store`, so a miss is never the copy a CDN keeps.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "The request was understood and refused, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests in the current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "The rate limit counter is unreachable. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceUnavailable"
                }
              }
            }
          }
        },
        "security": [
          {
            "basic": []
          }
        ],
        "summary": "Search published content",
        "tags": [
          "delivery"
        ]
      }
    },
    "/healthz": {
      "get": {
        "description": "Answers as long as the process is running, a graceful shutdown included: a failure here means \"restart this container\", which is the wrong answer for a pod that is draining on purpose. It deliberately touches no dependency either — a pod whose Redis is briefly unreachable should stop taking traffic, not be killed and restarted. Use `/readyz` to decide whether to send traffic.",
        "operationId": "health.live",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The process is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Liveness"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          }
        },
        "summary": "Liveness probe",
        "tags": [
          "health"
        ]
      }
    },
    "/readyz": {
      "get": {
        "description": "Whether this instance should be sent traffic. It answers 503 in two cases: a dependency is unreachable, or a graceful shutdown has begun — the second as soon as the signal arrives, so the pod leaves the load balancer’s rotation while it is still finishing the requests it has.",
        "operationId": "health.ready",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Every dependency is up and the process is taking traffic.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Readiness"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Internal"
                }
              }
            }
          },
          "503": {
            "description": "Stop routing traffic here: either a dependency is unreachable (`degraded`) or this instance is shutting down (`shutting_down`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Readiness"
                }
              }
            }
          }
        },
        "summary": "Readiness probe",
        "tags": [
          "health"
        ]
      }
    }
  },
  "info": {
    "title": "Quillet CMS API",
    "description": "Two surfaces, one deployment.\n\n`/api/v1` is the management API: a person signs in and gets a session cookie, and a machine sends an API key over HTTP basic. Every write is validated field by field and refused with a 422 that names each offending input.\n\n`/d/v1` is the delivery API: published content only, one environment, read with a delivery key. Its answers carry an `ETag` and a `Cache-Control`, and a publish invalidates them.",
    "version": "1.1.0",
    "contact": {}
  },
  "tags": [
    {
      "name": "auth",
      "description": "Registering, signing in, and the session cookie the dashboard uses."
    },
    {
      "name": "projects",
      "description": "Projects: their slug, their locales and their settings."
    },
    {
      "name": "locales",
      "description": "The locales a project may be authored in."
    },
    {
      "name": "members",
      "description": "Who works on a project, and at which role."
    },
    {
      "name": "environments",
      "description": "The parallel copies of a project’s content, and the short-lived tokens that open a draft of one on the customer’s own site."
    },
    {
      "name": "keys",
      "description": "Minting, listing and revoking a project’s API keys."
    },
    {
      "name": "documents",
      "description": "Reading and writing content, and its version history."
    },
    {
      "name": "schemas",
      "description": "The JSON Schemas content is written against."
    },
    {
      "name": "assets",
      "description": "Files shared by every environment of a project: uploading, describing and deleting them."
    },
    {
      "name": "folders",
      "description": "The tree a project organises its media library into."
    },
    {
      "name": "publishing",
      "description": "Deciding what the delivery API serves."
    },
    {
      "name": "sync",
      "description": "Comparing and pushing a working copy — the routes the CLI calls."
    },
    {
      "name": "promotion",
      "description": "Comparing two environments, copying a chosen subset from one into the other, and the record of every promotion that committed."
    },
    {
      "name": "import",
      "description": "Restoring a project archive: stage the metadata, upload the bytes, commit."
    },
    {
      "name": "delivery",
      "description": "The public read API: published content, one environment."
    },
    {
      "name": "search",
      "description": "Rebuilding the index behind the delivery search route."
    },
    {
      "name": "webhooks",
      "description": "Telling another system when content changed: subscriptions, their secrets, and what happened to each delivery."
    },
    {
      "name": "health",
      "description": "Liveness and readiness probes."
    }
  ],
  "servers": [],
  "components": {
    "securitySchemes": {
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "quillet_session",
        "description": "The session cookie `POST /api/v1/auth/login` sets. Sent by the browser automatically; a non-browser client should use an API key instead."
      },
      "basic": {
        "type": "http",
        "scheme": "basic",
        "description": "An API key, as `keyId:secret`. A `cli` key authenticates against the management API, carries the scopes it was minted with, and may be bound to one environment or work across all of them; a `delivery` key authenticates against `/d/v1` and is always bound to one environment. A personal access token, as `tokenId:secret`, authenticates as the person who minted it, with every right they hold."
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false,
        "description": "The envelope used where the answer is about the request as a whole rather than about a field: 401, plain 403, and 404. `error` is a machine code in every envelope this API serves — `unauthorized`, `forbidden`, `not_found`, or `http_<status>` for a status none of them names — and never a human reason phrase. `message` is the sentence to show somebody."
      },
      "AssetConflict": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "asset_conflict"
            ]
          },
          "message": {
            "type": "string"
          },
          "assets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "reason"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "error",
          "message",
          "assets"
        ],
        "additionalProperties": false,
        "description": "The project cannot take some of the archive’s assets under the ids the archive gives them: it already holds that id with different bytes — every pointer in the archive would then resolve to the wrong file — or it already holds those bytes under a different id, which the dedup index would refuse mid-commit, or another project on this instance holds that id, because an asset id is the primary key of the whole collection. Nothing was staged; the reason is given per id."
      },
      "AssetInUse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "in_use"
            ]
          },
          "message": {
            "type": "string"
          },
          "usedBy": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "env": {
                  "type": "string"
                },
                "key": {
                  "type": "string"
                },
                "locale": {
                  "type": "string"
                }
              },
              "required": [
                "env",
                "key",
                "locale"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "error",
          "message",
          "usedBy"
        ],
        "additionalProperties": false,
        "description": "The file is still named by live content, so it was not deleted. Not the document `InUse` body: an asset is shared by every environment of a project, so each referrer says which environment it is in. `usedBy` names the first 50, sorted by environment, key and locale; the references route lists them in full, with the path of each."
      },
      "AssetPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssetView"
            }
          },
          "nextCursor": {
            "nullable": true,
            "type": "string"
          },
          "usage": {
            "$ref": "#/components/schemas/AssetUsage"
          }
        },
        "required": [
          "items",
          "nextCursor",
          "usage"
        ],
        "additionalProperties": false,
        "description": "A page of a project’s files, newest first, and the project’s usage."
      },
      "AssetReferenceUse": {
        "type": "object",
        "properties": {
          "env": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "env",
          "key",
          "locale",
          "path"
        ],
        "additionalProperties": false,
        "description": "One place, in one document of one environment, that names the asset."
      },
      "AssetReferencesView": {
        "type": "object",
        "properties": {
          "usedBy": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssetReferenceUse"
            }
          }
        },
        "required": [
          "usedBy"
        ],
        "additionalProperties": false,
        "description": "Every live document in every environment whose current version points at the asset, once per place, capped at 200 and sorted by environment, key, locale and path."
      },
      "AssetUsage": {
        "type": "object",
        "properties": {
          "bytes": {
            "type": "number"
          },
          "quotaBytes": {
            "nullable": true,
            "type": "number"
          }
        },
        "required": [
          "bytes",
          "quotaBytes"
        ],
        "additionalProperties": false,
        "description": "What this project’s files add up to, against its quota. On every page rather than on a route of its own: the Media page shows both together, and one request is one loading state."
      },
      "AssetVariants": {
        "type": "object",
        "properties": {
          "widths": {
            "type": "array",
            "items": {
              "type": "number"
            }
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "webp",
                "avif",
                "jpeg",
                "png"
              ]
            }
          }
        },
        "required": [
          "widths",
          "formats"
        ],
        "additionalProperties": false,
        "description": "The scaled copies this image can be asked for: the configured widths that are not above its own, and the four output formats. Enough to build a `srcset` without knowing the deployment’s configuration. Append `?w=` and `?f=` with `&`, not a second `?`: a private asset’s `url` already carries `?exp=…&sig=…`."
      },
      "AssetView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "mime": {
            "type": "string"
          },
          "size": {
            "type": "number"
          },
          "sha256": {
            "type": "string"
          },
          "width": {
            "nullable": true,
            "type": "number"
          },
          "height": {
            "nullable": true,
            "type": "number"
          },
          "animated": {
            "type": "boolean"
          },
          "alt": {
            "nullable": true,
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "folderId": {
            "nullable": true,
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "variants": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/AssetVariants"
              }
            ]
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "name",
          "mime",
          "size",
          "sha256",
          "width",
          "height",
          "animated",
          "alt",
          "visibility",
          "folderId",
          "url",
          "variants",
          "createdBy",
          "createdAt",
          "updatedAt"
        ],
        "additionalProperties": false,
        "description": "One file of a project. The bytes never change — `name`, `alt`, `visibility` and `folderId` are the only mutable fields, and none is versioned. A `public` file’s `url` is readable by anyone who has it; a `private` file’s is signed and expires."
      },
      "AssetsMissing": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "assets_missing"
            ]
          },
          "message": {
            "type": "string"
          },
          "assets": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "error",
          "message",
          "assets"
        ],
        "additionalProperties": false,
        "description": "A commit was asked for while some of the import’s asset bytes had not arrived. Nothing was written. `assets` names the ids still to upload, exactly as `GET …/import/{importId}` does."
      },
      "AuthOptionsView": {
        "type": "object",
        "properties": {
          "registration": {
            "type": "string",
            "enum": [
              "open",
              "closed"
            ]
          },
          "github": {
            "type": "boolean"
          },
          "google": {
            "type": "boolean"
          }
        },
        "required": [
          "registration",
          "github",
          "google"
        ],
        "additionalProperties": false,
        "description": "Which ways in are open: whether the password sign-up form is, and which providers are configured. The dashboard reads it to draw the sign-in page."
      },
      "BaseMismatch": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "base_mismatch"
            ]
          },
          "message": {
            "type": "string"
          },
          "conflicts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "locale": {
                  "type": "string"
                },
                "remoteVersion": {
                  "nullable": true,
                  "type": "number"
                },
                "remoteHash": {
                  "nullable": true,
                  "type": "string"
                }
              },
              "required": [
                "key",
                "locale",
                "remoteVersion",
                "remoteHash"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "error",
          "message",
          "conflicts"
        ],
        "additionalProperties": false,
        "description": "A push named a base for some documents and the server has moved past it. Nothing was applied. `conflicts` lists each one with the head it has now; pull, resolve, and push again."
      },
      "BulkResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkResult"
            }
          }
        },
        "required": [
          "results"
        ],
        "additionalProperties": false,
        "description": "The batch, entry by entry. A push produces drafts: publishing is a separate call with a separate scope."
      },
      "BulkResult": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "unchanged",
              "deleted",
              "not-found"
            ]
          },
          "version": {
            "nullable": true,
            "type": "number"
          }
        },
        "required": [
          "key",
          "locale",
          "status",
          "version"
        ],
        "additionalProperties": false,
        "description": "What the push did to one document or deletion. A write that brings a deleted key back reports `updated` even when the bytes are unchanged and no version is appended: the key is serving again, which is a change a lock file has to record."
      },
      "CompareItem": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "added",
              "changed",
              "unchanged",
              "removed"
            ]
          },
          "type": {
            "type": "string"
          },
          "targetType": {
            "nullable": true,
            "type": "string"
          },
          "title": {
            "nullable": true,
            "type": "string"
          },
          "sourceVersion": {
            "nullable": true,
            "type": "number"
          },
          "sourceHash": {
            "nullable": true,
            "type": "string"
          },
          "targetVersion": {
            "nullable": true,
            "type": "number"
          },
          "targetHash": {
            "nullable": true,
            "type": "string"
          },
          "metadataDiffers": {
            "type": "boolean"
          },
          "missingRefs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "key",
          "locale",
          "status",
          "type",
          "targetType",
          "title",
          "sourceVersion",
          "sourceHash",
          "targetVersion",
          "targetHash",
          "metadataDiffers",
          "missingRefs"
        ],
        "additionalProperties": false,
        "description": "How one document stands in the target against the source: the two heads, and what a promotion of it would do."
      },
      "CompareResponse": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "summary": {
            "nullable": true,
            "type": "object",
            "properties": {
              "added": {
                "type": "number"
              },
              "changed": {
                "type": "number"
              },
              "removed": {
                "type": "number"
              },
              "unchanged": {
                "type": "number"
              }
            },
            "required": [
              "added",
              "changed",
              "removed",
              "unchanged"
            ],
            "additionalProperties": false
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompareItem"
            }
          },
          "nextCursor": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "from",
          "to",
          "summary",
          "items",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "A page of a comparison between two environments."
      },
      "Conflict": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "conflict"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false,
        "description": "Two writers reached the same document at once and this one lost every attempt it was allowed. Nothing is broken: read again and retry."
      },
      "CreatedWebhookView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "content.published",
                "content.unpublished",
                "content.deleted",
                "schema.published",
                "asset.created",
                "asset.updated",
                "asset.deleted",
                "environment.created",
                "environment.deleted"
              ]
            }
          },
          "envs": {
            "nullable": true,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "active": {
            "type": "boolean"
          },
          "paused": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/WebhookPause"
              }
            ]
          },
          "lastDelivery": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/WebhookLastDelivery"
              }
            ]
          },
          "consecutiveFailures": {
            "type": "number"
          },
          "verifySnippet": {
            "type": "string"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "secret": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "url",
          "events",
          "envs",
          "active",
          "paused",
          "lastDelivery",
          "consecutiveFailures",
          "verifySnippet",
          "createdBy",
          "createdAt",
          "updatedAt",
          "secret"
        ],
        "additionalProperties": false,
        "description": "A freshly created or rotated webhook. The `secret` appears in this response and nowhere else, ever — store it now or rotate the webhook for a new one."
      },
      "DeliveryAttempt": {
        "type": "object",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "code": {
            "nullable": true,
            "type": "number"
          },
          "error": {
            "nullable": true,
            "type": "string"
          },
          "durationMs": {
            "type": "number"
          }
        },
        "required": [
          "at",
          "code",
          "error",
          "durationMs"
        ],
        "additionalProperties": false,
        "description": "One attempt of a delivery."
      },
      "DeliveryDetailView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "event": {
            "type": "string",
            "enum": [
              "content.published",
              "content.unpublished",
              "content.deleted",
              "schema.published",
              "asset.created",
              "asset.updated",
              "asset.deleted",
              "environment.created",
              "environment.deleted",
              "ping"
            ]
          },
          "env": {
            "nullable": true,
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivering",
              "delivered",
              "failed"
            ]
          },
          "attempts": {
            "type": "number"
          },
          "maxAttempts": {
            "type": "number"
          },
          "lastCode": {
            "nullable": true,
            "type": "number"
          },
          "lastError": {
            "nullable": true,
            "type": "string"
          },
          "lastAttemptAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "nextAttemptAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "redeliveredFrom": {
            "nullable": true,
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "completedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "payload": {
            "$ref": "#/components/schemas/DeliveryPayload"
          },
          "log": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliveryAttempt"
            }
          },
          "response": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "id",
          "event",
          "env",
          "status",
          "attempts",
          "maxAttempts",
          "lastCode",
          "lastError",
          "lastAttemptAt",
          "nextAttemptAt",
          "redeliveredFrom",
          "createdAt",
          "completedAt",
          "payload",
          "log",
          "response"
        ],
        "additionalProperties": false,
        "description": "A delivery with its payload, every attempt and the response excerpt."
      },
      "DeliveryFolder": {
        "type": "object",
        "properties": {
          "title": {
            "nullable": true,
            "type": "string"
          },
          "description": {
            "nullable": true,
            "type": "string"
          },
          "order": {
            "nullable": true,
            "type": "number"
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "title",
          "description",
          "order",
          "path"
        ],
        "additionalProperties": false,
        "description": "A folder below the requested prefix."
      },
      "DeliveryFolderMeta": {
        "type": "object",
        "properties": {
          "title": {
            "nullable": true,
            "type": "string"
          },
          "description": {
            "nullable": true,
            "type": "string"
          },
          "order": {
            "nullable": true,
            "type": "number"
          }
        },
        "required": [
          "title",
          "description",
          "order"
        ],
        "additionalProperties": false,
        "description": "A folder’s own metadata, as its published `_index` describes it."
      },
      "DeliveryItem": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "json",
              "md",
              "text"
            ]
          },
          "title": {
            "nullable": true,
            "type": "string"
          },
          "order": {
            "type": "number"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "schema": {
            "nullable": true,
            "type": "string"
          },
          "version": {
            "type": "number"
          },
          "publishedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "contentHash": {
            "type": "string"
          }
        },
        "required": [
          "key",
          "locale",
          "type",
          "title",
          "order",
          "tags",
          "schema",
          "version",
          "publishedAt",
          "contentHash"
        ],
        "additionalProperties": false,
        "description": "Everything needed to draw a link, and nothing that would let a reader reconstruct the body. `schema` is the binding of the *key*: the item’s own row where it names one, otherwise the default locale’s row for the key, otherwise the folder’s `defaultSchema`, and under all three the component the stored body was written under — so an item that resolved to a translation reports what its body was measured against rather than nothing."
      },
      "DeliveryListing": {
        "type": "object",
        "properties": {
          "prefix": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "folder": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/DeliveryFolderMeta"
              }
            ]
          },
          "folders": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliveryFolder"
            }
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliveryItem"
            }
          }
        },
        "required": [
          "prefix",
          "locale",
          "folder",
          "folders",
          "items"
        ],
        "additionalProperties": false,
        "description": "The navigation under a folder: the prefix’s own `_index` as `folder`, what is published below it, and where each of those sits. Bodies are deliberately absent — a menu of a hundred pages would carry a hundred of them."
      },
      "DeliveryPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliveryView"
            }
          },
          "nextCursor": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "A page of deliveries, newest first; `nextCursor` is the last id, or null."
      },
      "DeliveryPayload": {
        "type": "object",
        "properties": {
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "data": {}
        },
        "required": [
          "occurredAt",
          "data"
        ],
        "additionalProperties": false,
        "description": "What was sent, as stored: `attempt` is added at send time."
      },
      "DeliveryView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "event": {
            "type": "string",
            "enum": [
              "content.published",
              "content.unpublished",
              "content.deleted",
              "schema.published",
              "asset.created",
              "asset.updated",
              "asset.deleted",
              "environment.created",
              "environment.deleted",
              "ping"
            ]
          },
          "env": {
            "nullable": true,
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivering",
              "delivered",
              "failed"
            ]
          },
          "attempts": {
            "type": "number"
          },
          "maxAttempts": {
            "type": "number"
          },
          "lastCode": {
            "nullable": true,
            "type": "number"
          },
          "lastError": {
            "nullable": true,
            "type": "string"
          },
          "lastAttemptAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "nextAttemptAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "redeliveredFrom": {
            "nullable": true,
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "completedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "event",
          "env",
          "status",
          "attempts",
          "maxAttempts",
          "lastCode",
          "lastError",
          "lastAttemptAt",
          "nextAttemptAt",
          "redeliveredFrom",
          "createdAt",
          "completedAt"
        ],
        "additionalProperties": false,
        "description": "One delivery: the outbox row and its log, as the listing shows it."
      },
      "DiffResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DiffResult"
            }
          }
        },
        "required": [
          "results"
        ],
        "additionalProperties": false,
        "description": "The comparison, one entry per document either side holds."
      },
      "DiffResult": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "unchanged",
              "local-change",
              "remote-change",
              "conflict",
              "new-local",
              "missing-local"
            ]
          },
          "remoteVersion": {
            "nullable": true,
            "type": "number"
          },
          "remoteHash": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "key",
          "locale",
          "status",
          "remoteVersion",
          "remoteHash"
        ],
        "additionalProperties": false,
        "description": "One document’s standing between a working copy and the server. `conflict` means both sides changed and the CLI has to choose."
      },
      "DocumentPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DocumentView"
            }
          },
          "nextCursor": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "A page of documents, newest cursor last."
      },
      "DocumentView": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "json",
              "md",
              "text"
            ]
          },
          "title": {
            "nullable": true,
            "type": "string"
          },
          "order": {
            "type": "number"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "searchable": {
            "type": "boolean"
          },
          "searchableSource": {
            "type": "string",
            "enum": [
              "document",
              "folder"
            ]
          },
          "schema": {
            "nullable": true,
            "type": "string"
          },
          "effectiveSchema": {
            "nullable": true,
            "type": "string"
          },
          "schemaSource": {
            "nullable": true,
            "type": "string",
            "enum": [
              "document",
              "key",
              "folder",
              "singleton"
            ]
          },
          "preview": {
            "nullable": true,
            "description": "The value of the component’s preview field in this document’s current body, cut to 120 characters, or `null`. The dashboard labels an instance `preview ?? title ?? key`.",
            "type": "string"
          },
          "version": {
            "nullable": true,
            "type": "number"
          },
          "contentHash": {
            "nullable": true,
            "type": "string"
          },
          "publishedVersion": {
            "nullable": true,
            "type": "number"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "content": {}
        },
        "required": [
          "key",
          "locale",
          "type",
          "title",
          "order",
          "tags",
          "searchable",
          "searchableSource",
          "schema",
          "effectiveSchema",
          "schemaSource",
          "preview",
          "version",
          "contentHash",
          "publishedVersion",
          "createdBy",
          "createdAt",
          "updatedAt"
        ],
        "additionalProperties": false,
        "description": "One document in one locale, as the management API sees it. `effectiveSchema` is the binding of the *key*: this row’s `schema` where it names one, otherwise the default locale’s row for the key, otherwise the folder’s `defaultSchema` — which is what the body was measured against, so it is what a form should be drawn from. `schemaSource` says which of the four it was: `document` for this row’s own `schema`, `key` for the default locale’s row, `folder` for an ancestor `_index`’s `defaultSchema`, and `singleton` for a key under `_singletons/`, which *is* its component’s instance and outranks all three — whether or not that component still carries `x-quillet.singleton`, since the key is the binding and the flag decides how many instances there may be. A translation written without `schema:` reads as `key`, so a translator is told the binding lives on another row rather than on the one in front of them. Under all four sits the component a stored body was written under, recorded on the row on every save and read only where all four say nothing, so a document that had a binding can never end up with none: the default locale’s row is read that way too, which is what keeps a key bound for its translations as well as for itself, and such an answer reads as `key` on a translation and `document` on the row itself."
      },
      "EnvironmentDeletion": {
        "type": "object",
        "properties": {
          "deleted": {
            "type": "object",
            "properties": {
              "documents": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "versions": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "keys": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "promotions": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "documents",
              "versions",
              "keys",
              "promotions"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "deleted"
        ],
        "additionalProperties": false,
        "description": "What deleting an environment removed, as counts."
      },
      "EnvironmentDetailView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "isDefault": {
            "type": "boolean"
          },
          "previewUrl": {
            "nullable": true,
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "documents": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "keys": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "id",
          "key",
          "name",
          "isDefault",
          "previewUrl",
          "createdAt",
          "documents",
          "keys"
        ],
        "additionalProperties": false,
        "description": "One environment and what it holds — what a client names before asking whether to delete it."
      },
      "EnvironmentView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "isDefault": {
            "type": "boolean"
          },
          "previewUrl": {
            "nullable": true,
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "key",
          "name",
          "isDefault",
          "previewUrl",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "One environment: a parallel copy of a project’s content."
      },
      "FolderList": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FolderView"
            }
          }
        },
        "required": [
          "items"
        ],
        "additionalProperties": false,
        "description": "Every folder of the project, flat, each carrying its `parentId` — the tree is assembled by the caller. Not paged: a project’s folders are made by hand and number in the dozens, and a flat list is what a picker of move targets wants."
      },
      "FolderView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "parentId": {
            "nullable": true,
            "type": "string"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "name",
          "parentId",
          "createdBy",
          "createdAt",
          "updatedAt"
        ],
        "additionalProperties": false,
        "description": "One folder of a project’s media library. Folders organise files for authors; nothing about them reaches the delivery API, and moving a file between them cannot change what any site receives."
      },
      "ForbiddenBatch": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "batch_forbidden"
            ]
          },
          "message": {
            "type": "string"
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "required": [
          "error",
          "message",
          "fields"
        ],
        "additionalProperties": false,
        "description": "A batch refused entry by entry, keyed by document key. Answered by the CLI push route, so one call tells a working copy about every folder it may not write. Its code is `batch_forbidden` rather than `forbidden`, because a guard turning the actor away outright shares the same status and is a different answer."
      },
      "ImportCommitView": {
        "type": "object",
        "properties": {
          "committed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "counts": {
            "type": "object",
            "properties": {
              "documents": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "versions": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "assets": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "published": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "documents",
              "versions",
              "assets",
              "published"
            ],
            "additionalProperties": false
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImportWarning"
            }
          },
          "failed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImportFailure"
            }
          }
        },
        "required": [
          "committed",
          "counts",
          "warnings"
        ],
        "additionalProperties": false,
        "description": "What the commit wrote, environment by environment. A 207 means at least one environment was rolled back and is named under `failed`; the rest committed and the import is still there to retry with."
      },
      "ImportFailure": {
        "type": "object",
        "properties": {
          "env": {
            "type": "string"
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "env",
          "error"
        ],
        "additionalProperties": false,
        "description": "An environment whose transaction was rolled back. The others still committed, and the import stays staged so this one can be retried on its own."
      },
      "ImportStagedView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "assets": {
            "type": "object",
            "properties": {
              "needed": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "reused": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "needed",
              "reused"
            ],
            "additionalProperties": false
          },
          "environments": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "id",
          "expiresAt",
          "assets",
          "environments"
        ],
        "additionalProperties": false,
        "description": "A staged archive: the metadata is stored and validated, and nothing has been written into the project yet. Upload the bytes of every `needed` asset, then commit."
      },
      "ImportStateView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "staging",
              "committing",
              "done",
              "aborted"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "environments": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "committed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "assets": {
            "type": "object",
            "properties": {
              "needed": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "reused": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "received": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "missing": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "needed",
              "reused",
              "received",
              "missing"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "state",
          "expiresAt",
          "environments",
          "committed",
          "assets"
        ],
        "additionalProperties": false,
        "description": "Where a staged import has got to: which environments are written, which asset bytes have arrived, and which are still missing. The answer a resumed upload reads to decide what to send."
      },
      "ImportWarning": {
        "type": "object",
        "properties": {
          "env": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "required": [
          "env",
          "key",
          "locale",
          "errors"
        ],
        "additionalProperties": false,
        "description": "One restored document whose newest body does not satisfy the schema now in force for it. The import is not refused over it: a restore puts back what was valid when it was written, and a schema that has moved on since is something for a person to decide about."
      },
      "InUse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "in_use"
            ]
          },
          "message": {
            "type": "string"
          },
          "usedBy": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "locale": {
                  "type": "string"
                }
              },
              "required": [
                "key",
                "locale"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "error",
          "message",
          "usedBy"
        ],
        "additionalProperties": false,
        "description": "The document is still referenced by live content, so it was not deleted and a batch containing the deletion applied nothing. `usedBy` names the first 50 referrers; the references route lists them in full, with the path of each. A component is refused with the same code and its own sentence, `This component is in use.`, while a live json document is bound to it — by its own `schema`, by the default locale’s row for its key, by a folder’s `defaultSchema`, or by being its singleton — or another component embeds or references it. Documents and components being deleted in the same batch do not count. A folder’s `_index` is refused with the same code and its own sentence, `This folder binds documents to a component.`, while its `defaultSchema` is what binds live documents under it — and so is a *save* of that `_index` that leaves the `defaultSchema` out, which takes the same binding away through the same reader: no operation may leave a document whose stored body was written under a binding without that binding."
      },
      "InlinedAsset": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "nullable": true,
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "mime": {
            "type": "string"
          },
          "size": {
            "type": "number"
          },
          "width": {
            "nullable": true,
            "type": "number"
          },
          "height": {
            "nullable": true,
            "type": "number"
          },
          "alt": {
            "nullable": true,
            "type": "string"
          },
          "caption": {
            "nullable": true,
            "type": "string"
          },
          "decorative": {
            "type": "boolean"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "variants": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/AssetVariants"
              }
            ]
          }
        },
        "required": [
          "id",
          "url",
          "name",
          "mime",
          "size",
          "width",
          "height",
          "alt",
          "caption",
          "decorative",
          "visibility",
          "variants"
        ],
        "additionalProperties": false,
        "description": "An asset as delivery inlines it: everything a page needs to draw an `<img>`, and nothing that describes the upload. `alt`, `caption` and `decorative` belong to *this* use of the file — the pointer’s own values, falling back to the file’s default description — so the same file reads differently in two documents. `url` is `null` on a private asset when the key does not hold the `assets:private` scope. A private `url` already carries `?exp=…&sig=…`; add `?w=`/`?f=` with `&`, not a second `?`."
      },
      "Internal": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "internal"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false,
        "description": "Something failed for a reason that is not the caller’s."
      },
      "KeyMeView": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "key"
            ]
          },
          "keyId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "project": {
            "type": "string"
          },
          "env": {
            "nullable": true,
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "content:read",
                "content:write",
                "content:publish",
                "assets:private"
              ]
            }
          }
        },
        "required": [
          "kind",
          "keyId",
          "name",
          "project",
          "env",
          "scopes"
        ],
        "additionalProperties": false,
        "description": "An API key, as the credential holding it sees itself."
      },
      "KeyView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "keyId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "cli",
              "delivery"
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "content:read",
                "content:write",
                "content:publish",
                "assets:private"
              ]
            }
          },
          "env": {
            "nullable": true,
            "type": "string"
          },
          "envDeleted": {
            "type": "boolean"
          },
          "expiresAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "lastUsedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "revokedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "keyId",
          "name",
          "kind",
          "scopes",
          "env",
          "envDeleted",
          "expiresAt",
          "lastUsedAt",
          "createdAt",
          "revokedAt"
        ],
        "additionalProperties": false,
        "description": "An API key, without its secret."
      },
      "Liveness": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          }
        },
        "required": [
          "status"
        ],
        "additionalProperties": false,
        "description": "The process is up and serving."
      },
      "LocaleCatalogue": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LocaleView"
            }
          }
        },
        "required": [
          "items"
        ],
        "additionalProperties": false,
        "description": "Every locale a project may choose, alphabetised by tag. The list is data compiled into the API and changes only with a release, which is why the answer is cached for an hour and carries a strong ETag."
      },
      "LocaleInUse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "in_use"
            ]
          },
          "message": {
            "type": "string"
          },
          "usedBy": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "env": {
                  "type": "string"
                },
                "key": {
                  "type": "string"
                }
              },
              "required": [
                "env",
                "key"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "error",
          "message",
          "usedBy"
        ],
        "additionalProperties": false,
        "description": "A locale being removed from the project still has live documents, so the update applied nothing. A document here is what the document listing lists — live rows, folder `_index` metadata excluded — so every key named is one a client can reach. The message counts them per locale; `usedBy` names the first 50 keys that hold one, once per environment however many removed locales it is in, environments in key order then keys. Not the document `InUse` body: `usedBy` here is `{ env, key }`, not `{ key, locale }`. Remove or move those documents, then try again. The same body answers a **default locale being moved** while a binding is read from it: every binding is read from the row at the project default locale — a key from its default-locale row, a folder from its default-locale `_index`, a component from `_schemas/<name>`, which exists only in the default locale — so moving it would unbind content, and an unbound body is stored and served exactly as it was sent. `usedBy` names the rows a binding is read from rather than the documents bound by them, because those are the rows that would have to move."
      },
      "LocaleView": {
        "type": "object",
        "properties": {
          "tag": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "nativeName": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "ltr",
              "rtl"
            ]
          }
        },
        "required": [
          "tag",
          "name",
          "nativeName",
          "direction"
        ],
        "additionalProperties": false,
        "description": "One locale a project may be authored in."
      },
      "MeView": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/PersonMeView"
          },
          {
            "$ref": "#/components/schemas/KeyMeView"
          }
        ],
        "description": "Whoever the credential belongs to: a person (`kind: \"user\"`), with how they signed in, or a `cli` API key (`kind: \"key\"`), with what it may reach."
      },
      "MemberView": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "viewer",
              "editor",
              "owner"
            ]
          }
        },
        "required": [
          "userId",
          "email",
          "name",
          "role"
        ],
        "additionalProperties": false,
        "description": "One member of a project, and the role they hold in it."
      },
      "MintedKeyView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "keyId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "cli",
              "delivery"
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "content:read",
                "content:write",
                "content:publish",
                "assets:private"
              ]
            }
          },
          "env": {
            "nullable": true,
            "type": "string"
          },
          "envDeleted": {
            "type": "boolean"
          },
          "expiresAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "lastUsedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "revokedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "secret": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "keyId",
          "name",
          "kind",
          "scopes",
          "env",
          "envDeleted",
          "expiresAt",
          "lastUsedAt",
          "createdAt",
          "revokedAt",
          "secret"
        ],
        "additionalProperties": false,
        "description": "A freshly minted API key. The `secret` appears in this response and nowhere else, ever — store it now or mint another key."
      },
      "MintedPersonalTokenView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "tokenId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "expiresAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "lastUsedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "revokedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "secret": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "tokenId",
          "name",
          "expiresAt",
          "lastUsedAt",
          "createdAt",
          "revokedAt",
          "secret"
        ],
        "additionalProperties": false,
        "description": "A freshly minted personal access token. The `secret` appears in this response and nowhere else, ever — store it now or mint another token."
      },
      "NotEmpty": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "not_empty"
            ]
          },
          "message": {
            "type": "string"
          },
          "environments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "env": {
                  "type": "string"
                },
                "documents": {
                  "type": "number"
                }
              },
              "required": [
                "env",
                "documents"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "error",
          "message",
          "environments"
        ],
        "additionalProperties": false,
        "description": "An import names a target environment that already holds documents — deleted ones included, because a deleted row still owns its key. Nothing was staged. A restore reproduces a state; merging one into live content is what promotion is for. Create a fresh environment and import into that."
      },
      "PayloadTooLarge": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "payload_too_large"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false,
        "description": "The request body is bigger than `JSON_BODY_LIMIT`, or `ASSET_MAX_BYTES` on an upload. Mechanically actionable, which is why it has an envelope of its own: a client that reads this splits its batch and pushes again."
      },
      "PersonMeView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "user"
            ]
          },
          "via": {
            "type": "string",
            "enum": [
              "session",
              "token"
            ]
          }
        },
        "required": [
          "id",
          "email",
          "name",
          "kind",
          "via"
        ],
        "additionalProperties": false,
        "description": "A signed-in person, and which credential they arrived with."
      },
      "PersonalTokenView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "tokenId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "expiresAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "lastUsedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "revokedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "tokenId",
          "name",
          "expiresAt",
          "lastUsedAt",
          "createdAt",
          "revokedAt"
        ],
        "additionalProperties": false,
        "description": "A personal access token, without its secret."
      },
      "PreviewTokenView": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "token",
          "expiresAt",
          "url"
        ],
        "additionalProperties": false,
        "description": "A short-lived pass to one document, in one locale, of one environment, and the address to open it at. **This is the only view in this API that carries a token.** It is valid for fifteen minutes from the moment it was minted, it is not stored anywhere and cannot be revoked one at a time, and it is not a credential for the management API: a preview read needs the site’s delivery key as well. Treat `url` as the secret it contains — anyone holding it sees that draft, and the drafts it references, until it expires. `token` is never logged by the API and appears in no error message."
      },
      "ProjectLimits": {
        "type": "object",
        "properties": {
          "assetMaxBytes": {
            "type": "number"
          }
        },
        "required": [
          "assetMaxBytes"
        ],
        "additionalProperties": false,
        "description": "The configured caps a client should check before it sends: today the largest single upload, in bytes."
      },
      "ProjectView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "defaultLocale": {
            "type": "string"
          },
          "locales": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "settings": {
            "type": "object",
            "additionalProperties": {}
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "role": {
            "nullable": true,
            "type": "string",
            "enum": [
              "viewer",
              "editor",
              "owner"
            ]
          },
          "limits": {
            "$ref": "#/components/schemas/ProjectLimits"
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "defaultLocale",
          "locales",
          "settings",
          "createdAt",
          "role",
          "limits"
        ],
        "additionalProperties": false,
        "description": "A project: its slug, its locales and its settings. `role` is the caller’s own membership rather than a fact about the project — `null` for an API key, which holds scopes and no role."
      },
      "PromoteResponse": {
        "type": "object",
        "properties": {
          "promotion": {
            "type": "string"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PromoteResult"
            }
          }
        },
        "required": [
          "promotion",
          "results"
        ],
        "additionalProperties": false,
        "description": "What a promotion applied, in the order it was asked for."
      },
      "PromoteResult": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "unchanged",
              "deleted",
              "not-found"
            ]
          },
          "version": {
            "nullable": true,
            "type": "number"
          },
          "publishStatus": {
            "nullable": true,
            "type": "string",
            "enum": [
              "published",
              "unchanged"
            ]
          }
        },
        "required": [
          "key",
          "locale",
          "status",
          "version",
          "publishStatus"
        ],
        "additionalProperties": false,
        "description": "What one entry of a promotion did in the target."
      },
      "PromotionPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PromotionSummaryView"
            }
          },
          "nextCursor": {
            "nullable": true,
            "description": "The last record’s id, to send back as `cursor`; `null` when this page was the last one.",
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "A page of the promotions into one environment, newest first."
      },
      "PromotionSummaryView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "promote",
              "clone"
            ]
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "actor": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "scope": {
            "nullable": true,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "prefix": {
                  "type": "string"
                },
                "locales": {
                  "nullable": true,
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "prefix",
                "locales"
              ],
              "additionalProperties": false
            }
          },
          "publish": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          },
          "counts": {
            "type": "object",
            "properties": {
              "created": {
                "type": "number"
              },
              "updated": {
                "type": "number"
              },
              "unchanged": {
                "type": "number"
              },
              "deleted": {
                "type": "number"
              },
              "notFound": {
                "type": "number"
              },
              "published": {
                "type": "number"
              }
            },
            "required": [
              "created",
              "updated",
              "unchanged",
              "deleted",
              "notFound",
              "published"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "kind",
          "from",
          "to",
          "actor",
          "at",
          "scope",
          "publish",
          "message",
          "counts"
        ],
        "additionalProperties": false,
        "description": "One promotion or clone that committed. The listing answers these, so ten records cost ten small rows."
      },
      "PromotionView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "promote",
              "clone"
            ]
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "actor": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "scope": {
            "nullable": true,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "prefix": {
                  "type": "string"
                },
                "locales": {
                  "nullable": true,
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "prefix",
                "locales"
              ],
              "additionalProperties": false
            }
          },
          "publish": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          },
          "counts": {
            "type": "object",
            "properties": {
              "created": {
                "type": "number"
              },
              "updated": {
                "type": "number"
              },
              "unchanged": {
                "type": "number"
              },
              "deleted": {
                "type": "number"
              },
              "notFound": {
                "type": "number"
              },
              "published": {
                "type": "number"
              }
            },
            "required": [
              "created",
              "updated",
              "unchanged",
              "deleted",
              "notFound",
              "published"
            ],
            "additionalProperties": false
          },
          "entries": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "locale": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "created",
                    "updated",
                    "unchanged",
                    "deleted",
                    "not-found"
                  ]
                },
                "version": {
                  "nullable": true,
                  "type": "number"
                },
                "published": {
                  "type": "boolean"
                }
              },
              "required": [
                "key",
                "locale",
                "status",
                "version",
                "published"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "id",
          "kind",
          "from",
          "to",
          "actor",
          "at",
          "scope",
          "publish",
          "message",
          "counts",
          "entries"
        ],
        "additionalProperties": false,
        "description": "One promotion, with what each of its entries did."
      },
      "PublishBlocker": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "target": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "path",
          "message",
          "target"
        ],
        "additionalProperties": false,
        "description": "One entry of `problems`, taken apart: `path` is the field the refusal is reported at, `message` is the sentence, and `target` is the key of the document the refusal is about — `null` when it is about this body alone, such as a field the schema requires and the body does not hold."
      },
      "PublishImpactView": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "version": {
            "type": "number"
          },
          "status": {
            "type": "string",
            "enum": [
              "published",
              "unchanged"
            ]
          },
          "affects": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "locale": {
                  "type": "string"
                }
              },
              "required": [
                "key",
                "locale"
              ],
              "additionalProperties": false
            }
          },
          "affectsTruncated": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "affectsLimit": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "affectsDepth": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "fallbackLocales": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "problems": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "blockers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublishBlocker"
            }
          }
        },
        "required": [
          "key",
          "locale",
          "version",
          "status",
          "affects",
          "affectsLimit",
          "affectsDepth",
          "fallbackLocales",
          "problems",
          "blockers"
        ],
        "additionalProperties": false,
        "description": "What publishing this document would do, computed by running every check the publish runs and then rolling back. `affects` lists live documents whose *published* body inlines this key, two hops deep — the same distance the delivery cache forgets on a publish. Publishing a non-default locale lists each affected document once, under the locale of the reader whose rendering changes: an inlined reference is resolved in the reader’s locale, so a referrer live only in the default locale is reached too. What is left out of `affects`: a document that only *links* to this key, because a link is served as written and reading it is a second request; anything past `affectsLimit` documents at one level, which sets `affectsTruncated`; and anything more than `affectsDepth` levels away. `fallbackLocales` is only ever non-empty when the default locale is being published. `problems` empty means the publish would go through, and `blockers` is the same list with the document each refusal is about."
      },
      "PublishResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublishResult"
            }
          }
        },
        "required": [
          "results"
        ],
        "additionalProperties": false,
        "description": "The state of every document in the batch after the call."
      },
      "PublishResult": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "version": {
            "nullable": true,
            "type": "number"
          },
          "publishedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "status": {
            "type": "string",
            "enum": [
              "published",
              "unchanged",
              "unpublished"
            ]
          }
        },
        "required": [
          "key",
          "locale",
          "version",
          "publishedAt",
          "status"
        ],
        "additionalProperties": false,
        "description": "What became of one document. `unchanged` means it was already where it was asked to be, which is not the same as failing."
      },
      "QuotaExceeded": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "quota_exceeded"
            ]
          },
          "message": {
            "type": "string"
          },
          "usedBytes": {
            "type": "number"
          },
          "quotaBytes": {
            "type": "number"
          }
        },
        "required": [
          "error",
          "message",
          "usedBytes",
          "quotaBytes"
        ],
        "additionalProperties": false,
        "description": "The project has filled `ASSET_PROJECT_QUOTA_BYTES` and the upload was not stored. The two numbers are on the body as well as in the sentence, so a dashboard can draw the bar without parsing prose. Delete something and try again."
      },
      "RateLimited": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "rate_limited"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false,
        "description": "The caller has spent its budget for the current window."
      },
      "Readiness": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded",
              "shutting_down"
            ]
          },
          "mongo": {
            "type": "string",
            "enum": [
              "up",
              "down"
            ]
          },
          "redis": {
            "type": "string",
            "enum": [
              "up",
              "down"
            ]
          }
        },
        "required": [
          "status",
          "mongo",
          "redis"
        ],
        "additionalProperties": false,
        "description": "Whether this instance should be sent traffic. `degraded` means a dependency is unreachable and `shutting_down` means a termination signal has arrived; both arrive with a 503, so a load balancer stops routing here without the pod being restarted."
      },
      "ReferenceUse": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "preview": {
            "nullable": true,
            "description": "The value of the component’s preview field in the *referrer’s* current body, cut to 120 characters, or `null`. The dashboard labels an instance `preview ?? title ?? key`.",
            "type": "string"
          }
        },
        "required": [
          "key",
          "locale",
          "path",
          "preview"
        ],
        "additionalProperties": false,
        "description": "One reference to a document, and where in the referrer it sits. `path` is dotted and root-relative, with numeric array indexes — `blocks.2.cta` — and empty when the referrer’s whole body is the reference."
      },
      "ReferencesView": {
        "type": "object",
        "properties": {
          "usedBy": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReferenceUse"
            }
          }
        },
        "required": [
          "usedBy"
        ],
        "additionalProperties": false,
        "description": "Every live document in the environment whose current version references this key, once per place it does. Capped at 200. A key that holds nothing still answers: the question is about the referrers, and a reference left dangling is exactly what somebody opens this to find."
      },
      "ResolvedAssetReference": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "missing": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "path",
          "id"
        ],
        "additionalProperties": false,
        "description": "One asset pointer the answer met, and whether it resolved."
      },
      "ResolvedDocument": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "json",
              "md",
              "text"
            ]
          },
          "content": {},
          "_meta": {
            "$ref": "#/components/schemas/ResolvedMeta"
          }
        },
        "required": [
          "key",
          "locale",
          "type",
          "content",
          "_meta"
        ],
        "additionalProperties": false,
        "description": "One published document, resolved into the locale asked for."
      },
      "ResolvedMeta": {
        "type": "object",
        "properties": {
          "resolvedLocale": {
            "type": "string"
          },
          "fallbackLocale": {
            "nullable": true,
            "type": "string"
          },
          "fallbacks": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "version": {
            "type": "number"
          },
          "publishedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "contentHash": {
            "type": "string"
          },
          "refs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResolvedReference"
            }
          },
          "refsTruncated": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "preview": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "assets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResolvedAssetReference"
            }
          },
          "assetsTruncated": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "resolvedLocale",
          "fallbackLocale",
          "fallbacks",
          "version",
          "publishedAt",
          "contentHash",
          "refs",
          "assets"
        ],
        "additionalProperties": false,
        "description": "Which locale the body came from, and which fields were filled in behind it. Nothing in the merged body itself says which is which."
      },
      "ResolvedReference": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "resolvedLocale": {
            "type": "string"
          },
          "version": {
            "type": "number"
          },
          "missing": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "path",
          "key"
        ],
        "additionalProperties": false,
        "description": "Either resolved — `resolvedLocale` and `version` describe the document inlined at `path` — or `missing: true`, meaning the slot holds `null`."
      },
      "ReviewStatus": {
        "type": "object",
        "properties": {
          "needed": {
            "type": "boolean"
          },
          "reason": {
            "nullable": true,
            "type": "string",
            "enum": [
              "never",
              "source-changed",
              "translation-changed",
              "source-removed",
              "source-locale-changed"
            ]
          },
          "reviewedSourceVersion": {
            "nullable": true,
            "type": "number"
          },
          "reviewedVersion": {
            "nullable": true,
            "type": "number"
          },
          "reviewedBy": {
            "nullable": true,
            "type": "string"
          },
          "reviewedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "sourceVersion": {
            "nullable": true,
            "type": "number"
          }
        },
        "required": [
          "needed",
          "reason",
          "reviewedSourceVersion",
          "reviewedVersion",
          "reviewedBy",
          "reviewedAt",
          "sourceVersion"
        ],
        "additionalProperties": false,
        "description": "Whether this translation still reflects the source it was reviewed against, measured on *drafts* like the completeness beside it. A review names two version numbers — the source’s and the translation’s — and either moving on needs another look: `never` (nobody has looked), `source-changed`, `translation-changed`, `source-removed`, `source-locale-changed`. Version numbers rather than content hashes, so a metadata-only save cannot flag a review and a screen can say “reviewed against v3; the source is v5 now”. Any source content change flags, including a restore of an earlier version: that is conservative on purpose, and the reason is reported so it can be dismissed in one action."
      },
      "SchemaSummary": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "nullable": true,
            "type": "number"
          },
          "publishedVersion": {
            "nullable": true,
            "type": "number"
          },
          "documents": {
            "type": "number"
          },
          "title": {
            "nullable": true,
            "type": "string"
          },
          "fields": {
            "type": "number"
          },
          "embeds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "singleton": {
            "type": "boolean"
          },
          "page": {
            "type": "boolean"
          }
        },
        "required": [
          "name",
          "version",
          "publishedVersion",
          "documents",
          "title",
          "fields",
          "embeds",
          "singleton",
          "page"
        ],
        "additionalProperties": false,
        "description": "One schema, its draft and published versions, and its usage."
      },
      "SchemasView": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SchemaSummary"
            }
          }
        },
        "required": [
          "items"
        ],
        "additionalProperties": false,
        "description": "Every schema this environment holds, in name order. The list a schema picker, a Schemas page and a reference target filter are all made of."
      },
      "SearchHit": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "heading": {
            "nullable": true,
            "type": "string"
          },
          "anchor": {
            "nullable": true,
            "type": "string"
          },
          "snippet": {
            "type": "string"
          },
          "score": {
            "type": "number"
          },
          "locale": {
            "type": "string"
          },
          "fallbackLocale": {
            "type": "string"
          }
        },
        "required": [
          "key",
          "title",
          "heading",
          "anchor",
          "snippet",
          "score",
          "locale"
        ],
        "additionalProperties": false,
        "description": "A hit names the document, the heading it was found under and that heading’s `anchor` — the fragment to append to the page’s URL. `heading` and `anchor` are null **together**, on a hit that is about the page as a whole. `snippet` is plain text with no markup at all: a site marks the terms itself, which is the only way the marking can match its own tokenisation. `score` is comparable within one answer and meaningless across two — sorting by it or showing it is using it right, thresholding on it is not. `locale` is the locale the text was *written* in; `fallbackLocale` is present only when that is the project’s default rather than the locale asked for."
      },
      "SearchReindexView": {
        "type": "object",
        "properties": {
          "documents": {
            "type": "number"
          },
          "entries": {
            "type": "number"
          }
        },
        "required": [
          "documents",
          "entries"
        ],
        "additionalProperties": false,
        "description": "How many published documents the rebuild considered — distinct keys, not translations — and how many entries the index holds for this environment afterwards. Running it twice answers the same counts: the pass is idempotent."
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchHit"
            }
          },
          "total": {
            "type": "number"
          },
          "_meta": {
            "type": "object",
            "properties": {
              "q": {
                "type": "string"
              },
              "locale": {
                "type": "string"
              },
              "limit": {
                "type": "number"
              },
              "offset": {
                "type": "number"
              }
            },
            "required": [
              "q",
              "locale",
              "limit",
              "offset"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "hits",
          "total",
          "_meta"
        ],
        "additionalProperties": false,
        "description": "The published pages that answer a question, best first. `total` is the size of a **200-entry window per locale**, not of the whole match set: there is no deep paging, and `total` is exact for everything a caller can reach. It may exceed 200 where a fallback locale was merged in; paging still stops at 200."
      },
      "ServiceUnavailable": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "rate_limit_unavailable"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false,
        "description": "The rate limit counter could not be reached, so the request was refused rather than admitted uncounted. Transient."
      },
      "StorageUnavailable": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "unavailable"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": false,
        "description": "The asset store was reachable at boot and is not now, so the bytes were neither written nor removed. Nothing was half-applied. Transient."
      },
      "TranslationStatus": {
        "type": "object",
        "properties": {
          "locale": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "source",
              "missing",
              "partial",
              "translated"
            ]
          },
          "fallbacks": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "version": {
            "nullable": true,
            "type": "number"
          },
          "publishedVersion": {
            "nullable": true,
            "type": "number"
          },
          "review": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ReviewStatus"
              }
            ]
          }
        },
        "required": [
          "locale",
          "state",
          "fallbacks",
          "version",
          "publishedVersion",
          "review"
        ],
        "additionalProperties": false,
        "description": "One locale of a key: whether it exists, what it still inherits, and whether it still reflects the source it was reviewed against. Completeness and review are separate facts — a `translated` entry can need review, and a `partial` one can have been reviewed."
      },
      "TranslationsView": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "defaultLocale": {
            "type": "string"
          },
          "locales": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TranslationStatus"
            }
          }
        },
        "required": [
          "key",
          "defaultLocale",
          "locales"
        ],
        "additionalProperties": false,
        "description": "Every locale the project has, in its order, and how complete each is for this key — measured on the *current* versions, which is what the dashboard edits, with the same merge a save and a publish use. `source` is the default locale; `missing` is a locale with no row; `partial` inherits at least one translatable field; `translated` inherits none. Fields the component shares across locales (`translatable: false`) are never counted as missing. Markdown and text documents are `missing` or `translated`, never `partial`."
      },
      "UserView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "email",
          "name"
        ],
        "additionalProperties": false,
        "description": "A person, as they may be shown to a client."
      },
      "ValidateResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ValidateResult"
            }
          }
        },
        "required": [
          "results"
        ],
        "additionalProperties": false,
        "description": "Every document the schema is in force for, checked and reported. Nothing is written: a schema that has moved on from its content is a report somebody acts on, not a migration the API performs."
      },
      "ValidateResult": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "valid": {
            "type": "boolean"
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "preview": {
            "nullable": true,
            "description": "The document’s stored label: the value of the component’s preview field in its current body, cut to 120 characters, or `null`. The dashboard labels an instance `preview ?? title ?? key`.",
            "type": "string"
          }
        },
        "required": [
          "key",
          "locale",
          "valid",
          "fields",
          "preview"
        ],
        "additionalProperties": false,
        "description": "One document checked against a schema, and why it failed. For a locale other than the project’s default, `fields` describes the resolved body — this locale merged over the default’s — because that is what a reader receives. A field marked `x-quillet.translatable: false` is measured with the default locale’s value, so a translation is never reported broken for one; inside a list or a block list the flag is inert, because an array is merged whole."
      },
      "ValidationError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "validation_failed"
            ]
          },
          "message": {
            "type": "string"
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "required": [
          "error",
          "message",
          "fields"
        ],
        "additionalProperties": false,
        "description": "Input the API refused, with one message per offending field. The keys are dotted paths into the request, so a form can place every message under the input that caused it. The one key that is not a path is `_form`: a problem with the body as a whole — an array that is empty, a scalar where an object belongs — has no field to sit under and belongs in the banner."
      },
      "VersionPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VersionView"
            }
          },
          "nextCursor": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "A page of a document history, newest first."
      },
      "VersionView": {
        "type": "object",
        "properties": {
          "number": {
            "type": "number"
          },
          "contentHash": {
            "type": "string"
          },
          "message": {
            "nullable": true,
            "type": "string"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "publishedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "unpublishedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "promotedFrom": {
            "nullable": true,
            "description": "The environment, version and kind this was promoted or cloned from.",
            "type": "object",
            "properties": {
              "env": {
                "type": "string"
              },
              "version": {
                "type": "number"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "promote",
                  "clone"
                ]
              }
            },
            "required": [
              "env",
              "version",
              "kind"
            ],
            "additionalProperties": false
          },
          "content": {}
        },
        "required": [
          "number",
          "contentHash",
          "message",
          "createdBy",
          "createdAt",
          "publishedAt",
          "unpublishedAt",
          "promotedFrom"
        ],
        "additionalProperties": false,
        "description": "One immutable version of a document."
      },
      "VisibilityMismatch": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "visibility_mismatch"
            ]
          },
          "message": {
            "type": "string"
          },
          "requestedVisibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "asset": {
            "$ref": "#/components/schemas/AssetView"
          }
        },
        "required": [
          "error",
          "message",
          "requestedVisibility",
          "asset"
        ],
        "additionalProperties": false,
        "description": "The same bytes are already in the project with the other visibility, and the upload named the one it wanted. Nothing was stored and the existing file is unchanged — not even its visibility; `asset` is that file, so a client can offer it as it stands without a second request. An upload that sent no `visibility` part expressed no preference and is answered 200 with the existing asset instead."
      },
      "WebhookLastDelivery": {
        "type": "object",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "status": {
            "type": "string",
            "enum": [
              "delivered",
              "failed"
            ]
          },
          "code": {
            "nullable": true,
            "type": "number"
          }
        },
        "required": [
          "at",
          "status",
          "code"
        ],
        "additionalProperties": false,
        "description": "The outcome of the most recent attempt on any delivery of the webhook."
      },
      "WebhookPause": {
        "type": "object",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "at",
          "reason"
        ],
        "additionalProperties": false,
        "description": "Why the worker turned a webhook off, and when. Resume with `PATCH { \"active\": true }`."
      },
      "WebhookView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "content.published",
                "content.unpublished",
                "content.deleted",
                "schema.published",
                "asset.created",
                "asset.updated",
                "asset.deleted",
                "environment.created",
                "environment.deleted"
              ]
            }
          },
          "envs": {
            "nullable": true,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "active": {
            "type": "boolean"
          },
          "paused": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/WebhookPause"
              }
            ]
          },
          "lastDelivery": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/WebhookLastDelivery"
              }
            ]
          },
          "consecutiveFailures": {
            "type": "number"
          },
          "verifySnippet": {
            "type": "string"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "name",
          "url",
          "events",
          "envs",
          "active",
          "paused",
          "lastDelivery",
          "consecutiveFailures",
          "verifySnippet",
          "createdBy",
          "createdAt",
          "updatedAt"
        ],
        "additionalProperties": false,
        "description": "A webhook subscription, without its secret."
      },
      "WorkQueueItem": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "title": {
            "nullable": true,
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "json",
              "md",
              "text"
            ]
          },
          "effectiveSchema": {
            "nullable": true,
            "type": "string"
          },
          "version": {
            "nullable": true,
            "type": "number"
          },
          "publishedVersion": {
            "nullable": true,
            "type": "number"
          },
          "updatedAt": {
            "nullable": true,
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$",
            "example": "2026-01-31T12:00:00.000Z"
          },
          "state": {
            "type": "string",
            "enum": [
              "source",
              "missing",
              "partial",
              "translated"
            ]
          },
          "fallbacks": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "review": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/ReviewStatus"
              }
            ]
          }
        },
        "required": [
          "key",
          "locale",
          "title",
          "type",
          "effectiveSchema",
          "version",
          "publishedVersion",
          "updatedAt",
          "state",
          "fallbacks",
          "review"
        ],
        "additionalProperties": false,
        "description": "One locale of one key with something outstanding. Completeness (`state`, `fallbacks`), review (`review`) and publication (`version` against `publishedVersion`) travel together because they are separate facts: a `translated` row can need review and still hold unpublished edits. For the `missing` view the row does not exist, so `title` and `type` come from the key’s default-locale row and the versions and `updatedAt` are null."
      },
      "WorkQueuePage": {
        "type": "object",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "unpublished",
              "missing",
              "partial",
              "needs-review"
            ]
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WorkQueueItem"
            }
          },
          "nextCursor": {
            "nullable": true,
            "type": "string"
          },
          "scanned": {
            "type": "number"
          }
        },
        "required": [
          "view",
          "items",
          "nextCursor",
          "scanned"
        ],
        "additionalProperties": false,
        "description": "One page of unfinished work. The cursor is where the *scan* stopped rather than where the matches stopped — a request looks at a bounded number of keys and says how many in `scanned` — so a view that matches almost nothing still terminates instead of paging for ever. Keep following `nextCursor` until it is null; a page with no items and a cursor is a normal answer, not the end."
      }
    }
  }
}
