{
  "openapi": "3.1.0",
  "info": {
    "title": "Bojakt API",
    "version": "1.0.0",
    "summary": "Distribution for listings published on bojakt. First-party supply only.",
    "description": "Distribution for listings published on bojakt. Swedish rental homes that\nlandlords listed with us directly, over HTTPS, with change notifications.\n\n**First-party only.** Every listing this API returns was published on bojakt by\nits landlord and passed our manual review. bojakt also aggregates third-party\ninventory for seekers on the website — **none of that is exposed here, and none\nof it ever will be.** If you need those sources, licence them from the sources.\n\n**Read-only.** There is no endpoint that creates or modifies a listing, so a\nleaked key can never change our data, and nothing here touches the database\ndirectly.\n\n## Authentication\n\nSend the key we issued you as a bearer token. `X-API-Key` is accepted too,\nbecause half of every HTTP client reaches for it first.\n\n```\nAuthorization: Bearer bk_live_…\n```\n\nUsing a key accepts the [API terms of use](/villkor). One clause matters more\nthan the rest: **removals must propagate** — when a landlord takes a listing\ndown, it has to stop appearing on your surfaces within 24 hours. Re-check what\nyou display daily, and treat a `404` as withdrawn.\n\nKeys are shown **once**, at creation, in the [dashboard](/dashboard) — we store\nonly a SHA-256 hash, so a lost key is replaced, never recovered. Paste one into\nthe auth box in the sidebar and every example on this page becomes runnable\nagainst the live API.\n\nKeys carry **scopes**. Calling an endpoint without its scope returns\n`403 insufficient_scope`, and the body names both what was required and what\nwas granted, so the fix doesn't need a support round trip.\n\n| Scope | Grants |\n|---|---|\n| `listings:read` | `/v1/listings`, `/v1/listings/{id}`, `/v1/cities` |\n| `webhooks:write` | Managing webhook subscriptions |\n\n## Request ids\n\nEvery response carries `X-Request-Id`, and error bodies repeat it as\n`error.request_id`. **Quote it when you contact us** — it is how a specific\ncall gets found rather than guessed at from a timestamp.\n\nSend your own `X-Request-Id` and we will use it instead, so the same id appears\nin your logs and ours. It must be 8–64 characters of `[A-Za-z0-9_-]`; anything\nelse is ignored and we generate one.\n\n## Rate limits and quotas\n\nTwo separate limits, because they protect different things.\n\n- **`rate_limit_per_min`** (120 by default) bounds bursts against the\n  database. Exceeding it returns `429 rate_limited` with `Retry-After`.\n- **`monthly_quota`** is the commercial limit, counted durably in\n  Postgres so it survives our own deploys. Exceeding it returns\n  `429 quota_exceeded` with `Retry-After` set to the seconds until 00:00 UTC\n  on the 1st — so backing off by the header is correct for both 429s, even though\n  one means seconds and the other can mean days.\n\nRejected calls are metered too, so a 401 storm shows up on your dashboard rather\nthan disappearing silently.\n\n## Pagination\n\nCursor-based, never page numbers. The catalogue is re-upserted hourly, and an\noffset would **silently skip rows** when the set shifts under a paging client —\na sync quietly missing listings, with no error anywhere. Keep passing\n`pagination.nextCursor` until `hasMore` is `false`.\n\n```js\nlet cursor = null;\nconst all = [];\n\ndo {\n  const url = new URL(\"https://api.bojakt.se/v1/listings\");\n  url.searchParams.set(\"limit\", \"200\");\n  if (cursor) url.searchParams.set(\"cursor\", String(cursor));\n\n  const res = await fetch(url, {\n    headers: { Authorization: `Bearer ${process.env.BOJAKT_KEY}` },\n  });\n  const { data, pagination } = await res.json();\n\n  all.push(...data);\n  cursor = pagination.nextCursor;\n} while (cursor);\n```\n\nFor a recurring sync, prefer `since=` the timestamp of your last successful\nrun over re-walking the whole catalogue.\n\n## Webhooks\n\n> **Ask us before relying on webhooks.** Delivery is currently operated manually\n> while we onboard our first partners, so it is not yet suitable for a\n> latency-sensitive integration. Polling `/v1/listings` with `since=` is the\n> supported path today. We will tell you when that changes.\n\nRather than polling for new listings, subscribe. Every delivery carries:\n\n```\nX-Bojakt-Signature: t=<unix>,v1=<hmac-sha256>\n```\n\nan HMAC-SHA256 over `${t}.${rawBody}`. The timestamp is **inside** the signed\nstring — sign only the body and a captured delivery replays forever. Reject\nanything older than 5 minutes, compare digests in constant time, and sign the\n*raw* body before any JSON parsing.\n\n```js\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nexport function verify(rawBody, header, secret) {\n  const { t, v1 } = Object.fromEntries(\n    header.split(\",\").map((kv) => kv.split(\"=\")),\n  );\n  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;\n\n  const expected = createHmac(\"sha256\", secret)\n    .update(`${t}.${rawBody}`)\n    .digest(\"hex\");\n\n  const a = Buffer.from(expected, \"hex\");\n  const b = Buffer.from(v1, \"hex\");\n  return a.length === b.length && timingSafeEqual(a, b);\n}\n```\n\n**Delivery is at-least-once.** A receiver that times out is retried even if it\ndid process the event, so handlers must be idempotent on `event_id`. Up to\n8 attempts: the first is immediate, then retries at +1, +2, +4, +8,\n+16, +32 and +64 minutes. After that the delivery is parked as failed. Any `2xx` acknowledges.\n\nPayloads carry a `listing_id`, not the listing body, so a delayed retry never\nhands you a stale price — fetch `/v1/listings/{id}` for current data.\n\n## What the data is, and isn't\n\n- Every listing is `source: \"bojakt\"` — published by its landlord on bojakt and\n  **manually reviewed before publication.** Drafts, paused and rejected listings\n  are never exposed. That review is the reason this feed is worth consuming: the\n  Swedish sublet market is where most rental fraud happens, and someone has read\n  every one of these.\n- Because it is first-party, volume is smaller than a scraped aggregate and grows\n  with our landlord side. Check `/v1/cities` before assuming coverage.\n- `municipality` is populated for a subset of sources only. Treat it as\n  optional and filter on `city` when you need coverage.\n- Listings disappear when they expire, so a cached id starts returning\n  `404`. That is normal churn, not an error to alert on.\n- **No personal data is exposed.** Seeker emails, phone numbers and watch\n  criteria are not part of this API and never will be.",
    "contact": {
      "name": "Bojakt",
      "email": "bojakt.se@gmail.com"
    },
    "termsOfService": "https://api.bojakt.se/villkor",
    "license": {
      "name": "Bojakt API terms of use",
      "url": "https://api.bojakt.se/villkor"
    }
  },
  "servers": [
    {
      "url": "https://api.bojakt.se",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Listings",
      "description": "The catalogue. Cursor-paginated, image-filtered, newest first."
    },
    {
      "name": "Cities",
      "description": "Inventory counts per city — the cheapest call to make first, because it answers where there is enough stock to be worth integrating on."
    },
    {
      "name": "Webhooks",
      "description": "Subscribe to listing events instead of polling. Deliveries are HMAC-signed and at-least-once."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/listings": {
      "get": {
        "operationId": "list-listings",
        "summary": "The catalogue",
        "description": "Published bojakt listings, newest first — first-party supply only, never aggregated third-party inventory. Only listings with at least one image are returned. Cursor-paginated: pass the previous response's nextCursor to continue.\n\n- Unknown query parameters are rejected with 400 rather than ignored — a typo that silently returned the whole country would look like a working integration.\n\n- For a recurring sync use since= rather than re-walking the catalogue.",
        "tags": [
          "Listings"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "x-scalar-scopes": [
          "listings:read"
        ],
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "Exact city name, case-insensitive.",
            "schema": {
              "type": "string"
            },
            "example": "Uppsala"
          },
          {
            "name": "municipality",
            "in": "query",
            "required": false,
            "description": "Kommun. Populated for a subset of sources — filter on city when you need coverage.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "county",
            "in": "query",
            "required": false,
            "description": "Län, e.g. “Stockholms län”.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "minRent",
            "in": "query",
            "required": false,
            "description": "SEK/month, inclusive.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "maxRent",
            "in": "query",
            "required": false,
            "description": "SEK/month, inclusive.",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": "12000"
          },
          {
            "name": "minRooms",
            "in": "query",
            "required": false,
            "description": "Room count, inclusive.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "maxRooms",
            "in": "query",
            "required": false,
            "description": "Room count, inclusive.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "firsthand",
            "in": "query",
            "required": false,
            "description": "true for first-hand contracts, false for sublets. Omit for both.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "example": "true"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "ISO 8601. Listings first seen after this instant — the incremental-sync parameter.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1–200, default 50. Higher values are clamped and the response echoes what was applied.",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": "3"
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "pagination.nextCursor from the previous page.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Listing"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": 3465199,
                      "source": "bojakt",
                      "url": "https://bojakt.se/bostad/3465199",
                      "rent": 12000,
                      "currency": "SEK",
                      "rooms": 2,
                      "squareMeters": 58,
                      "firsthand": true,
                      "city": "Uppsala",
                      "municipality": null,
                      "county": "Uppsala län",
                      "street": "Kungsgatan",
                      "latitude": 59.858,
                      "longitude": 17.645,
                      "images": [
                        "https://…"
                      ],
                      "publishedAt": "2026-07-29T12:00:00.000Z",
                      "firstSeenAt": "2026-07-30T06:01:00.000Z"
                    }
                  ],
                  "pagination": {
                    "limit": 3,
                    "maxLimit": 200,
                    "nextCursor": 3465196,
                    "hasMore": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/listings/{id}": {
      "get": {
        "operationId": "get-listing",
        "summary": "One listing",
        "description": "Fetch a single active listing. 404 covers both “never existed” and “no longer active” — an expired listing is gone as far as the API is concerned.\n\n- Cached ids start returning 404 when listings expire. That is normal churn, not an error to alert on.",
        "tags": [
          "Listings"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "x-scalar-scopes": [
          "listings:read"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Listing id.",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "example": "3465199"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Listing"
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": 3465199,
                    "source": "bojakt",
                    "rent": 12000,
                    "city": "Uppsala",
                    "firsthand": true,
                    "images": [
                      "https://…"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/cities": {
      "get": {
        "operationId": "cities",
        "summary": "Inventory per city",
        "description": "Every city we have published listings in, with counts. The cheapest call to make first — it answers where there is stock, without paging the catalogue to find out.",
        "tags": [
          "Cities"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "x-scalar-scopes": [
          "listings:read"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/City"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "city": "Eskilstuna",
                      "total": 1123,
                      "firsthand": 1054,
                      "minRent": 3200
                    },
                    {
                      "city": "Stockholm",
                      "total": 877,
                      "firsthand": 294,
                      "minRent": 4500
                    }
                  ],
                  "meta": {
                    "count": 87
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "operationId": "list-webhooks",
        "summary": "Your subscriptions",
        "description": "Lists this partner's webhooks. Secrets are never returned — they are shown once at creation, for the same reason API keys are.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "x-scalar-scopes": [
          "webhooks:write"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "data": [
                    {
                      "id": "9f1c…",
                      "url": "https://yours.example/hooks/bojakt",
                      "events": [
                        "listing.published"
                      ],
                      "city": "Uppsala",
                      "active": true,
                      "createdAt": "2026-07-30T08:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "post": {
        "operationId": "create-webhook",
        "summary": "Subscribe to new listings",
        "description": "Register an https endpoint to receive listing.published instead of polling. The response contains a whsec_… secret, shown once — store it, it signs every delivery.\n\n- URLs must be https and publicly routable. Private and loopback addresses are rejected.\n\n- city is an optional server-side filter, so a partner watching one city never receives (or is billed for) the whole country.",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "x-scalar-scopes": [
          "webhooks:write"
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookCreate"
              },
              "example": {
                "url": "https://yours.example/hooks/bojakt",
                "events": [
                  "listing.published"
                ],
                "city": "Uppsala"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "data": {
                    "id": "9f1c…",
                    "url": "https://yours.example/hooks/bojakt",
                    "events": [
                      "listing.published"
                    ],
                    "city": "Uppsala",
                    "active": true
                  },
                  "secret": "whsec_…",
                  "note": "Store this secret now — it is not retrievable later."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  },
  "webhooks": {
    "listing.published": {
      "post": {
        "summary": "A listing matching your subscription appeared",
        "description": "Sent to your registered URL. Verify `X-Bojakt-Signature` before trusting the body, and dedupe on `event_id` — delivery is at-least-once.",
        "tags": [
          "Webhooks"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event_id": "evt_1041",
                "type": "listing.published",
                "created": "2026-07-30T09:14:02.000Z",
                "data": {
                  "listing_id": 3465199
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx acknowledges. Anything else — or a timeout — is retried."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer bk_live_…`. Issued in the dashboard, shown once."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Alternative to the bearer header. Same key."
      }
    },
    "schemas": {
      "Listing": {
        "type": "object",
        "required": [
          "id",
          "source",
          "url",
          "rent",
          "currency",
          "firsthand",
          "images"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 3465199
          },
          "source": {
            "type": "string",
            "const": "bojakt",
            "description": "Always `bojakt` — this API serves first-party listings only. Kept in the payload so the field stays stable if that ever changes.",
            "example": "bojakt"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical link to the listing at its source."
          },
          "rent": {
            "type": "integer",
            "description": "SEK per month.",
            "example": 12000
          },
          "currency": {
            "type": "string",
            "example": "SEK"
          },
          "rooms": {
            "type": [
              "number",
              "null"
            ],
            "description": "Room count. Halves are real (1.5 rok).",
            "example": 2
          },
          "squareMeters": {
            "type": [
              "integer",
              "null"
            ],
            "example": 58
          },
          "firsthand": {
            "type": "boolean",
            "description": "true = first-hand contract, false = sublet."
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "example": "Uppsala"
          },
          "municipality": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kommun. Populated for a subset of sources only."
          },
          "county": {
            "type": [
              "string",
              "null"
            ],
            "example": "Uppsala län"
          },
          "street": {
            "type": [
              "string",
              "null"
            ],
            "example": "Kungsgatan"
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ],
            "example": 59.858
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ],
            "example": 17.645
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "At least one — listings without images are not returned."
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "firstSeenAt": {
            "type": "string",
            "format": "date-time",
            "description": "When bojakt first saw it. The field `since` filters on."
          }
        }
      },
      "City": {
        "type": "object",
        "properties": {
          "city": {
            "type": "string",
            "example": "Eskilstuna"
          },
          "total": {
            "type": "integer",
            "example": 1123
          },
          "firsthand": {
            "type": "integer",
            "example": 1054
          },
          "minRent": {
            "type": [
              "integer",
              "null"
            ],
            "example": 3200
          }
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "example": 50
          },
          "maxLimit": {
            "type": "integer",
            "example": 200
          },
          "nextCursor": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null on the last."
          },
          "hasMore": {
            "type": "boolean"
          }
        }
      },
      "WebhookCreate": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Must be https and publicly routable. Loopback and private ranges are rejected."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "listing.published"
              ]
            },
            "default": [
              "listing.published"
            ]
          },
          "city": {
            "type": "string",
            "description": "Optional server-side filter, so watching one city never delivers the whole country."
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Idempotency key. Dedupe on this."
          },
          "type": {
            "type": "string",
            "enum": [
              "listing.published"
            ]
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "listing_id": {
                "type": "integer"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "detail"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code. Branch on this."
              },
              "detail": {
                "type": "string",
                "description": "Human-readable explanation. Show this."
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request. `code` is one of `unknown_parameter`, `invalid_parameter`, `invalid_range`, `invalid_json`, `invalid_url`, `insecure_url`, `private_url`, `unknown_event`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unknown_parameter",
                "detail": "…"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No usable credential. `code` is one of `missing_credentials`, `malformed_key`, `invalid_key`, `key_revoked`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "missing_credentials",
                "detail": "…"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated but not allowed. `insufficient_scope` names the required and granted scopes; `partner_suspended` means the account is disabled.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "insufficient_scope",
                "detail": "…"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No such active resource.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "detail": "…"
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "`rate_limited` is worth retrying after `Retry-After`. `quota_exceeded` is not, until the month rolls over.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "detail": "…"
              }
            }
          }
        }
      },
      "Unavailable": {
        "description": "Transient failure on our side. Retry with backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unavailable",
                "detail": "…"
              }
            }
          }
        }
      }
    }
  }
}