{
  "openapi": "3.0.3",
  "info": {
    "title": "relayted API",
    "version": "1",
    "description": "One API for all relayted services: cross-browser screenshots (shots), web page to Markdown/text/metadata/HTML/PDF (websitepro) and Nostr relay tests (labs). Prepaid balance in Payloads (10000 Payloads = 1 EUR), topped up with Bitcoin Lightning, pay per delivered item. Every result that is not delivered is refunded automatically. API keys are created by a human at https://api.relayted.de/account; no endpoint creates or returns keys."
  },
  "servers": [{ "url": "https://api.relayted.de" }],
  "security": [{ "apiKey": [] }],
  "tags": [{ "name": "services" }, { "name": "jobs" }, { "name": "wallet" }, { "name": "public" }],
  "paths": {
    "/v1": { "get": { "tags": ["public"], "summary": "Index: services and links", "security": [], "responses": { "200": { "description": "OK" } } } },
    "/v1/pricing": { "get": { "tags": ["public"], "summary": "Current price list in Payloads and EUR", "security": [], "responses": { "200": { "description": "OK" } } } },
    "/v1/services": { "get": { "tags": ["public"], "summary": "List of services", "security": [], "responses": { "200": { "description": "OK" } } } },
    "/v1/services/{service}": {
      "get": {
        "tags": ["public"], "summary": "Live catalogue of a service (browsers and versions, check ids, outputs)", "security": [],
        "parameters": [{ "$ref": "#/components/parameters/service" }],
        "responses": { "200": { "description": "OK" }, "404": { "$ref": "#/components/responses/Error" }, "503": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/websitepro/quote": { "post": { "tags": ["services"], "summary": "Price a websitepro request (free, nothing is fetched)", "requestBody": { "$ref": "#/components/requestBodies/Websitepro" }, "responses": { "200": { "$ref": "#/components/responses/Quote" }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" } } } },
    "/v1/websitepro/jobs": {
      "post": {
        "tags": ["services"], "summary": "Convert a web page (one result per output)",
        "parameters": [{ "$ref": "#/components/parameters/wait" }],
        "requestBody": { "$ref": "#/components/requestBodies/Websitepro" },
        "responses": { "201": { "$ref": "#/components/responses/Job" }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" }, "402": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "503": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/shots/quote": { "post": { "tags": ["services"], "summary": "Price a screenshot request and list every screenshot (free)", "requestBody": { "$ref": "#/components/requestBodies/Shots" }, "responses": { "200": { "$ref": "#/components/responses/Quote" }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" } } } },
    "/v1/shots/jobs": {
      "post": {
        "tags": ["services"], "summary": "Take screenshots (one result per browser version x viewport x scheme)",
        "parameters": [{ "$ref": "#/components/parameters/wait" }],
        "requestBody": { "$ref": "#/components/requestBodies/Shots" },
        "responses": { "201": { "$ref": "#/components/responses/Job" }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" }, "402": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "503": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/labs/quote": { "post": { "tags": ["services"], "summary": "Price a relay test (free, the relay is not contacted)", "requestBody": { "$ref": "#/components/requestBodies/Labs" }, "responses": { "200": { "$ref": "#/components/responses/Quote" }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" } } } },
    "/v1/labs/jobs": {
      "post": {
        "tags": ["services"], "summary": "Test a Nostr relay (one JSON report per module)",
        "parameters": [{ "$ref": "#/components/parameters/wait" }],
        "requestBody": { "$ref": "#/components/requestBodies/Labs" },
        "responses": { "201": { "$ref": "#/components/responses/Job" }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" }, "402": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "503": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/jobs": {
      "get": {
        "tags": ["jobs"], "summary": "Recent jobs of this key",
        "parameters": [{ "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 30 } }],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/Job" } } } } } } }, "401": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "tags": ["jobs"], "summary": "State of a job",
        "parameters": [{ "$ref": "#/components/parameters/id" }, { "$ref": "#/components/parameters/wait" }],
        "responses": { "200": { "$ref": "#/components/responses/Job" }, "401": { "$ref": "#/components/responses/Error" }, "404": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/jobs/{id}/results/{key}": {
      "get": {
        "tags": ["jobs"], "summary": "Download one result file",
        "parameters": [
          { "$ref": "#/components/parameters/id" },
          { "name": "key", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Result key from the job (URL-encoded)." },
          { "name": "thumb", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "shots only: return a small WebP thumbnail instead of the PNG." }
        ],
        "responses": { "200": { "description": "The file, with its real content type, as a download", "content": { "*/*": { "schema": { "type": "string", "format": "binary" } } } }, "401": { "$ref": "#/components/responses/Error" }, "404": { "$ref": "#/components/responses/Error" }, "409": { "$ref": "#/components/responses/Error" }, "410": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/jobs/{id}/zip": {
      "get": {
        "tags": ["jobs"], "summary": "shots only: all finished screenshots of a completed job as ZIP",
        "parameters": [{ "$ref": "#/components/parameters/id" }],
        "responses": { "200": { "description": "ZIP archive", "content": { "application/zip": { "schema": { "type": "string", "format": "binary" } } } }, "404": { "$ref": "#/components/responses/Error" }, "409": { "$ref": "#/components/responses/Error" }, "410": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/balance": {
      "get": { "tags": ["wallet"], "summary": "Balance of this key", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "balance": { "type": "integer", "description": "Payloads" }, "unit": { "type": "string" }, "eur": { "type": "number" } } } } } }, "401": { "$ref": "#/components/responses/Error" } } }
    },
    "/v1/transactions": {
      "get": {
        "tags": ["wallet"], "summary": "Ledger of this wallet, newest first",
        "parameters": [{ "name": "before", "in": "query", "schema": { "type": "integer" }, "description": "Return entries with an id below this one (paging)." }],
        "responses": { "200": { "description": "OK" }, "401": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/topups": {
      "post": {
        "tags": ["wallet"], "summary": "Create a Lightning invoice that tops up this key's balance",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "eur": { "type": "number", "description": "Amount in EUR (0.25 to 100.00)." } }, "required": ["eur"] }, "example": { "eur": 5 } } } },
        "responses": { "201": { "$ref": "#/components/responses/Topup" }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" }, "429": { "$ref": "#/components/responses/Error" }, "502": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/topups/{id}": {
      "get": { "tags": ["wallet"], "summary": "State of a top-up (poll until status is paid)", "parameters": [{ "$ref": "#/components/parameters/id" }], "responses": { "200": { "$ref": "#/components/responses/Topup" }, "401": { "$ref": "#/components/responses/Error" }, "404": { "$ref": "#/components/responses/Error" } } }
    }
  },
  "components": {
    "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "description": "API key, e.g. RAPI-XXXX-XXXX-XXXX-XXXX-XXXX. Created at https://api.relayted.de/account." } },
    "parameters": {
      "id": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
      "service": { "name": "service", "in": "path", "required": true, "schema": { "type": "string", "enum": ["websitepro", "shots", "labs"] } },
      "wait": { "name": "wait", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 25 }, "description": "Hold the request open until the job is finished, at most this many seconds (long polling)." }
    },
    "requestBodies": {
      "Websitepro": {
        "required": true,
        "content": { "application/json": { "schema": { "type": "object", "required": ["url", "outputs"], "properties": {
          "url": { "type": "string", "description": "Public http(s) URL." },
          "outputs": { "type": "array", "minItems": 1, "items": { "type": "string", "enum": ["metadata", "markdown", "text", "html", "pdf"] } }
        } }, "example": { "url": "https://example.com/article", "outputs": ["markdown", "metadata"] } } }
      },
      "Shots": {
        "required": true,
        "content": { "application/json": { "schema": { "type": "object", "required": ["url", "browsers"], "properties": {
          "url": { "type": "string", "description": "Public http(s) URL." },
          "browsers": { "type": "array", "minItems": 1, "description": "Browser ids from GET /v1/services/shots. A string means the newest version.", "items": { "oneOf": [{ "type": "string" }, { "type": "object", "required": ["id", "versions"], "properties": { "id": { "type": "string" }, "versions": { "type": "array", "items": { "type": "string" } } } }] } },
          "viewports": { "type": "array", "maxItems": 3, "description": "Desktop browsers only. \"WIDTHxHEIGHT\", a preset id (fhd, qhd, laptop, hd, tablet, phone) or {w,h}. Default 1920x1080.", "items": { "oneOf": [{ "type": "string" }, { "type": "object", "required": ["w", "h"], "properties": { "w": { "type": "integer", "minimum": 320, "maximum": 3840 }, "h": { "type": "integer", "minimum": 320, "maximum": 2400 } } }] } },
          "schemes": { "type": "array", "items": { "type": "string", "enum": ["light", "dark"] }, "default": ["light"] },
          "options": { "type": "object", "properties": { "fullPage": { "type": "boolean" }, "retina": { "type": "boolean" }, "hideCookieBanner": { "type": "boolean" }, "delay": { "type": "integer", "enum": [0, 2, 5] } } }
        } }, "example": { "url": "https://example.com", "browsers": ["chrome", "firefox"], "viewports": ["1920x1080"], "schemes": ["light", "dark"], "options": { "fullPage": true } } } }
      },
      "Labs": {
        "required": true,
        "content": { "application/json": { "schema": { "type": "object", "required": ["relay"], "properties": {
          "relay": { "type": "string", "description": "wss://host[:port][/path], ws://..., or a bare host (wss)." },
          "modules": { "type": "array", "items": { "type": "string", "enum": ["quick", "conformance"] } },
          "checks": { "type": "array", "items": { "type": "string" }, "description": "Single check ids (field selectable of GET /v1/services/labs). One result with key custom." },
          "webhook": { "type": "string", "description": "Optional https URL that receives the finished reports." }
        } }, "example": { "relay": "wss://relay.example.com", "modules": ["quick"] } } }
      }
    },
    "responses": {
      "Job": { "description": "The job and the new balance", "content": { "application/json": { "schema": { "type": "object", "properties": { "job": { "$ref": "#/components/schemas/Job" }, "balance": { "type": "integer", "description": "Payloads" } } } } } },
      "Quote": { "description": "Price of the request; nothing was charged or created", "content": { "application/json": { "schema": { "type": "object", "properties": {
        "service": { "type": "string" }, "target": { "type": "string" }, "cost": { "type": "integer", "description": "Payloads" }, "eur": { "type": "number" },
        "balance": { "type": "integer" }, "sufficient": { "type": "boolean" },
        "items": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string" }, "price": { "type": "integer" } }, "additionalProperties": true } }
      } } } } },
      "Topup": { "description": "Top-up", "content": { "application/json": { "schema": { "type": "object", "properties": { "topup": { "type": "object", "properties": {
        "id": { "type": "string" }, "status": { "type": "string", "enum": ["pending", "paid", "expired", "failed"] }, "eur": { "type": "number" }, "eurCents": { "type": "integer" },
        "payloads": { "type": "integer", "description": "Payloads credited once paid, bonus included" }, "bonusPayloads": { "type": "integer" },
        "bolt11": { "type": "string", "nullable": true, "description": "Lightning invoice while pending" }, "sats": { "type": "integer", "nullable": true }, "expiresAt": { "type": "integer", "description": "Unix ms" }
      } }, "balance": { "type": "integer" } } } } } },
      "Error": { "description": "Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Job": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "service": { "type": "string", "enum": ["websitepro", "shots", "labs"] },
          "status": { "type": "string", "enum": ["queued", "processing", "completed", "failed"], "description": "completed = at least one result delivered; failed = nothing delivered, nothing charged." },
          "target": { "type": "string", "description": "Host name of the target." },
          "cost": { "type": "integer", "description": "Payloads charged at creation." },
          "refunded": { "type": "integer", "description": "Payloads given back for undelivered results." },
          "charged": { "type": "integer", "description": "cost - refunded." },
          "error": { "type": "string" },
          "createdAt": { "type": "integer", "description": "Unix ms" },
          "finishedAt": { "type": "integer", "description": "Unix ms" },
          "expiresAt": { "type": "integer", "description": "Unix ms. Results are deleted after this." },
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/Result" } }
        },
        "additionalProperties": true
      },
      "Result": {
        "type": "object",
        "properties": {
          "key": { "type": "string", "description": "Output name, module name or screenshot file name." },
          "status": { "type": "string", "enum": ["pending", "done", "failed"] },
          "price": { "type": "integer", "description": "Payloads" },
          "refunded": { "type": "boolean" },
          "error": { "type": "string" },
          "url": { "type": "string", "description": "Download address (needs the Authorization header). Present when done and not expired." },
          "contentType": { "type": "string" },
          "bytes": { "type": "integer" },
          "filename": { "type": "string" }
        },
        "additionalProperties": true
      },
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "object", "required": ["code"], "properties": { "code": { "type": "string", "description": "Stable machine-readable code. See https://api.relayted.de/docs#errors." }, "message": { "type": "string" } }, "additionalProperties": true } }
      }
    }
  }
}
