{
  "openapi": "3.1.0",
  "info": {
    "title": "Trustburn Public API",
    "version": "1.0.0",
    "summary": "Read reviews and manage business listings on Trustburn.",
    "description": "The Trustburn Public API (v1) lets an integration do two things:\n\n1. **Manage its own listings.** Create and edit the business profiles the\n   account owns, upload a logo and photos, and read the reviews those\n   listings have received.\n2. **Read public review data.** Look up any active Trustburn company by id\n   or domain, read its reviews, and read its rating statistics.\n\nEverything is JSON over HTTPS. Authentication is a Bearer API key; browser\nsession cookies are never accepted on `/api/v1/*`.\n\n**What the API deliberately does not do:** it cannot submit reviews. There\nis no write path for review content of any kind, for anyone, and there will\nnot be one.\n\n### Quotas at a glance\n\n| Limit | Value | Enforced by |\n|---|---|---|\n| Requests per minute, per key | 60 | `X-RateLimit-Limit` / `X-RateLimit-Remaining`, `429 rate_limited` |\n| Requests per minute, per IP | 120 (burst 60) | edge, `429 rate_limited` with `Retry-After: 60` |\n| Listings created per day, per key | 200 | `429 daily_limit_reached` |\n| Company reads per day, per key | 1000 | `X-Daily-Reads-*`, `429 daily_read_limit_reached` |\n\nDays are UTC days. Only successful calls count against the daily quotas: a\n`dry_run` create, a validation failure and a 404 are all free.\n\n### Attribution\n\nEvery company card and every review page carries an `attribution` object:\n\n```json\n\"attribution\": { \"text\": \"Reviews via Trustburn\", \"url\": \"https://trustburn.com/reviews/semalt\" }\n```\n\nWhen you display Trustburn review data you must show that text and link it\nto that URL. See `https://trustburn.com/developers#attribution`.\n",
    "termsOfService": "https://trustburn.com/terms-of-service",
    "contact": {
      "name": "Trustburn support",
      "url": "https://trustburn.com/support",
      "email": "sales@trustburn.com"
    }
  },
  "servers": [
    {
      "url": "https://trustburn.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Who the key belongs to, and what it is allowed to do."
    },
    {
      "name": "Reference data",
      "description": "Closed vocabularies - categories and geography - used by listing fields."
    },
    {
      "name": "Listings",
      "description": "The business profiles this account owns. Requires `listings:read` / `listings:write`."
    },
    {
      "name": "Companies",
      "description": "Public read access to any active Trustburn company. Requires `companies:read`."
    },
    {
      "name": "Reviews",
      "description": "Public read access to reviews. Requires `reviews:read`."
    },
    {
      "name": "Review transfers",
      "description": "Move reviews a business already collected on another platform onto its\nTrustburn profile, at its owner's request. Requires `listings:read` /\n`listings:write`.\n"
    },
    {
      "name": "Agency",
      "description": "Agency listing-years: buy a package, then spend one unit per listing to\ngive it a dofollow link and the Verified badge for a year.\n"
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Key and account info",
        "description": "Confirms the key is live and reports the account it belongs to, the\nkey's scopes, and every quota with today's usage already counted.\nThe cheapest possible health check - it makes no Elasticsearch call.\n",
        "operationId": "getMe",
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-Daily-Reads-Limit": {
                "$ref": "#/components/headers/XDailyReadsLimit"
              },
              "X-Daily-Reads-Remaining": {
                "$ref": "#/components/headers/XDailyReadsRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "account": {
                    "id": 4210,
                    "name": "Acme Agency",
                    "email_masked": "ja***@acme.com",
                    "companies": 11
                  },
                  "key": {
                    "id": 11,
                    "name": "Agency integration",
                    "prefix": "tb_live_a1b2c3d4",
                    "scopes": [
                      "listings:read",
                      "listings:write",
                      "companies:read",
                      "reviews:read"
                    ],
                    "created_at": "2026-09-08T14:27:07Z",
                    "last_used_at": null
                  },
                  "limits": {
                    "rate_per_min": 60,
                    "listings_per_day": 200,
                    "listings_created_today": 0,
                    "reads_per_day": 1000,
                    "reads_today": 0
                  },
                  "api_version": "v1"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/categories": {
      "get": {
        "tags": [
          "Reference data"
        ],
        "summary": "List categories",
        "description": "The closed category taxonomy. `slug` is the stable identifier - it is\nwhat `POST /listings` and `PATCH /listings/{id}` accept in\n`categories`, and it is what appears in public URLs\n(`https://trustburn.com/categories/{slug}`). Sorted by slug.\n",
        "operationId": "listCategories",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Category"
                  }
                },
                "example": [
                  {
                    "slug": "accounting",
                    "name": "Accounting"
                  },
                  {
                    "slug": "airlines-aviation",
                    "name": "Airlines/aviation"
                  },
                  {
                    "slug": "seo-services",
                    "name": "SEO Services"
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/geo/countries": {
      "get": {
        "tags": [
          "Reference data"
        ],
        "summary": "List countries",
        "description": "ISO 3166-1 alpha-2 codes accepted by the `country` field of a listing.",
        "operationId": "listCountries",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Country"
                  }
                },
                "example": [
                  {
                    "code": "AF",
                    "name": "Afghanistan"
                  },
                  {
                    "code": "US",
                    "name": "United States"
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/geo/states": {
      "get": {
        "tags": [
          "Reference data"
        ],
        "summary": "List states of a country",
        "operationId": "listStates",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": true,
            "description": "ISO 3166-1 alpha-2 code.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z]{2}$",
              "example": "US"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GeoRef"
                  }
                },
                "example": [
                  {
                    "id": 1399,
                    "name": "Delaware"
                  },
                  {
                    "id": 1400,
                    "name": "Alaska"
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/geo/cities": {
      "get": {
        "tags": [
          "Reference data"
        ],
        "summary": "List cities of a state",
        "description": "Only cities with a population over 10,000 - the same list the business\ncabinet offers. The threshold is deliberate: a listing placed in a city\noutside that list could not afterwards be edited in the cabinet without\nmoving it.\n\n`q` filters by name prefix and caps the result at 50 rows.\n",
        "operationId": "listCities",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z]{2}$",
              "example": "US"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": true,
            "description": "State id from `GET /geo/states`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 1407
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive name prefix.",
            "schema": {
              "type": "string",
              "example": "den"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GeoRef"
                  }
                },
                "example": [
                  {
                    "id": 115237,
                    "name": "Denison"
                  },
                  {
                    "id": 115247,
                    "name": "Denton"
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/listings": {
      "get": {
        "tags": [
          "Listings"
        ],
        "summary": "List your listings",
        "description": "Listings owned by the key's account, newest first.",
        "operationId": "listListings",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Out-of-range values are clamped, not rejected.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListingPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Listings"
        ],
        "summary": "Create a listing",
        "description": "Creates a business profile owned by the key's account and returns the\nfull card with `201`.\n\n**Idempotent by domain.** If the account already owns a listing on the\nsubmitted domain, nothing is created: the existing card is returned\nwith `200` and `\"existing\": true`. Repeating a create whose response\nyou lost is therefore safe. If somebody *else* owns the domain, the\ncall fails with `409 domain_taken` and the response names only the\nlisting's id and public URL.\n\n**Order of checks** (each one is cheaper than the next, so junk bodies\ncost nothing): field validation -> domain already listed ->\ndaily quota -> domain reachable over HTTP(S) on a public address ->\ncontent gate -> `dry_run` -> write.\n\n`dry_run: true` runs every check and returns `200` with the normalized\nfields under `would_create`, without writing anything and without\nspending daily quota.\n\nMedia (`logo_url`, `photos`) is imported after the listing exists. A\nmedia failure never cancels a created listing - rejected photo URLs\ncome back in `photos_rejected`.\n",
        "operationId": "createListing",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListingCreate"
              },
              "examples": {
                "minimal": {
                  "summary": "Required fields only",
                  "value": {
                    "name": "Northwind Locks",
                    "website": "https://northwindlocks.com",
                    "phone": "+1 415 555 0134",
                    "email": "hello@northwindlocks.com",
                    "country": "US"
                  }
                },
                "full": {
                  "summary": "With geography, categories and media",
                  "value": {
                    "name": "Northwind Locks",
                    "website": "https://northwindlocks.com",
                    "phone": "+1 415 555 0134",
                    "email": "hello@northwindlocks.com",
                    "country": "US",
                    "state_id": 1416,
                    "city": "San Francisco",
                    "address": "1 Market St, San Francisco, CA",
                    "description": "24/7 residential and commercial locksmith.",
                    "categories": [
                      "security-and-investigations"
                    ],
                    "logo_url": "https://northwindlocks.com/logo.png",
                    "photos": [
                      "https://northwindlocks.com/img/van.jpg"
                    ]
                  }
                },
                "dry_run": {
                  "summary": "Validate without writing",
                  "value": {
                    "name": "Northwind Locks",
                    "website": "https://northwindlocks.com",
                    "phone": "+1 415 555 0134",
                    "email": "hello@northwindlocks.com",
                    "country": "US",
                    "dry_run": true
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ListingCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Either the listing already existed and is owned by this account\n(`existing: true`), or `dry_run` was set.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Listing"
                    },
                    {
                      "$ref": "#/components/schemas/DryRunResult"
                    }
                  ]
                },
                "examples": {
                  "dry_run": {
                    "value": {
                      "ok": true,
                      "would_create": {
                        "name": "Northwind Locks",
                        "website": "https://northwindlocks.com",
                        "domain": "northwindlocks.com",
                        "phone": "+1 415 555 0134",
                        "email": "hello@northwindlocks.com",
                        "address": "",
                        "country": "US",
                        "state": null,
                        "city": null,
                        "categories": [],
                        "description": "",
                        "logo_url": null,
                        "photos": []
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidJson"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/DomainTaken"
          },
          "422": {
            "$ref": "#/components/responses/ListingRejected"
          },
          "429": {
            "$ref": "#/components/responses/DailyLimitReached"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/listings/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "get": {
        "tags": [
          "Listings"
        ],
        "summary": "Get one of your listings",
        "description": "A listing this account does not own answers `404`, not `403` - so ids cannot be probed.",
        "operationId": "getListing",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Listings"
        ],
        "summary": "Update one of your listings",
        "description": "Partial update. Omitted fields keep their current values - the current\nstate is loaded first and your body is applied on top, so categories,\nsocial links and opening hours survive a partial PATCH.\n\n`website` cannot be changed: the domain is the listing's identity.\nSending it fails with `422`.\n\nChanging `country` clears `state_id`/`city_id` unless the same request\nsupplies new ones; changing `state_id`/`state` clears `city_id` the\nsame way.\n",
        "operationId": "updateListing",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListingUpdate"
              },
              "example": {
                "description": "24/7 residential and commercial locksmith, licensed in California.",
                "categories": [
                  "security-and-investigations"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidJson"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/listings/{id}/logo": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "post": {
        "tags": [
          "Listings"
        ],
        "summary": "Set the listing logo",
        "description": "Either upload a file (`multipart/form-data`, field `file`) or hand over\na URL (`application/json`, `{\"url\": \"...\"}`). The image goes through\nthe same format, size and blank-image checks as the cabinet upload and\nis resized to 200x200. An existing logo is replaced.\n\nReturns the updated listing card.\n",
        "operationId": "setListingLogo",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://northwindlocks.com/logo.png"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/listings/{id}/photos": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "post": {
        "tags": [
          "Listings"
        ],
        "summary": "Add gallery photos",
        "description": "Either one uploaded file (`multipart/form-data`, field `file`) or up to\n10 URLs per call (`{\"urls\": [...]}`). The gallery holds at most 12\nowner photos in total.\n\nPhotos that fail a check are reported in `photos_rejected` on the\nreturned card; the call itself still succeeds. Reasons seen there:\n`not_an_http_url`, `gallery_full`, `import_failed`, plus the image\nchecks' own reasons.\n",
        "operationId": "addListingPhotos",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "urls"
                ],
                "properties": {
                  "urls": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              },
              "example": {
                "urls": [
                  "https://northwindlocks.com/img/van.jpg",
                  "https://northwindlocks.com/img/team.jpg"
                ]
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Listing"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/listings/{id}/photos/{photoId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        },
        {
          "name": "photoId",
          "in": "path",
          "required": true,
          "description": "Photo id from the listing card's `photos[].id`.",
          "schema": {
            "type": "integer",
            "minimum": 1
          }
        }
      ],
      "delete": {
        "tags": [
          "Listings"
        ],
        "summary": "Delete a gallery photo",
        "operationId": "deleteListingPhoto",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "deleted": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "deleted": {
                    "id": 759204
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/listings/{id}/reviews": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "get": {
        "tags": [
          "Listings",
          "Reviews"
        ],
        "summary": "Reviews of one of your listings",
        "description": "Read-only. Requires only `listings:read` - these are the reviews of a\nlisting the account owns, so they do not spend the company-read quota.\n\nFor richer review fields (owner replies, helpful counts, filters,\nsorting) use `GET /companies/{id}/reviews`.\n",
        "operationId": "listListingReviews",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListingReviewPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/reviews/transfers/sources": {
      "get": {
        "tags": [
          "Review transfers"
        ],
        "summary": "Source platforms",
        "description": "The closed list of platforms a transfer may name. `code` is what you send\nas `source`; `label` is the name that appears in the badge sentence on\nevery transferred review - \"Transferred from Clutch at the owner's\nrequest\". `other` is labelled \"another platform\" so that sentence still\nreads.\n\n`url_mode` says whether `POST .../transfers` with `mode: \"url\"` works for\nthat platform - i.e. whether we can read the owner's public profile page\nthere ourselves. `reason` is a sentence you can show a person as it\nstands: for a platform we cannot fetch it says why and what to do\ninstead, and for one we can it says what a fetch will and will not get.\nRead these instead of hard-coding a list; they change without a version\nbump when a platform starts or stops blocking automated readers.\n\n`connector` marks a platform we reach through its own API rather than by\nreading a page - today only **Google**, through Business Profile OAuth -\nand `configured` says whether ours is live. A connector is authorised by\nthe OWNER, in a browser, on their Import reviews page: it cannot be set\nup over this API and never will be. Once they have, post\n`{source:\"google\", mode:\"google\"}` and we read their reviews from Google\ndirectly. Until then that call answers `409 google_not_connected` with a\nsentence you can show them as it stands.\n",
        "operationId": "listTransferSources",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ReviewTransferSource"
                  }
                },
                "example": [
                  {
                    "code": "techbehemoths",
                    "label": "TechBehemoths",
                    "url_mode": true,
                    "reason": "We read the reviews TechBehemoths publishes on your profile page. Reviews hidden behind their \"View more\" button are only loaded by their JavaScript and cannot be fetched - we will tell you how many we could see.",
                    "connector": false,
                    "configured": false
                  },
                  {
                    "code": "trustpilot",
                    "label": "Trustpilot",
                    "url_mode": false,
                    "reason": "Trustpilot blocks automated access - download the export from your Trustpilot business account and upload that file instead.",
                    "connector": false,
                    "configured": false
                  },
                  {
                    "code": "google",
                    "label": "Google",
                    "url_mode": false,
                    "reason": "The Google Business Profile connection is being set up. Copy your Google reviews into our CSV template and upload it for now.",
                    "connector": true,
                    "configured": false
                  },
                  {
                    "code": "other",
                    "label": "another platform",
                    "url_mode": false,
                    "reason": "Pick the platform by name to fetch from a link. For anything not on the list, copy the reviews into our CSV template and upload it.",
                    "connector": false,
                    "configured": false
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/reviews/transfers/template.csv": {
      "get": {
        "tags": [
          "Review transfers"
        ],
        "summary": "CSV template",
        "description": "Our own CSV template, with the canonical header row and two filled\nexample rows. Answers `text/csv` as an attachment, not JSON.\n\nYou do not have to use it: a Trustpilot, G2 or Capterra export is\nrecognised as-is. Use it when the export you have is none of those, or\nwhen the reviews are being assembled by hand - which is how Google\nreviews get here today, since we do not fetch from Google.\n",
        "operationId": "getTransferTemplate",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "author_name,rating,date,title,text,review_url,source_review_id\n\"Jane Cooper\",5,2025-03-14,\"Delivered exactly what they promised\",\"We hired them for a six-month rebuild and they shipped on time.\",https://www.example-platform.com/reviews/12345,12345\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/listings/{id}/reviews/transfers": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "get": {
        "tags": [
          "Review transfers"
        ],
        "summary": "Transfer history",
        "description": "Every transfer opened for this listing, newest first, at most 50 - from\nany channel, so a transfer the owner started in their cabinet is listed\nhere too.\n\n`transfers_today.used` is how many transfers were opened in the last 24\nhours (informational, there is no cap - `max` is always null);\n`transferred_reviews` counts the transferred reviews live on the profile\nright now, by source.\n",
        "operationId": "listReviewTransfers",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewTransferPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Review transfers"
        ],
        "summary": "Open a transfer (preview)",
        "description": "Move reviews this business already collected on another platform onto its\nTrustburn profile, at the owner's request. Two steps: this call parses,\nvalidates and de-duplicates, and answers `201` with a transfer in\n`preview`; nothing is written to the review index until you call\n`/confirm`.\n\n**The reviews count.** A transferred review is a full review here - it\ncounts towards `avg_rate` and the review counter and shows in the feed on\nthe company page. The only difference is a badge reading \"Transferred\nfrom {Source} at the owner's request\", linking to `source_url`. They are\ndeliberately left out of the page's `Review` structured data, because\nGoogle's review-snippet policy forbids marking up reviews collected from\nother sites.\n\n**`source_url` is mandatory** and must be the company's profile on the\nsource platform, not a search page and not your own site. It is the badge\nlink, and the only way a reader can check the claim.\n\n**Two body shapes.**\n\n* `application/json` - `rows[]` in our canonical shape. Nothing is\n  rejected up front: a row we will not import comes back in\n  `rejected_rows` with its row number and reason, so you can show the\n  owner exactly which ones and why.\n* `multipart/form-data` - `file` plus the same scalar fields. CSV\n  (comma, semicolon or tab, with or without BOM) and JSON are read;\n  XLSX is not - export as CSV. Trustpilot, G2 and Capterra exports are\n  recognised by their header row, and so is our own template. Headers we\n  do not recognise answer `422 unknown_format` with the file's actual\n  headers, which you send back as `mapping`.\n\nA body carrying both a file and `rows` uses the file.\n\n**Limits.** 5,000 rows and 10 MB per transfer. There is no cap on the\nnumber of transfers; re-sending the same reviews is harmless because rows\nalready on the profile are dropped as duplicates. A transfer does **not**\nspend a `listings_per_day` unit.\n\n**Rows we drop.** Rating outside 1-5 after normalisation (a 10-point\nscale is rescaled: `9/10` becomes 5), text under 20 characters, no\nusable date, a date in the future or over 10 years old, contact details\nor links in the body, and duplicates - within the file and against\neverything previously transferred for this company.\n\n**Three body shapes, not two.** The third is `mode: \"url\"`: send\n`{source, source_url, mode:\"url\"}` and we go and read that profile page\nourselves, with our own honest user agent, one request per second, no\nheadless browser. That answers **`202`**, not `201`, and the difference is\nthe contract - the transfer exists but is EMPTY. A background job fetches\nand parses it within the minute. Poll `GET .../transfers/{transferId}`\nuntil `status` leaves `uploaded`/`parsing`:\n\n* `preview` - `items` and the counters are there; confirm as usual.\n* `failed` - read `error`. It is a sentence written for a person: a\n  missing profile page, a platform that refused us, a layout we could\n  not read. A failed fetch does not spend one of the three daily slots.\n\n`fetch_note` on the transfer says what the fetch could NOT see. A profile\nthat advertises 18 reviews but only publishes 12 in its HTML says so\nthere rather than silently importing 12.\n\nOnly some platforms can be fetched - `GET /reviews/transfers/sources`\ncarries `url_mode` and a `reason` per platform, and a `mode:\"url\"` for\none that cannot answers `422 url_not_supported_for_source` with that same\nreason. `source_url` must be a profile page on the platform you named;\nanything else is a `422` on `source_url`.\n\n**Google is a fourth shape, `mode: \"google\"`.** Send\n`{source:\"google\", mode:\"google\"}` and nothing else - no `rows`, no\n`file`, and no `source_url`, because the profile link every transferred\nreview carries is Google's own link for the location the owner picked,\nread off the connection rather than taken from you. It answers `202`\nwith an empty transfer and is polled exactly like `mode: \"url\"`.\n\nThat needs a Google Business Profile connection on the listing, and only\nthe OWNER can make one: OAuth needs a browser and their Google account.\nThere is no endpoint here that starts it and there will not be. Without\none you get `409 google_not_connected` carrying a sentence written to be\nshown to them - point them at their Import reviews page. `GET\n/reviews/transfers/sources` tells you in advance, on the `google` row:\n`connector: true` always, `configured: true` once our own Google access\nis live.\n",
        "operationId": "createReviewTransfer",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source"
                ],
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "file",
                      "url",
                      "google",
                      "manual"
                    ],
                    "description": "`url` fetches the profile at `source_url` in the background and\nanswers `202` with an empty transfer to poll. Omit it (or send\n`file`) to supply the reviews yourself in `rows`, which is then\nrequired. Only platforms whose `url_mode` is true accept `url`.\n\n`google` reads the owner's reviews through the Google Business\nProfile connection on the listing - `source` must be `google`,\nand `source_url` and `rows` are not sent at all. Answers `202`\nlike `url`, or `409 google_not_connected` when the owner has\nnot connected Google in their cabinet.\n\n`manual` imports reviews you send in `rows`, each carrying its\nown `review_url` - the link to that exact review on the source\nplatform. That link is required on every row and becomes that\nreview's badge link; `source_url` is then optional. Answers\n`201` like the default rows path.\n",
                    "example": "url"
                  },
                  "source": {
                    "type": "string",
                    "description": "A `code` from `GET /reviews/transfers/sources`.",
                    "example": "clutch"
                  },
                  "source_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "This company's profile on that platform. Mandatory for `file`\nand `url`. Not accepted for `google` (the link comes from the\nlocation the owner chose in their Google account). Optional for\n`manual`, where each row carries its own `review_url` instead.\n",
                    "example": "https://clutch.co/profile/northwind-locks"
                  },
                  "lang": {
                    "type": "string",
                    "description": "Optional ISO 639-1 hint for the language of the reviews. Saves us a\ndetection call; one language is decided per transfer either way.\n",
                    "example": "en"
                  },
                  "rows": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5000,
                    "description": "Required for the default rows path and for `mode: \"manual\"` (each row then also needs its own `review_url`); not sent for `url` or `google`. A url fetch is capped at 1,000 reviews.",
                    "items": {
                      "$ref": "#/components/schemas/ReviewRowInput"
                    }
                  }
                }
              },
              "example": {
                "source": "clutch",
                "source_url": "https://clutch.co/profile/northwind-locks",
                "rows": [
                  {
                    "author_name": "Jane Cooper",
                    "rating": "5",
                    "date": "2025-03-14",
                    "title": "Delivered exactly what they promised",
                    "text": "We hired them for a six-month rebuild and they shipped on time. Communication was clear every week.",
                    "review_url": "https://clutch.co/profile/northwind-locks#review-12345",
                    "source_review_id": "12345"
                  }
                ]
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "source",
                  "source_url",
                  "file"
                ],
                "properties": {
                  "source": {
                    "type": "string",
                    "example": "trustpilot"
                  },
                  "source_url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "CSV or JSON export, at most 10 MB."
                  },
                  "lang": {
                    "type": "string"
                  },
                  "mapping": {
                    "type": "string",
                    "description": "JSON object `{our_field: \"their column header\"}`, sent after a\n`422 unknown_format`. Fields you may name - `author_name`,\n`rating`, `date`, `title`, `text`, `review_url`,\n`source_review_id`, and `_pros` / `_cons` for exports that\nsplit the body in two. At least the rating and a body column\nare required.\n",
                    "example": "{\"author_name\":\"Kunde\",\"rating\":\"Sterne\",\"date\":\"Datum\",\"text\":\"Bewertung\"}"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Transfer opened, awaiting confirmation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transfer": {
                      "$ref": "#/components/schemas/ReviewTransfer"
                    }
                  }
                },
                "example": {
                  "transfer": {
                    "id": 5,
                    "status": "preview",
                    "channel": "api",
                    "source": "clutch",
                    "source_label": "Clutch",
                    "source_url": "https://clutch.co/profile/northwind-locks",
                    "detected_format": null,
                    "file_name": null,
                    "total": 4,
                    "pending": 2,
                    "imported": 0,
                    "duplicates": 1,
                    "rejected": 1,
                    "error": null,
                    "created_at": "2026-09-09T09:25:45Z",
                    "parsed_at": "2026-09-09T09:25:45Z",
                    "imported_at": null,
                    "undone_at": null,
                    "rejected_rows": [
                      {
                        "row_no": 2,
                        "status": "duplicate",
                        "reason": "duplicate_in_file",
                        "excerpt": "We hired them for a six-month rebuild and they shipped on time."
                      },
                      {
                        "row_no": 3,
                        "status": "rejected",
                        "reason": "contact_details_in_text",
                        "excerpt": "Solid delivery overall. Reach them on +1 415 555 0132 if you want the same crew."
                      }
                    ]
                  }
                }
              }
            }
          },
          "202": {
            "description": "`mode: \"url\"` only. The transfer is open and EMPTY; a background job\nis fetching the profile. Poll `GET .../transfers/{transferId}`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transfer": {
                      "$ref": "#/components/schemas/ReviewTransfer"
                    }
                  }
                },
                "example": {
                  "transfer": {
                    "id": 12,
                    "status": "uploaded",
                    "channel": "api",
                    "mode": "url",
                    "source": "techbehemoths",
                    "source_label": "TechBehemoths",
                    "source_url": "https://techbehemoths.com/company/northwind-locks",
                    "fetch_note": null,
                    "detected_format": null,
                    "file_name": null,
                    "total": 0,
                    "pending": 0,
                    "imported": 0,
                    "duplicates": 0,
                    "rejected": 0,
                    "error": null,
                    "created_at": "2026-09-09T10:27:35Z",
                    "parsed_at": null,
                    "imported_at": null,
                    "undone_at": null
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/GoogleNotConnected"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/TransferRejected"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ReviewIndexUnavailable"
          }
        }
      }
    },
    "/listings/{id}/reviews/transfers/{transferId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        },
        {
          "$ref": "#/components/parameters/TransferId"
        }
      ],
      "get": {
        "tags": [
          "Review transfers"
        ],
        "summary": "One transfer and its rows",
        "description": "The transfer plus a page of the rows we parsed, every status, so you can\nshow the owner the same preview the cabinet shows. `review_id` on an\nimported row is the review's id in\n`GET /listings/{id}/reviews`.\n",
        "operationId": "getReviewTransfer",
        "security": [
          {
            "bearerAuth": [
              "listings:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewTransferDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Review transfers"
        ],
        "summary": "Undo a transfer",
        "description": "Withdraw a completed transfer. Every review it wrote is removed from the\nprofile, the rating and the counter are recomputed, and the ledger keeps\nthe record - the transfer moves to `undone`, it is not deleted.\n\nSelf-service for 30 days after the import; after that only we can do it,\non the owner's written request. Only a transfer in `done` can be undone.\n",
        "operationId": "undoReviewTransfer",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawn.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transfer": {
                      "$ref": "#/components/schemas/ReviewTransfer"
                    }
                  }
                },
                "example": {
                  "transfer": {
                    "id": 5,
                    "status": "undone",
                    "source": "clutch",
                    "source_label": "Clutch",
                    "total": 4,
                    "imported": 2,
                    "duplicates": 1,
                    "rejected": 1,
                    "removed": 2,
                    "undone_at": "2026-09-09T09:29:48Z",
                    "company": {
                      "id": 25230012,
                      "avg_rate": 4.7,
                      "reviews_cnt": 31
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/TransferNotUndoable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ReviewIndexUnavailable"
          }
        }
      }
    },
    "/listings/{id}/reviews/transfers/{transferId}/confirm": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        },
        {
          "$ref": "#/components/parameters/TransferId"
        }
      ],
      "post": {
        "tags": [
          "Review transfers"
        ],
        "summary": "Confirm a transfer (import)",
        "description": "Writes every `pending` row into the live review index and recomputes the\ncompany's rating on the spot. Only a transfer in `preview` can be\nconfirmed; confirming twice answers `409`.\n\nBy calling this you state that the business authorised the transfer and\nthat these are genuine customer reviews of it. Every imported review\ncarries the badge and the source link, and the ledger records the key\nthat asked.\n\nOn a 5,000-row transfer this takes about half a minute - it is a\nsynchronous write into the review index. Set your client timeout to at\nleast 180 seconds.\n",
        "operationId": "confirmReviewTransfer",
        "security": [
          {
            "bearerAuth": [
              "listings:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Imported.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transfer": {
                      "$ref": "#/components/schemas/ReviewTransfer"
                    }
                  }
                },
                "example": {
                  "transfer": {
                    "id": 5,
                    "status": "done",
                    "channel": "api",
                    "source": "clutch",
                    "source_label": "Clutch",
                    "source_url": "https://clutch.co/profile/northwind-locks",
                    "total": 4,
                    "pending": 0,
                    "imported": 2,
                    "duplicates": 1,
                    "rejected": 1,
                    "imported_at": "2026-09-09T09:25:55Z",
                    "company": {
                      "id": 25230012,
                      "avg_rate": 4.7,
                      "reviews_cnt": 31
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/TransferNotConfirmable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/TransferImportFailed"
          },
          "503": {
            "$ref": "#/components/responses/ReviewIndexUnavailable"
          }
        }
      }
    },
    "/agency/balance": {
      "get": {
        "tags": [
          "Agency"
        ],
        "summary": "Listing-year balance",
        "description": "How many agency listing-years this account has left, how many it has\nalready spent, every package it bought, and the current price ladder.\n\nThe balance is not a stored number. It is always the sum of\n`units_total - units_used` over this account's packages, so it cannot\ndrift away from the packages it is made of.\n\n`upgrades_active` counts distinct listings whose agency year has not\nexpired - a renewed listing is one listing, not two.\n",
        "operationId": "getAgencyBalance",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgencyBalance"
                },
                "example": {
                  "units_available": 23,
                  "units_used": 2,
                  "packages": [
                    {
                      "id": 4,
                      "units_total": 25,
                      "units_used": 2,
                      "unit_price": 15,
                      "source": "stripe",
                      "created_at": "2026-09-08T15:02:11Z"
                    }
                  ],
                  "upgrades_active": 2,
                  "pricing": {
                    "tiers": [
                      {
                        "min": 25,
                        "max": 99,
                        "unit_price": 15
                      },
                      {
                        "min": 100,
                        "max": 499,
                        "unit_price": 12
                      },
                      {
                        "min": 500,
                        "unit_price": 9
                      }
                    ],
                    "min_units": 25,
                    "currency": "USD",
                    "term": "1 year"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/agency/checkout": {
      "post": {
        "tags": [
          "Agency"
        ],
        "summary": "Buy a package of listing-years",
        "description": "Creates a Stripe Checkout Session for `units` listing-years and returns\nits URL. Send the buyer there; nothing is charged and nothing lands on\nthe balance until Stripe confirms the payment.\n\nThe minimum order is 25 units. The whole order is billed at the price of\nthe band it falls into - 25 units cost 25 x $15, not a mix of bands.\nThe price is decided here, from `pricing`; it is never taken from the\nrequest.\n\n`success_url` and `cancel_url` are optional and must be absolute\n`https://` URLs. Left out, the buyer comes back to\n`https://trustburn.com/business/api?agency=success|cancel`.\n\nThere is no idempotency key and none is needed: an unpaid session costs\nnothing and expires on Stripe's side by itself.\n",
        "operationId": "createAgencyCheckout",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AgencyCheckoutRequest"
              },
              "example": {
                "units": 100
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session created. Send the buyer to `checkout_url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgencyCheckout"
                },
                "example": {
                  "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_a1b2c3",
                  "session_id": "cs_live_a1b2c3",
                  "units": 100,
                  "unit_price": 12,
                  "total": 1200
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "422": {
            "description": "`units` is missing, not a whole number, or below the 25-unit\nminimum; or a return URL is not an absolute `https://` URL. The\nprice ladder is included so a client can correct the order without\na second call.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "validation_failed",
                    "message": "request validation failed",
                    "fields": {
                      "units": "required: whole number, at least 25"
                    },
                    "pricing": {
                      "tiers": [
                        {
                          "min": 25,
                          "max": 99,
                          "unit_price": 15
                        },
                        {
                          "min": 100,
                          "max": 499,
                          "unit_price": 12
                        },
                        {
                          "min": 500,
                          "unit_price": 9
                        }
                      ],
                      "min_units": 25,
                      "currency": "USD",
                      "term": "1 year"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "The payment session could not be created. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "checkout_unavailable",
                    "message": "the payment session could not be created; please try again"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/listings/{id}/upgrade": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ListingId"
        }
      ],
      "post": {
        "tags": [
          "Agency"
        ],
        "summary": "Spend one listing-year on a listing",
        "description": "Spends one unit from the account's balance and gives this listing, for a\nyear: a **dofollow** link to its website on its public Trustburn page,\nand the **Verified** badge - with no verification step.\n\nCall it again to renew: another unit, another year added to the current\nend date (not to today, so renewing early loses nothing).\n\nThe listing must belong to the key's account; anything else answers\n`404`, the same as an unknown id, so listing ids cannot be probed.\n\nNo body. A repeated call within a few seconds of the previous one is\ntreated as a double-click: it changes nothing, spends nothing, and\nanswers `200` with `charged: false`.\n\nThere are no refunds and a spent unit cannot be moved to another\nlisting.\n",
        "operationId": "upgradeListing",
        "responses": {
          "200": {
            "description": "The listing now has its agency year.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListingUpgrade"
                },
                "example": {
                  "listing_id": 25594885,
                  "tier": "agency",
                  "dofollow": true,
                  "verified": true,
                  "ends_at": "2027-09-08",
                  "units_available": 24,
                  "charged": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Another request is spending credits for this account right now.\nSafe to retry.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "upgrade_conflict",
                    "message": "another request is spending credits for this account; retry"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/companies": {
      "get": {
        "tags": [
          "Companies"
        ],
        "summary": "Find a company by domain",
        "description": "Returns the full public card of the active company on that domain.\nThe domain is normalized the same way listing creation normalizes it,\nso `https://WWW.Example.com/path` and `example.com` find the same card.\n\nA domain can carry more than one active card; the lowest id wins and\n`duplicates` reports how many were found.\n\nResponse is cacheable: `Cache-Control: private, max-age=300` plus an\n`ETag`. Send it back as `If-None-Match` to get `304` - which costs no\ndaily read quota.\n",
        "operationId": "findCompanyByDomain",
        "security": [
          {
            "bearerAuth": [
              "companies:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255,
              "example": "semalt.com"
            }
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "X-Daily-Reads-Limit": {
                "$ref": "#/components/headers/XDailyReadsLimit"
              },
              "X-Daily-Reads-Remaining": {
                "$ref": "#/components/headers/XDailyReadsRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Company"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/ReadLimited"
          }
        }
      }
    },
    "/companies/lookup": {
      "post": {
        "tags": [
          "Companies"
        ],
        "summary": "Look up many domains at once",
        "description": "Up to 100 domains per call. Costs **one** unit of the daily read quota\nno matter how many domains it carries - which is why it returns the\n*compact* card (id, name, domain, public URL, rating, verified/claimed,\nattribution) and not the full one. For the full card, follow up with\n`GET /companies/{id}`, which costs one unit each.\n\nThe response mirrors the request: one entry per submitted domain, in\nthe order sent, with `company: null` where nothing was found. A domain\nthat could not be parsed comes back with an empty `domain` string.\n",
        "operationId": "lookupCompanies",
        "security": [
          {
            "bearerAuth": [
              "companies:read"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domains"
                ],
                "properties": {
                  "domains": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "domains": [
                  "semalt.com",
                  "example-not-here-xyz.com"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupResult"
                },
                "example": {
                  "items": [
                    {
                      "domain": "semalt.com",
                      "company": {
                        "id": 6,
                        "name": "Semalt",
                        "domain": "semalt.com",
                        "public_url": "https://trustburn.com/reviews/semalt",
                        "rating": {
                          "avg": 5,
                          "count": 499
                        },
                        "verified": true,
                        "claimed": true,
                        "attribution": {
                          "text": "Reviews via Trustburn",
                          "url": "https://trustburn.com/reviews/semalt"
                        }
                      }
                    },
                    {
                      "domain": "example-not-here-xyz.com",
                      "company": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidJson"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/ReadLimited"
          }
        }
      }
    },
    "/companies/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        },
        {
          "$ref": "#/components/parameters/IfNoneMatch"
        }
      ],
      "get": {
        "tags": [
          "Companies"
        ],
        "summary": "Get a company",
        "description": "The public card of any active Trustburn company - the same facts an\nanonymous visitor sees on `https://trustburn.com/reviews/{slug}`.\n\nA company with no public page (inactive, or no slug) answers `404`,\nthe same as an unknown id.\n\nThe company's contact email is deliberately **not** part of this\nresponse, even though the public page shows it.\n",
        "operationId": "getCompany",
        "security": [
          {
            "bearerAuth": [
              "companies:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "X-Daily-Reads-Limit": {
                "$ref": "#/components/headers/XDailyReadsLimit"
              },
              "X-Daily-Reads-Remaining": {
                "$ref": "#/components/headers/XDailyReadsRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Company"
                },
                "example": {
                  "id": 6,
                  "name": "Semalt",
                  "website": "https://semalt.com",
                  "domain": "semalt.com",
                  "public_url": "https://trustburn.com/reviews/semalt",
                  "description": "Professional SEO services, keyword ranking monitoring service, competitor analysis. Free plans.",
                  "phone": "+1-855-979-6663",
                  "address": "Pavla Skoropadskoho str. 9A, Kyiv, Ukraine",
                  "country": {
                    "code": "UA",
                    "name": "Ukraine"
                  },
                  "state": {
                    "id": 4671,
                    "name": "Kyivska oblast"
                  },
                  "city": {
                    "id": 109897,
                    "name": "Kyiv"
                  },
                  "categories": [
                    {
                      "slug": "seo-services",
                      "name": "SEO Services"
                    }
                  ],
                  "logo_url": "https://trustburn.com/api/images/company-pic/8-6-company.png",
                  "rating": {
                    "avg": 5,
                    "count": 499,
                    "distribution": {
                      "1": 0,
                      "2": 0,
                      "3": 0,
                      "4": 0,
                      "5": 499
                    }
                  },
                  "verified": true,
                  "claimed": true,
                  "created_at": "2020-09-30T22:00:00Z",
                  "attribution": {
                    "text": "Reviews via Trustburn",
                    "url": "https://trustburn.com/reviews/semalt"
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/ReadLimited"
          }
        }
      }
    },
    "/companies/{id}/reviews": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        }
      ],
      "get": {
        "tags": [
          "Reviews"
        ],
        "summary": "List a company's reviews",
        "description": "The same reviews the public page shows, with the same hidden/deleted\nexclusions. Requires the `reviews:read` scope.\n\nPaging is capped at `page * per_page <= 10000`; going deeper answers\n`422`. Narrow with `min_rating` / `max_rating` instead.\n\n`verified` on a review means \"invited by the business\" - the author\ncame in through the company's own review invitation. There is no other\nverification signal on a review.\n\n`total` respects `min_rating`/`max_rating`; `summary` is always the\ncompany's whole review body.\n",
        "operationId": "listCompanyReviews",
        "security": [
          {
            "bearerAuth": [
              "reviews:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "description": "Values above 50 are clamped to 50, not rejected.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "oldest",
                "highest",
                "lowest"
              ],
              "default": "newest"
            }
          },
          {
            "name": "min_rating",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5
            }
          },
          {
            "name": "max_rating",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Daily-Reads-Limit": {
                "$ref": "#/components/headers/XDailyReadsLimit"
              },
              "X-Daily-Reads-Remaining": {
                "$ref": "#/components/headers/XDailyReadsRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyReviewPage"
                },
                "example": {
                  "items": [
                    {
                      "id": "ok9OJ5EBI73yNbb31zYq",
                      "rating": 5,
                      "title": "Brett Walton is professional",
                      "text": "Brett Walton is professional",
                      "author_name": "abhilash",
                      "created_at": "2024-08-06T10:49:23Z",
                      "verified": false,
                      "helpful_count": 0,
                      "owner_reply": null,
                      "url": "https://trustburn.com/review/ok9OJ5EBI73yNbb31zYq",
                      "lang": "en"
                    }
                  ],
                  "summary": {
                    "avg": 5,
                    "count": 499
                  },
                  "pagination": {
                    "page": 1,
                    "per_page": 2,
                    "total": 499,
                    "total_pages": 250
                  },
                  "attribution": {
                    "text": "Reviews via Trustburn",
                    "url": "https://trustburn.com/reviews/semalt"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/ReadLimited"
          }
        }
      }
    },
    "/companies/{id}/stats": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        },
        {
          "$ref": "#/components/parameters/IfNoneMatch"
        }
      ],
      "get": {
        "tags": [
          "Companies"
        ],
        "summary": "Rating statistics",
        "description": "Rating, star distribution, a 12-month series and the timestamp of the\nnewest review - one call instead of paging the whole review body.\n\nCacheable exactly like `GET /companies/{id}`.\n",
        "operationId": "getCompanyStats",
        "security": [
          {
            "bearerAuth": [
              "companies:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "X-Daily-Reads-Limit": {
                "$ref": "#/components/headers/XDailyReadsLimit"
              },
              "X-Daily-Reads-Remaining": {
                "$ref": "#/components/headers/XDailyReadsRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyStats"
                },
                "example": {
                  "company_id": 6,
                  "rating": {
                    "avg": 5,
                    "count": 499
                  },
                  "distribution": {
                    "1": 0,
                    "2": 0,
                    "3": 0,
                    "4": 0,
                    "5": 499
                  },
                  "by_month": [
                    {
                      "month": "2026-05",
                      "count": 0,
                      "avg": 0
                    },
                    {
                      "month": "2026-06",
                      "count": 1,
                      "avg": 5
                    }
                  ],
                  "last_review_at": "2026-06-29T16:53:34Z"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/ReadLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer tb_live_...`\n\nKeys are created in the business cabinet at\n`https://trustburn.com/business/api`. A key is shown once, at creation;\nTrustburn stores only its SHA-256 hash and can never show it again.\n\nSession cookies are not accepted on `/api/v1/*`. A key carries a set of\nscopes; a call that needs a scope the key lacks answers\n`403 insufficient_scope`.\n\nScopes:\n  * `listings:read` - read your own listings and the reference data.\n  * `listings:write` - create and edit your listings, upload media.\n  * `companies:read` - read any public company card and its statistics.\n  * `reviews:read` - read any public company's reviews.\n"
      }
    },
    "parameters": {
      "ListingId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Listing id. Listings this account does not own answer `404`.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "CompanyId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Trustburn company id.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "example": 6
        }
      },
      "TransferId": {
        "name": "transferId",
        "in": "path",
        "required": true,
        "description": "Transfer id. A transfer belonging to another listing answers `404`.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "ETag from a previous response. A match answers `304` and spends no read quota.",
        "schema": {
          "type": "string"
        }
      }
    },
    "headers": {
      "XRateLimitLimit": {
        "description": "Requests allowed per minute for this key.",
        "schema": {
          "type": "integer",
          "example": 60
        }
      },
      "XRateLimitRemaining": {
        "description": "Requests left in the current 60-second window, after this one.",
        "schema": {
          "type": "integer",
          "example": 59
        }
      },
      "XDailyReadsLimit": {
        "description": "Company reads allowed per UTC day for this key. `0` means unlimited.",
        "schema": {
          "type": "integer",
          "example": 1000
        }
      },
      "XDailyReadsRemaining": {
        "description": "Company reads left today, after this one.",
        "schema": {
          "type": "integer",
          "example": 998
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "example": 42
        }
      },
      "ETag": {
        "description": "Strong entity tag of the JSON body. Cloudflare may weaken it to `W/\"...\"`; send back whatever you received.",
        "schema": {
          "type": "string",
          "example": "\"9d816d67ea958f9a5348271e022c6b88\""
        }
      },
      "CacheControl": {
        "description": "Always `private` - a response depends on the key that asked for it.",
        "schema": {
          "type": "string",
          "example": "private, max-age=300"
        }
      }
    },
    "responses": {
      "NotModified": {
        "description": "Your `If-None-Match` still matches. No body, no read quota spent.",
        "headers": {
          "ETag": {
            "$ref": "#/components/headers/ETag"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/CacheControl"
          }
        }
      },
      "Unauthorized": {
        "description": "No key, a malformed key, or a key that is unknown, revoked, or belongs\nto a disabled account.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": {
                    "code": "unauthorized",
                    "message": "API key required: send \"Authorization: Bearer tb_live_...\""
                  }
                }
              },
              "invalid": {
                "value": {
                  "error": {
                    "code": "unauthorized",
                    "message": "invalid or revoked API key"
                  }
                }
              }
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "The key is valid but lacks the scope this endpoint requires.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "insufficient_scope",
                "message": "this key does not have the \"companies:read\" scope",
                "required_scope": "companies:read"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Blocked (`forbidden`), or the key lacks the required scope (`insufficient_scope`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "forbidden",
                "message": "blocked"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Unknown endpoint, or a resource that does not exist or is not visible\nto this key. Listings you do not own answer `404` on purpose.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "endpoint": {
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "unknown endpoint"
                  }
                }
              },
              "company": {
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "company not found"
                  }
                }
              },
              "listing": {
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "listing not found"
                  }
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The account has no listing-years left. Buy a package with\n`POST /agency/checkout`; the price ladder comes with the error so no\nextra call is needed.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "no_agency_credits",
                "message": "no agency listing-years left on this account; buy a package to continue",
                "checkout": "/v1/agency/checkout",
                "pricing": {
                  "tiers": [
                    {
                      "min": 25,
                      "max": 99,
                      "unit_price": 15
                    },
                    {
                      "min": 100,
                      "max": 499,
                      "unit_price": 12
                    },
                    {
                      "min": 500,
                      "unit_price": 9
                    }
                  ],
                  "min_units": 25,
                  "currency": "USD",
                  "term": "1 year"
                }
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The path exists but not for this HTTP method.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "method_not_allowed",
                "message": "method DELETE is not allowed for this endpoint"
              }
            }
          }
        }
      },
      "InvalidJson": {
        "description": "The request body is not valid JSON.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_json",
                "message": "request body is not valid JSON"
              }
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "One or more fields were rejected. `fields` maps field name to the reason.\nAll bad fields are reported at once, not one per request.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "validation_failed",
                "message": "request validation failed",
                "fields": {
                  "name": "must be 2 to 150 characters",
                  "website": "required",
                  "phone": "required",
                  "email": "required",
                  "country": "required"
                }
              }
            }
          }
        }
      },
      "ListingRejected": {
        "description": "Validation failure (`validation_failed`), or the domain does not answer\non a public address (`domain_unreachable`), or the site does not\nqualify for a listing (`content_rejected`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "validation_failed": {
                "value": {
                  "error": {
                    "code": "validation_failed",
                    "message": "request validation failed",
                    "fields": {
                      "website": "not a valid registrable domain"
                    }
                  }
                }
              },
              "domain_unreachable": {
                "value": {
                  "error": {
                    "code": "domain_unreachable",
                    "message": "the domain does not answer over HTTP(S) on a public address",
                    "domain": "northwindlocks.com"
                  }
                }
              },
              "content_rejected": {
                "value": {
                  "error": {
                    "code": "content_rejected",
                    "message": "the site does not qualify for a listing",
                    "reason": "parked domain",
                    "domain": "northwindlocks.com"
                  }
                }
              }
            }
          }
        }
      },
      "DomainTaken": {
        "description": "Somebody else already has a listing on this domain.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "domain_taken",
                "message": "a listing for this domain already exists",
                "listing": {
                  "id": 6,
                  "public_url": "https://trustburn.com/reviews/semalt"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests: `rate_limited` from the per-key window (60/min) or\nfrom the edge (120/min per IP).\n",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "rate limit exceeded: 60 requests per minute",
                "retry_after": 42
              }
            }
          }
        }
      },
      "DailyLimitReached": {
        "description": "`rate_limited` (per-minute window) or `daily_limit_reached` - this key\nhas already created its allowance of listings for the current UTC day.\n",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "daily_limit_reached",
                "message": "this key may create 200 listings per day (UTC); 200 already created today",
                "limit": 200,
                "used": 200
              }
            }
          }
        }
      },
      "ReadLimited": {
        "description": "`rate_limited` (per-minute window) or `daily_read_limit_reached` - the\nkey has spent its company-read allowance for the current UTC day.\n",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-Daily-Reads-Limit": {
            "$ref": "#/components/headers/XDailyReadsLimit"
          },
          "X-Daily-Reads-Remaining": {
            "$ref": "#/components/headers/XDailyReadsRemaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "daily_read_limit_reached",
                "message": "daily read limit exceeded: 1000 company reads per day (UTC)"
              }
            }
          }
        }
      },
      "TransferRejected": {
        "description": "`validation_failed` when a field of the request was wrong, or\n`unknown_format` when the uploaded file's header row matched none of the\nknown exports. The `unknown_format` body carries the file's real headers:\nsend them back as `mapping` and the same file parses.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "validation_failed": {
                "value": {
                  "error": {
                    "code": "validation_failed",
                    "message": "request validation failed",
                    "fields": {
                      "source_url": "required: an http(s) link to this company profile on the source platform"
                    }
                  }
                }
              },
              "unknown_format": {
                "value": {
                  "error": {
                    "code": "unknown_format",
                    "message": "The column headers were not recognised.",
                    "headers": [
                      "Kunde",
                      "Sterne",
                      "Datum",
                      "Bewertung"
                    ],
                    "fields": {
                      "mapping": "send {our_field: \"their column header\"} for at least the rating and the text"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The upload is over 10 MB, or the request carries more than 5,000 rows.\nA body over 12 MB is refused at the edge with the same code, before PHP\nsees it, so that call is not in your usage log.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "file": {
                "value": {
                  "error": {
                    "code": "payload_too_large",
                    "message": "the uploaded file is larger than 10 MB",
                    "max_file_bytes": 10485760
                  }
                }
              },
              "rows": {
                "value": {
                  "error": {
                    "code": "payload_too_large",
                    "message": "A single transfer carries at most 5000 reviews.",
                    "max_rows": 5000
                  }
                }
              }
            }
          }
        }
      },
      "GoogleNotConnected": {
        "description": "`mode: \"google\"` on a listing with no usable Google Business Profile\nconnection. Nothing in the request is wrong and nothing you can send\nwill fix it: only the owner can connect Google, in a browser, on their\nImport reviews page. `message` is written to be shown to them as it\nstands, and `configured` says which of the two situations this is - our\nown Google access still being set up, or simply nobody having connected\nthis listing yet.\n\n`google_no_location` means they connected but have not said which of\ntheir Google locations this listing is; `google_no_profile_url` means\nGoogle gave us no public link for that location, and a transferred\nreview with no source link is the one thing we will not publish.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "not_connected": {
                "value": {
                  "error": {
                    "code": "google_not_connected",
                    "message": "This company has no live Google Business Profile connection. Connect it on the Import reviews page first -- it needs a browser, so it cannot be done over the API.",
                    "configured": true
                  }
                }
              },
              "connector_not_live": {
                "value": {
                  "error": {
                    "code": "google_not_connected",
                    "message": "The Google Business Profile connection is being set up. Copy your Google reviews into our CSV template and upload it for now.",
                    "configured": false
                  }
                }
              },
              "no_location": {
                "value": {
                  "error": {
                    "code": "google_no_location",
                    "message": "Choose which of your Google locations to import from first."
                  }
                }
              }
            }
          }
        }
      },
      "TransferNotConfirmable": {
        "description": "The transfer is not in `preview` - most often it has already been\nconfirmed. `nothing_to_import` means every row was a duplicate or was\nrejected, so there is nothing left to write.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "not_confirmable": {
                "value": {
                  "error": {
                    "code": "transfer_not_confirmable",
                    "message": "This transfer is done, not awaiting confirmation."
                  }
                }
              },
              "nothing_to_import": {
                "value": {
                  "error": {
                    "code": "nothing_to_import",
                    "message": "Every row in this transfer was a duplicate or was rejected."
                  }
                }
              }
            }
          }
        }
      },
      "TransferNotUndoable": {
        "description": "`transfer_not_undoable` - only a completed transfer can be withdrawn.\n`undo_window_closed` - the 30-day self-service window has passed; write\nto support and we will withdraw it.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "not_undoable": {
                "value": {
                  "error": {
                    "code": "transfer_not_undoable",
                    "message": "Only a completed transfer can be undone (this one is preview)."
                  }
                }
              },
              "window_closed": {
                "value": {
                  "error": {
                    "code": "undo_window_closed",
                    "message": "Self-service undo is available for 30 days after the import.",
                    "undo_window_days": 30
                  }
                }
              }
            }
          }
        }
      },
      "TransferImportFailed": {
        "description": "The review index refused every write. The transfer is left in `failed`\nand nothing was imported; quote its id to support.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "import_failed",
                "message": "the reviews could not be written to the review index"
              }
            }
          }
        }
      },
      "ReviewIndexUnavailable": {
        "description": "The review index was unreachable. Nothing was changed; retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "review_index_unavailable",
                "message": "Review index unavailable."
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Something failed on our side. Nothing about the failure is exposed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "internal error"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The single error envelope for everything under `/api/v1/*`.\n`code` is stable and safe to branch on; `message` is for humans.\nSome codes add fields: `fields` (validation), `required_scope`,\n`retry_after`, `limit`/`used`, `domain`, `reason`, `listing`.\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code. The complete set:\n`unauthorized`, `insufficient_scope`, `forbidden`,\n`bad_request`, `invalid_json`, `not_found`,\n`method_not_allowed`, `domain_taken`, `validation_failed`,\n`domain_unreachable`, `content_rejected`, `rate_limited`,\n`daily_limit_reached`, `daily_read_limit_reached`,\n`create_failed`, `internal_error`, `error`.\n",
                "enum": [
                  "unauthorized",
                  "insufficient_scope",
                  "forbidden",
                  "bad_request",
                  "invalid_json",
                  "not_found",
                  "method_not_allowed",
                  "domain_taken",
                  "validation_failed",
                  "domain_unreachable",
                  "content_rejected",
                  "rate_limited",
                  "daily_limit_reached",
                  "daily_read_limit_reached",
                  "create_failed",
                  "internal_error",
                  "error"
                ]
              },
              "message": {
                "type": "string"
              },
              "fields": {
                "type": "object",
                "description": "Present on `validation_failed`. Field name -> reason.",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "required_scope": {
                "type": "string",
                "description": "Present on `insufficient_scope`."
              },
              "retry_after": {
                "type": "integer",
                "description": "Present on the per-key `rate_limited`. Seconds."
              },
              "limit": {
                "type": "integer",
                "description": "Present on `daily_limit_reached`."
              },
              "used": {
                "type": "integer",
                "description": "Present on `daily_limit_reached`."
              },
              "domain": {
                "type": "string",
                "description": "Present on `domain_unreachable` / `content_rejected`."
              },
              "reason": {
                "type": "string",
                "description": "Present on `content_rejected`."
              },
              "listing": {
                "type": "object",
                "description": "Present on `domain_taken`. Only the id and public URL of the existing listing.",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "public_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri"
                  }
                }
              }
            },
            "additionalProperties": true
          }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "account": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email_masked": {
                "type": "string",
                "example": "ja***@acme.com"
              },
              "companies": {
                "type": "integer",
                "description": "How many companies this account is attached to."
              }
            }
          },
          "key": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "prefix": {
                "type": "string",
                "description": "First 16 characters of the key - the public half, safe to log.",
                "example": "tb_live_a1b2c3d4"
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "listings:read",
                    "listings:write",
                    "companies:read",
                    "reviews:read"
                  ]
                }
              },
              "created_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "last_used_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "The *previous* call - the current one is stamped after this row is read."
              }
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "rate_per_min": {
                "type": "integer"
              },
              "listings_per_day": {
                "type": "integer"
              },
              "listings_created_today": {
                "type": "integer"
              },
              "reads_per_day": {
                "type": "integer",
                "description": "`0` means unlimited."
              },
              "reads_today": {
                "type": "integer"
              }
            }
          },
          "api_version": {
            "type": "string",
            "const": "v1"
          }
        }
      },
      "Category": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "example": "seo-services"
          },
          "name": {
            "type": "string",
            "example": "SEO Services"
          }
        }
      },
      "Country": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "example": "UA"
          },
          "name": {
            "type": "string",
            "example": "Ukraine"
          }
        }
      },
      "GeoRef": {
        "type": "object",
        "description": "A state or a city, by primary key.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "Rating": {
        "type": "object",
        "properties": {
          "avg": {
            "type": "number",
            "format": "float"
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "Distribution": {
        "type": "object",
        "description": "Star counts, always with all five keys present.",
        "properties": {
          "1": {
            "type": "integer"
          },
          "2": {
            "type": "integer"
          },
          "3": {
            "type": "integer"
          },
          "4": {
            "type": "integer"
          },
          "5": {
            "type": "integer"
          }
        }
      },
      "Attribution": {
        "type": "object",
        "description": "The credit line you must show when you display this data, and the URL\nit must link to.\n",
        "properties": {
          "text": {
            "type": "string",
            "const": "Reviews via Trustburn"
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "Photo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Listing": {
        "type": "object",
        "description": "A business profile owned by the key's account.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "public_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The listing's page on Trustburn. `null` while it has no public page."
          },
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2."
          },
          "state": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/GeoRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "city": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/GeoRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Category"
            }
          },
          "description": {
            "type": "string"
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "photos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Photo"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "owner_verified": {
            "type": [
              "string",
              "null"
            ],
            "description": "How ownership was proved, e.g. `email`, `instant`. `agency` means it\ncame with an agency listing-year. `null` when it was not proved.\n"
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "growth",
              "pro",
              "agency"
            ],
            "description": "The listing's current plan. `agency` is a listing-year spent through\n`POST /listings/{id}/upgrade`. A real subscription outranks it.\n"
          },
          "dofollow": {
            "type": "boolean",
            "description": "Whether the \"Visit website\" link on the public page is a real dofollow\nlink right now. True needs both a plan that includes it and a website\naddress on the listing.\n"
          },
          "agency_ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Last day of the agency listing-year, or `null` if there is none."
          },
          "rating": {
            "$ref": "#/components/schemas/Rating"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": "null",
            "description": "Always `null`. There is no edit timestamp on the companies table\nand adding one would mean an ALTER on a 25-million-row hot table.\nThe field is present so clients do not have to change their parser\nwhen it starts carrying a value.\n"
          },
          "existing": {
            "type": "boolean",
            "description": "Only on `POST /listings` when the account already owned this domain."
          },
          "photos_rejected": {
            "type": "array",
            "description": "Photo URLs that did not make it, with a reason each.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "example": "gallery_full"
                }
              }
            }
          }
        }
      },
      "AgencyPricing": {
        "type": "object",
        "description": "The volume ladder. The whole order is billed at its band's price.",
        "properties": {
          "tiers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "min": {
                  "type": "integer"
                },
                "max": {
                  "type": "integer",
                  "description": "Absent on the top band, which has no upper bound."
                },
                "unit_price": {
                  "type": "number",
                  "description": "US dollars per listing-year."
                }
              }
            }
          },
          "min_units": {
            "type": "integer",
            "example": 25
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "term": {
            "type": "string",
            "example": "1 year"
          }
        }
      },
      "AgencyPackage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "units_total": {
            "type": "integer"
          },
          "units_used": {
            "type": "integer"
          },
          "unit_price": {
            "type": "number",
            "description": "US dollars paid per listing-year in this package."
          },
          "source": {
            "type": "string",
            "enum": [
              "stripe",
              "manual"
            ],
            "description": "`manual` means it was booked by sales against an invoice."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AgencyBalance": {
        "type": "object",
        "properties": {
          "units_available": {
            "type": "integer"
          },
          "units_used": {
            "type": "integer"
          },
          "packages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgencyPackage"
            }
          },
          "upgrades_active": {
            "type": "integer",
            "description": "Distinct listings whose agency year has not expired."
          },
          "pricing": {
            "$ref": "#/components/schemas/AgencyPricing"
          }
        }
      },
      "AgencyCheckoutRequest": {
        "type": "object",
        "required": [
          "units"
        ],
        "properties": {
          "units": {
            "type": "integer",
            "minimum": 25,
            "description": "How many listing-years to buy. At least 25."
          },
          "success_url": {
            "type": "string",
            "format": "uri",
            "description": "Absolute `https://` URL. Defaults to the Trustburn cabinet."
          },
          "cancel_url": {
            "type": "string",
            "format": "uri",
            "description": "Absolute `https://` URL. Defaults to the Trustburn cabinet."
          }
        }
      },
      "AgencyCheckout": {
        "type": "object",
        "properties": {
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "Stripe Checkout. Send the buyer here."
          },
          "session_id": {
            "type": "string",
            "example": "cs_live_a1b2c3"
          },
          "units": {
            "type": "integer"
          },
          "unit_price": {
            "type": "number"
          },
          "total": {
            "type": "number",
            "description": "US dollars for the whole order."
          }
        }
      },
      "ListingUpgrade": {
        "type": "object",
        "properties": {
          "listing_id": {
            "type": "integer"
          },
          "tier": {
            "type": "string",
            "description": "The listing's plan after the call. `agency`, unless a paid subscription outranks it."
          },
          "dofollow": {
            "type": "boolean"
          },
          "verified": {
            "type": "boolean"
          },
          "ends_at": {
            "type": "string",
            "format": "date",
            "description": "Last day covered by the agency year."
          },
          "units_available": {
            "type": "integer",
            "description": "Balance after the call."
          },
          "charged": {
            "type": "boolean",
            "description": "`false` means the call landed inside the double-click window and\nnothing was spent - the listing already had this exact upgrade.\n"
          }
        }
      },
      "ListingPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Listing"
            }
          },
          "page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "ListingCreate": {
        "type": "object",
        "required": [
          "name",
          "website",
          "phone",
          "email",
          "country"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 150
          },
          "website": {
            "type": "string",
            "description": "The business's own site. Normalized to a registrable domain, which\nbecomes the listing's identity and can never be changed afterwards.\n"
          },
          "phone": {
            "type": "string",
            "maxLength": 64
          },
          "email": {
            "type": "string",
            "maxLength": 256
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Za-z]{2}$"
          },
          "state_id": {
            "type": "integer",
            "description": "From `GET /geo/states`. Takes precedence over `state`."
          },
          "state": {
            "type": "string",
            "description": "State name. Ambiguous names answer `422` with `candidates`."
          },
          "city_id": {
            "type": "integer",
            "description": "From `GET /geo/cities`. Takes precedence over `city`."
          },
          "city": {
            "type": "string",
            "description": "City name. Ambiguous names answer `422` with `candidates`."
          },
          "address": {
            "type": "string",
            "maxLength": 256
          },
          "description": {
            "type": "string",
            "maxLength": 8000
          },
          "categories": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "string",
              "description": "Category slug from `GET /categories`."
            }
          },
          "logo_url": {
            "type": "string",
            "format": "uri"
          },
          "photos": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "dry_run": {
            "type": "boolean",
            "default": false,
            "description": "Run every check and return the normalized fields without writing anything."
          }
        }
      },
      "ListingUpdate": {
        "type": "object",
        "description": "Same fields as `ListingCreate`, all optional, except `website`, which\nis rejected. Omitted fields keep their current values.\n",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 150
          },
          "phone": {
            "type": "string",
            "maxLength": 64
          },
          "email": {
            "type": "string",
            "maxLength": 256
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Za-z]{2}$"
          },
          "state_id": {
            "type": "integer"
          },
          "state": {
            "type": "string"
          },
          "city_id": {
            "type": "integer"
          },
          "city": {
            "type": "string"
          },
          "address": {
            "type": "string",
            "maxLength": 256
          },
          "description": {
            "type": "string",
            "maxLength": 8000
          },
          "categories": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "string"
            }
          },
          "logo_url": {
            "type": "string",
            "format": "uri"
          },
          "photos": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "DryRunResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "would_create": {
            "type": "object",
            "description": "The submitted fields after normalization - exactly what a real create would store.",
            "properties": {
              "name": {
                "type": "string"
              },
              "website": {
                "type": "string"
              },
              "domain": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "address": {
                "type": "string"
              },
              "country": {
                "type": "string"
              },
              "state": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/GeoRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "city": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/GeoRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "categories": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Category"
                }
              },
              "description": {
                "type": "string"
              },
              "logo_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "photos": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ListingReview": {
        "type": "object",
        "description": "The lean review shape returned for your own listings.",
        "properties": {
          "id": {
            "type": "string"
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "title": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "author_name": {
            "type": "string"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ListingReviewPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListingReview"
            }
          },
          "page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "summary": {
            "$ref": "#/components/schemas/Rating"
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "Company": {
        "type": "object",
        "description": "The public card of any active Trustburn company - the facts an\nanonymous visitor sees on the company page. The contact email is not\nincluded.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "public_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "description": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "country": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Country"
              },
              {
                "type": "null"
              }
            ]
          },
          "state": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/GeoRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "city": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/GeoRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Category"
            }
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "rating": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Rating"
              },
              {
                "type": "object",
                "properties": {
                  "distribution": {
                    "$ref": "#/components/schemas/Distribution"
                  }
                }
              }
            ]
          },
          "verified": {
            "type": "boolean",
            "description": "Ownership was really proved - the same test that draws the Verified\nbadge on the public page. A claim that is merely pending is not\n`verified`; it shows up as `claimed`.\n"
          },
          "claimed": {
            "type": "boolean",
            "description": "Somebody has attached this company to their account."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "duplicates": {
            "type": "integer",
            "description": "Only on `GET /companies?domain=` when several active cards share the domain."
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "CompanyCompact": {
        "type": "object",
        "description": "The short card returned by `POST /companies/lookup`.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "public_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "rating": {
            "$ref": "#/components/schemas/Rating"
          },
          "verified": {
            "type": "boolean"
          },
          "claimed": {
            "type": "boolean"
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "LookupResult": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "description": "One entry per submitted domain, in the order sent.",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string",
                  "description": "The normalized domain. Empty when the input could not be parsed."
                },
                "company": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CompanyCompact"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "Review": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "title": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "author_name": {
            "type": "string",
            "description": "Display name, or `Customer` when the author has none - the same substitution the page makes."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "verified": {
            "type": "boolean",
            "description": "The review came in through the business's own invitation (\"Invited by the business\")."
          },
          "helpful_count": {
            "type": "integer"
          },
          "owner_reply": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "created_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "lang": {
            "type": "string",
            "description": "Language of the review text, e.g. `en`, `ru`."
          }
        }
      },
      "CompanyReviewPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Review"
            }
          },
          "summary": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Rating"
              },
              {
                "description": "Across every review of the company, ignoring `min_rating`/`max_rating`."
              }
            ]
          },
          "pagination": {
            "type": "object",
            "properties": {
              "page": {
                "type": "integer"
              },
              "per_page": {
                "type": "integer"
              },
              "total": {
                "type": "integer",
                "description": "Matching reviews, after `min_rating`/`max_rating`."
              },
              "total_pages": {
                "type": "integer"
              }
            }
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          }
        }
      },
      "CompanyStats": {
        "type": "object",
        "properties": {
          "company_id": {
            "type": "integer"
          },
          "rating": {
            "$ref": "#/components/schemas/Rating"
          },
          "distribution": {
            "$ref": "#/components/schemas/Distribution"
          },
          "by_month": {
            "type": "array",
            "description": "Last 12 months, oldest first. Months with no reviews are present with zeros.",
            "items": {
              "type": "object",
              "properties": {
                "month": {
                  "type": "string",
                  "example": "2026-06"
                },
                "count": {
                  "type": "integer"
                },
                "avg": {
                  "type": "number",
                  "format": "float"
                }
              }
            }
          },
          "last_review_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ReviewTransferSource": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "google",
              "clutch",
              "trustpilot",
              "g2",
              "capterra",
              "designrush",
              "techbehemoths",
              "superbcompanies",
              "other"
            ]
          },
          "label": {
            "type": "string",
            "example": "Clutch"
          },
          "url_mode": {
            "type": "boolean",
            "description": "Whether `mode: \"url\"` is accepted for this platform."
          },
          "reason": {
            "type": "string",
            "description": "Why not, or what a fetch does and does not get. Written to be shown\nto a person unchanged.\n"
          },
          "connector": {
            "type": "boolean",
            "description": "Whether this platform is reached through its own API instead of by\nreading a page - today only Google, through Business Profile OAuth.\nThe owner authorises it in their cabinet; it cannot be set up over\nthis API.\n"
          },
          "configured": {
            "type": "boolean",
            "description": "Whether that connector is live on our side. `connector: true,\nconfigured: false` is Google before its API access is granted:\n`mode: \"google\"` answers `409 google_not_connected` and the CSV\ntemplate is the way through.\n"
          }
        }
      },
      "ReviewRowInput": {
        "type": "object",
        "description": "One review as it exists on the source platform. Everything is accepted as\na string; we normalise. A row we cannot import is not an error - it comes\nback in `rejected_rows` with its row number and reason.\n",
        "required": [
          "rating",
          "date",
          "text"
        ],
        "properties": {
          "author_name": {
            "type": "string",
            "maxLength": 120,
            "description": "Empty becomes \"Anonymous\".",
            "example": "Jane Cooper"
          },
          "rating": {
            "type": "string",
            "description": "`5`, `4.0`, `4,5`, `5/5`, `9/10`, `8 out of 10`, `4 stars` all work.\nA scale larger than 5 is rescaled and rounded, so a 9/10 shows as 5\nstars here - not as an unexplained 9.\n",
            "example": "5"
          },
          "date": {
            "type": "string",
            "description": "The ORIGINAL review's date, so the feed stays honest. Not in the future, not over 10 years old.",
            "example": "2025-03-14"
          },
          "title": {
            "type": "string",
            "maxLength": 255,
            "description": "Optional. Derived from the opening of `text` when absent."
          },
          "text": {
            "type": "string",
            "minLength": 20,
            "description": "The review body. Contact details and links get the row rejected, exactly as they would in an organic review."
          },
          "review_url": {
            "type": "string",
            "format": "uri",
            "description": "Optional. The individual review on the source platform."
          },
          "source_review_id": {
            "type": "string",
            "maxLength": 128,
            "description": "Optional. The platform's own id for the review. When present it is\nthe de-duplication key, which is what makes re-uploading a fuller\nexport safe.\n"
          }
        }
      },
      "ReviewTransfer": {
        "type": "object",
        "description": "One transfer. For a file it starts at `preview`; for `mode: \"url\"` it\nstarts at `uploaded`, passes through `parsing` while the background job\nworks, and lands on `preview` or `failed`. From `preview` it walks to\n`done` (or `failed`), and `undone` after a withdrawal. Counters - `total`\nis every parsed row, `pending` is what a confirm would write, `imported`\nwhat it wrote, `duplicates` and `rejected` what it will not. They are all\nzero until a URL transfer reaches `preview`.\n",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "uploaded",
              "parsing",
              "preview",
              "importing",
              "done",
              "failed",
              "undone"
            ]
          },
          "channel": {
            "type": "string",
            "enum": [
              "cabinet",
              "api",
              "admin"
            ],
            "description": "Who opened it. `api` is you; `cabinet` is the owner; `admin` is us, on their written request."
          },
          "mode": {
            "type": "string",
            "enum": [
              "file",
              "url",
              "google"
            ],
            "description": "Where the rows came from - an upload, a fetch of `source_url`, or the\nowner's Google Business Profile connection.\n"
          },
          "source": {
            "type": "string",
            "example": "clutch"
          },
          "source_label": {
            "type": "string",
            "example": "Clutch"
          },
          "source_url": {
            "type": "string",
            "format": "uri"
          },
          "fetch_note": {
            "type": [
              "string",
              "null"
            ],
            "description": "`mode: \"url\"` only. What the fetch could not see, in a sentence -\ne.g. a profile that advertises more reviews than its page publishes.\nShow it to the owner; it is the difference between a partial import\nthey understand and one they complain about.\n"
          },
          "detected_format": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which export shape the file matched - `trustburn`, `trustpilot`,\n`g2`, `capterra`, `json`, `manual`. `url:<source>` for a fetch.\nNull for a `rows` body.\n"
          },
          "file_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer"
          },
          "pending": {
            "type": "integer"
          },
          "imported": {
            "type": "integer"
          },
          "duplicates": {
            "type": "integer"
          },
          "rejected": {
            "type": "integer"
          },
          "removed": {
            "type": "integer",
            "description": "Reviews withdrawn. Present on the undo response only."
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "parsed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "imported_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "undone_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "company": {
            "type": "object",
            "description": "The recomputed profile numbers. Present on confirm and undo.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "avg_rate": {
                "type": "number",
                "format": "float"
              },
              "reviews_cnt": {
                "type": "integer"
              }
            }
          },
          "rejected_rows": {
            "type": "array",
            "description": "Rows that will not be imported. Present on the create response.",
            "items": {
              "type": "object",
              "properties": {
                "row_no": {
                  "type": "integer",
                  "description": "1-based position in the rows you sent, or in the file after its header."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "duplicate",
                    "rejected"
                  ]
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "invalid_rating",
                    "text_too_short",
                    "invalid_date",
                    "date_in_future",
                    "date_too_old",
                    "contact_details_in_text",
                    "duplicate_in_file",
                    "already_transferred"
                  ]
                },
                "excerpt": {
                  "type": "string",
                  "description": "First 120 characters of the body, so the owner can recognise the row."
                }
              }
            }
          }
        }
      },
      "ReviewTransferItem": {
        "type": "object",
        "description": "One parsed row of a transfer.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "row_no": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "imported",
              "duplicate",
              "rejected"
            ]
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "author_name": {
            "type": "string"
          },
          "rating": {
            "type": "integer",
            "minimum": 0,
            "maximum": 5
          },
          "title": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "lang": {
            "type": "string",
            "example": "en"
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "review_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "source_review_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "review_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The imported review's id, the same one `GET /listings/{id}/reviews` reports. Null until imported."
          }
        }
      },
      "ReviewTransferPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReviewTransfer"
            }
          },
          "total": {
            "type": "integer",
            "description": "Transfers returned (at most 50)."
          },
          "transfers_today": {
            "type": "object",
            "properties": {
              "used": {
                "type": "integer",
                "description": "Transfers opened in the last 24 hours (informational)."
              },
              "max": {
                "type": "integer",
                "nullable": true,
                "example": null,
                "description": "Always null - there is no daily cap."
              }
            }
          },
          "transferred_reviews": {
            "type": "object",
            "description": "Transferred reviews live on the profile right now.",
            "properties": {
              "count": {
                "type": "integer"
              },
              "sources": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                },
                "example": {
                  "clutch": 12,
                  "google": 4
                }
              }
            }
          }
        }
      },
      "ReviewTransferDetail": {
        "type": "object",
        "properties": {
          "transfer": {
            "$ref": "#/components/schemas/ReviewTransfer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReviewTransferItem"
            }
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "total": {
            "type": "integer",
            "description": "Rows in the whole transfer, not on this page."
          }
        }
      }
    }
  }
}