{
  "openapi": "3.1.0",
  "info": {
    "title": "zpe-screens",
    "version": "1.0.1",
    "description": "A commit-true library of our own product surfaces: one capture in, searchable text and image URLs out. Two operations are published — the search and the ingest. Every capture carries the commit it came from, and an ingest with no commit is refused. Every operation lives under the versioned base /_/api/v1 and the document below is served at /_/api/v1/openapi.json."
  },
  "servers": [
    {
      "url": "/screens",
      "description": "screens on zpe.vc"
    }
  ],
  "paths": {
    "/_/api/v1/screens": {
      "get": {
        "operationId": "getScreens",
        "summary": "Search captures by the text on the page, host, path and time",
        "tags": [
          "screens"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Words to match against the rendered text. Every word must appear, and each one matches as a prefix. FTS5 operators cannot survive the parser, so a search is words and not a query language.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "host",
            "in": "query",
            "description": "A host as the app registry spells it, e.g. `zpe.vc`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "query",
            "description": "The page's path, starting with `/`. Exact match, so two captures of the same page are one timeline.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Only captures at or after this instant: epoch milliseconds or an ISO date.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many rows to return, 1 to 200. Defaults to 50, newest first.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The captures the question names, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "query",
                    "count",
                    "rows"
                  ],
                  "properties": {
                    "query": {
                      "type": "object",
                      "description": "The filters the request was read as, after defaults.",
                      "properties": {
                        "q": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "host": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "path": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "since": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "limit": {
                          "type": "integer"
                        }
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "How many rows this answer carries."
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "host",
                          "path",
                          "viewport",
                          "commit_id",
                          "captured_at",
                          "sha256",
                          "image_url"
                        ],
                        "properties": {
                          "id": {
                            "type": "integer",
                            "description": "The capture's id, and the address of its image."
                          },
                          "host": {
                            "type": "string",
                            "description": "The host the page was served from, as the app registry spells it."
                          },
                          "path": {
                            "type": "string",
                            "description": "The page's path on that host, starting with `/`."
                          },
                          "final_url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Where the browser ended up, when it was redirected."
                          },
                          "viewport": {
                            "type": "string",
                            "description": "The capture's viewport, `<width>x<height>`."
                          },
                          "commit_id": {
                            "type": "string",
                            "description": "The full commit the page was deployed from — the row's reason to exist."
                          },
                          "deploy_id": {
                            "type": "string",
                            "description": "The deployment's id, when the sweep knew it."
                          },
                          "captured_at": {
                            "type": "integer",
                            "description": "Epoch milliseconds."
                          },
                          "captured_at_iso": {
                            "type": "string",
                            "format": "date-time",
                            "description": "The same instant, readable."
                          },
                          "sha256": {
                            "type": "string",
                            "description": "The sha256 of the image bytes, which is also the object key."
                          },
                          "image_bytes": {
                            "type": "integer",
                            "description": "The image's size in bytes."
                          },
                          "image_url": {
                            "type": "string",
                            "description": "Where to fetch the bytes. A key or a door session, like the search."
                          },
                          "text_excerpt": {
                            "type": "string",
                            "description": "The first 240 characters of the rendered text."
                          },
                          "text_length": {
                            "type": "integer",
                            "description": "The full rendered text's length in characters; the whole text is searchable."
                          },
                          "previous": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "The capture this one replaced on the same host and path, or null for the first capture of a page.",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "sha256": {
                                "type": "string"
                              },
                              "commit_id": {
                                "type": "string"
                              },
                              "captured_at": {
                                "type": "integer"
                              },
                              "image_url": {
                                "type": "string"
                              }
                            }
                          },
                          "changed_since_previous": {
                            "type": [
                              "boolean",
                              "null"
                            ],
                            "description": "Whether the bytes differ from the previous capture. Null when there is no previous."
                          },
                          "match_snippet": {
                            "type": "string",
                            "description": "The matching text around the query, present only on a searched row."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The query string could not be read: `q` held no words, `host` or `path` was malformed, `since` was not a date, or `limit` was outside 1 to 200",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key and no door session was sent, or the key sent does not open this account",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key is not granted this app for reads, or the door named no account to scope the read to",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This key has spent its request ceiling; the refusal says when to retry",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door did not answer, so the credential could not be checked",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "post": {
        "operationId": "postScreens",
        "summary": "Store one capture, bound to the commit it came from",
        "tags": [
          "screens"
        ],
        "requestBody": {
          "required": true,
          "description": "The image, its rendered text, and the page and commit it belongs to. Every field is validated and refused by name.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image",
                  "host",
                  "path",
                  "commit",
                  "viewport"
                ],
                "properties": {
                  "image": {
                    "type": "string",
                    "description": "The image as a data URL (`data:image/webp;base64,…`) or bare base64. The R2 object key is the sha256 of these bytes, so a second identical capture costs no second object."
                  },
                  "content_type": {
                    "type": "string",
                    "description": "The image's type. Defaults to the data URL's, or `image/webp`."
                  },
                  "host": {
                    "type": "string",
                    "pattern": "^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$",
                    "description": "The host the page was served from, lower case, no scheme and no path."
                  },
                  "path": {
                    "type": "string",
                    "description": "The page's path on that host. Must start with `/`; the first capture of a path compares against nothing."
                  },
                  "commit": {
                    "type": "string",
                    "description": "The full commit the page was deployed from. REQUIRED: a capture with no commit cannot answer \"what did this page say before this change\", and the route refuses it by name."
                  },
                  "viewport": {
                    "type": "string",
                    "pattern": "^[1-9]\\d{0,4}x[1-9]\\d{0,4}$",
                    "description": "The capture's viewport, `<width>x<height>`, e.g. `1440x900`."
                  },
                  "text": {
                    "type": "string",
                    "description": "The page's rendered text. The whole of it is indexed for search; the row carries an excerpt."
                  },
                  "deploy_id": {
                    "type": "string",
                    "description": "The deployment's id, when the sweep knows it."
                  },
                  "final_url": {
                    "type": "string",
                    "description": "Where the browser ended up. The sweep refuses a `/build` landing rather than storing it."
                  },
                  "captured_at": {
                    "type": [
                      "integer",
                      "string"
                    ],
                    "description": "When the capture was taken: epoch milliseconds or an ISO date. Defaults to now."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The library already held this capture, and answers the row it holds",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "sha256",
                    "r2_key",
                    "image_bytes",
                    "captured_at",
                    "object_written",
                    "row_created"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "The row's id. A repeated capture returns the id the library already held."
                    },
                    "sha256": {
                      "type": "string",
                      "description": "The sha256 of the image bytes."
                    },
                    "r2_key": {
                      "type": "string",
                      "description": "The object key the bytes live under. Equal captures share it."
                    },
                    "image_bytes": {
                      "type": "integer"
                    },
                    "captured_at": {
                      "type": "integer"
                    },
                    "object_written": {
                      "type": "boolean",
                      "description": "False when the bucket already held these bytes."
                    },
                    "row_created": {
                      "type": "boolean",
                      "description": "False when this host, path, commit and hash were already stored."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "A new capture was stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "sha256",
                    "r2_key",
                    "image_bytes",
                    "captured_at",
                    "object_written",
                    "row_created"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "The row's id. A repeated capture returns the id the library already held."
                    },
                    "sha256": {
                      "type": "string",
                      "description": "The sha256 of the image bytes."
                    },
                    "r2_key": {
                      "type": "string",
                      "description": "The object key the bytes live under. Equal captures share it."
                    },
                    "image_bytes": {
                      "type": "integer"
                    },
                    "captured_at": {
                      "type": "integer"
                    },
                    "object_written": {
                      "type": "boolean",
                      "description": "False when the bucket already held these bytes."
                    },
                    "row_created": {
                      "type": "boolean",
                      "description": "False when this host, path, commit and hash were already stored."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request could not be read",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing or does not open this account",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "This account may not do that",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key minted on the account's settings page at zpe.vc/home/settings. Send it as `Authorization: Bearer <key>`."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem+json: `type`, `title`, `status`, `code` and `instance`, plus `detail` when a person should read a sentence.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "instance"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "bad-request",
              "unauthenticated",
              "malformed",
              "unknown",
              "mismatch",
              "revoked",
              "expired",
              "another-account",
              "scope-refused",
              "grant-refused",
              "forbidden",
              "not-found",
              "method-not-allowed",
              "conflict",
              "gone",
              "payload-too-large",
              "unsupported-media-type",
              "unprocessable",
              "rate-limited",
              "payment-required",
              "not-open",
              "internal",
              "unavailable"
            ]
          },
          "instance": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          }
        }
      }
    }
  },
  "x-surface": {
    "app": "screens",
    "base": "/_/api/v1",
    "document": "/_/api/v1/openapi.json",
    "generated_by": "zpe-kit machineDocument"
  },
  "x-zpe-routes": 6,
  "x-zpe-documented": 2,
  "x-zpe-internal": [
    "GET /_/api/v1",
    "GET /_/api/v1/health",
    "GET /_/api/v1/openapi.json",
    "GET /_/api/v1/screens/:id/image"
  ]
}
