Trustburn API
Manage your business listings and read Trustburn review data from your own systems. JSON over HTTPS, one Bearer key, no SDK required.
Overview
The Trustburn API lets an integration do two things:
- Manage its own listings. Create and edit the business profiles your account owns, upload a logo and photos, read the reviews those listings received.
- Read public review data. Look up any active Trustburn company by id or domain, read its reviews, and read its rating statistics.
It exists for the people who were doing this by hand: agencies and multi-location businesses running dozens or hundreds of profiles, and product teams that want a company's Trustburn rating inside their own screens. Creating and editing listings through the API costs nothing.
What the API deliberately does not do: it cannot submit reviews. There is no write path for review content, for anybody, and there will not be one — a review that a machine can post is not a review anyone should trust.
Base URL for every call:
https://trustburn.com/api/v1
All responses are JSON. All timestamps are UTC, ISO 8601 with a literal Z.
Getting a key
Sign in to your Trustburn business account and open API keys in the cabinet. Name the key after the thing that will use it, press Create key, and copy the value.
A key looks like this:
tb_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
The first 16 characters (tb_live_a1b2c3d4) are the key's public prefix — that is what the cabinet shows in the list and what is safe to put in a log line. The rest is the secret.
Your first call should be GET /me: it confirms the key works and tells you the scopes and quotas it carries.
Authentication
Send the key as a Bearer token on every request:
curl -s https://trustburn.com/api/v1/me \
-H "Authorization: Bearer $TB_KEY"
Browser session cookies are not accepted on /api/v1/*. A key is the only way in, and a missing, malformed, unknown or revoked key answers 401 unauthorized.
Scopes
Every key carries a set of scopes. New keys get all four. A call that needs a scope the key lacks answers 403 insufficient_scope and names the scope it wanted.
| Scope | Grants |
|---|---|
listings:read | Read your own listings and their reviews; read the category and geography reference data. |
listings:write | Create and edit your listings; upload a logo and photos; delete a photo. |
companies:read | Read any active company's public card and its rating statistics. |
reviews:read | Read any active company's reviews. |
Rate limits & quotas
| Limit | Default | What you get when you cross it |
|---|---|---|
| Requests per minute, per key | 60 | 429 rate_limited with retry_after and a Retry-After header |
| Requests per minute, per IP | 120 (burst 60) | 429 rate_limited from the edge, Retry-After: 60 |
| Listings created per day, per key | 200 | 429 daily_limit_reached |
| Company reads per day, per key | 1000 | 429 daily_read_limit_reached |
Photos per /photos call | 10 URLs | rejected with 422 validation_failed |
| Owner photos in one gallery | 12 | extra URLs come back in photos_rejected as gallery_full |
Domains per /companies/lookup | 100 | 422 validation_failed |
| Review paging depth | page × per_page ≤ 10000 | 422 validation_failed |
| Categories per listing | 3 | 422 validation_failed |
Every response carries the current state of your budget:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key. |
X-RateLimit-Remaining | Requests left in the current 60-second window, after this one. |
X-Daily-Reads-Limit | Company reads allowed per UTC day. 0 means unlimited. |
X-Daily-Reads-Remaining | Company reads left today, after this one. |
Retry-After | Seconds to wait. Sent with 429. |
What actually costs quota
- Days are UTC days, not calendar days in your timezone.
- Only successful calls count. A validation failure, a
404, a409on a taken domain and adry_runcreate are all free. - One
POST /companies/lookupcosts one read no matter how many of the 100 domains it carries. That is why it returns the compact card. GET /companies/{id},GET /companies?domain=andGET /companies/{id}/statssendETagandCache-Control: private, max-age=300. Store the ETag, send it back asIf-None-Match, and a304costs you nothing.- Reading your own listings and their reviews does not touch the company-read quota at all.
W/"…". Send back whatever you received — both forms are accepted.Errors
Every failure under /api/v1/* uses one envelope. code is stable and safe to branch on; message is for a human reading a log.
{
"error": {
"code": "validation_failed",
"message": "request validation failed",
"fields": {
"name": "must be 2 to 150 characters",
"website": "required"
}
}
}
| Code | HTTP | Means |
|---|---|---|
unauthorized | 401 | No key, a malformed key, or a key that is unknown, revoked, or belongs to a disabled account. |
insufficient_scope | 403 | Valid key, wrong scope. The response carries required_scope. |
forbidden | 403 | Blocked account or IP. |
not_found | 404 | Unknown endpoint, or a resource this key cannot see. Listings you do not own answer 404 on purpose, so ids cannot be probed. |
method_not_allowed | 405 | The path exists, but not for that HTTP method. |
domain_taken | 409 | Somebody else already has a listing on that domain. The response names only its id and public URL. |
bad_request | 400 | The request could not be understood. |
invalid_json | 400 | The body was sent as JSON but is not valid JSON. |
validation_failed | 422 | One or more fields rejected. fields maps field name to reason — all bad fields at once, not one per request. |
domain_unreachable | 422 | The domain does not answer over HTTP(S) on a public address. |
content_rejected | 422 | The site does not qualify for a listing (parked, empty, unreadable). reason says which. |
no_agency_credits | 402 | No agency listing-years left. The response carries checkout and the full pricing ladder. |
upgrade_conflict | 409 | Two upgrades for the same account collided. Safe to retry. |
checkout_unavailable | 502 | The payment session could not be created. Retry. |
unknown_format | 422 | The uploaded review export matched none of the known column layouts. The response carries the file’s real headers; send them back as mapping. |
payload_too_large | 413 | A review export over 10 MB, or more than 5,000 rows in one transfer. |
transfer_not_confirmable | 409 | That transfer is not awaiting confirmation — usually it was already confirmed. |
nothing_to_import | 409 | Every row in the transfer was a duplicate or was rejected. |
transfer_not_undoable | 409 | Only a completed transfer can be withdrawn. |
undo_window_closed | 409 | The 30-day self-service undo window has passed. Contact support. |
transfer_daily_limit | 429 | That listing has already opened its three review transfers in the last 24 hours. |
google_not_connected | 409 | You sent mode:"google" for a listing with no usable Google Business Profile connection. Only the owner can make one, in their cabinet — OAuth needs a browser. configured in the response says whether our own Google access is live yet; the message is written to be shown to them. |
google_no_location | 409 | The owner connected Google but has not yet said which of their Google locations this listing is. |
url_not_supported_for_source | 422 | You sent mode:"url" for a platform we cannot fetch. The message is that platform’s own reason; url_capable_sources lists the ones that do work. |
import_failed | 502 | The review index refused every write. Nothing was imported; quote the transfer id to support. |
review_index_unavailable | 503 | The review index was unreachable. Nothing changed; retry. |
rate_limited | 429 | Too many requests this minute — from your key's window or from the edge. |
daily_limit_reached | 429 | This key has created its allowance of listings for the current UTC day. |
daily_read_limit_reached | 429 | This key has spent its company-read allowance for the current UTC day. |
create_failed | 500 | The listing passed every check but could not be written. |
internal_error | 500 | Something failed on our side. Nothing about the failure is exposed. |
error | 4xx/5xx | Fallback for a status with no more specific code. |
Endpoints
Every example below assumes your key is in $TB_KEY. Responses are abridged where a full list would run for pages; the complete shapes are in the API reference.
Account
/v1/meany scopeConfirms the key is live and reports the account it belongs to, the key's scopes, and every quota with today's usage already counted. The cheapest health check there is.
curl -s https://trustburn.com/api/v1/me \
-H "Authorization: Bearer $TB_KEY"{
"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"
}Reference data
Closed vocabularies. A listing's categories, country, state_id and city_id must come from here.
/v1/categorieslistings:readThe whole category taxonomy, sorted by slug. slug is the stable identifier and the one that appears in public URLs.
curl -s https://trustburn.com/api/v1/categories \
-H "Authorization: Bearer $TB_KEY"[
{ "slug": "accounting", "name": "Accounting" },
{ "slug": "airlines-aviation", "name": "Airlines/aviation" },
{ "slug": "seo-services", "name": "SEO Services" }
]/v1/geo/countrieslistings:readISO 3166-1 alpha-2 codes accepted by a listing's country.
curl -s https://trustburn.com/api/v1/geo/countries \
-H "Authorization: Bearer $TB_KEY"[
{ "code": "AF", "name": "Afghanistan" },
{ "code": "UA", "name": "Ukraine" },
{ "code": "US", "name": "United States" }
]/v1/geo/states?country=USlistings:readcurl -s "https://trustburn.com/api/v1/geo/states?country=US" \
-H "Authorization: Bearer $TB_KEY"[
{ "id": 1399, "name": "Delaware" },
{ "id": 1400, "name": "Alaska" }
]/v1/geo/cities?country=&state=&q=listings:readCities with a population over 10,000 — the same list the business cabinet offers, on purpose: a listing placed in a city outside that list could not afterwards be edited in the cabinet without moving it. q filters by name prefix and caps the result at 50.
curl -s "https://trustburn.com/api/v1/geo/cities?country=US&state=1407&q=den" \
-H "Authorization: Bearer $TB_KEY"[
{ "id": 115237, "name": "Denison" },
{ "id": 115247, "name": "Denton" }
]Listings
Listings are the business profiles your account owns. A listing you do not own answers 404.
/v1/listingslistings:readYour listings, newest first. page (default 1) and per_page (default 50, max 100); out-of-range values are clamped, not rejected.
curl -s "https://trustburn.com/api/v1/listings?per_page=2" \
-H "Authorization: Bearer $TB_KEY"{
"items": [
{
"id": 25594885,
"name": "Hafisa Magezi Foundation",
"website": "https://hafisamagezi.org",
"domain": "hafisamagezi.org",
"public_url": "https://trustburn.com/reviews/hafisamagezi-org",
"phone": "+256 393 250 400",
"email": "[email protected]",
"address": "Kyebando, Erisa, Kawempe Division, Kampala, 214661, UG",
"country": "UG",
"state": { "id": 396, "name": "Central Region" },
"city": { "id": 110903, "name": "Kampala" },
"categories": [
{ "slug": "philanthropy", "name": "Philanthropy" }
],
"description": "…",
"logo_url": "https://trustburn.com/api/images/company-pic/company-pic-….png",
"photos": [
{ "id": 759204, "url": "https://trustburn.com/api/images/company-review-pic/web-….jpg" }
],
"status": "active",
"owner_verified": null,
"rating": { "avg": 0, "count": 0 },
"created_at": "2026-09-07T16:35:29Z",
"updated_at": null
}
],
"page": 1,
"per_page": 2,
"total": 11
}updated_at is always null today. The field is in the response so your parser will not have to change when it starts carrying a value./v1/listingslistings:writeCreates a listing and answers 201 with the full card. Required: name, website, phone, email, country. Optional: address, description, state_id/state, city_id/city, categories (up to 3 slugs), logo_url, photos (up to 10 URLs).
Idempotent by domain. If your account already owns a listing on that domain, nothing is created — the existing card comes back with 200 and "existing": true. Retrying a create whose response you lost is therefore safe. If somebody else owns the domain you get 409 domain_taken.
Order of checks: field validation → domain already listed → daily quota → domain reachable → content gate → write. The cheap checks run first, so a stream of malformed bodies costs no outbound requests.
"dry_run": true runs every check and returns the normalized fields under would_create without writing anything and without spending quota.
curl -s -X POST https://trustburn.com/api/v1/listings \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Northwind Locks",
"website": "https://northwindlocks.com",
"phone": "+1 415 555 0134",
"email": "[email protected]",
"country": "US",
"city": "San Francisco",
"categories": ["security-and-investigations"],
"dry_run": true
}'{
"ok": true,
"would_create": {
"name": "Northwind Locks",
"website": "https://northwindlocks.com",
"domain": "northwindlocks.com",
"phone": "+1 415 555 0134",
"email": "[email protected]",
"address": "",
"country": "US",
"state": { "id": 1416, "name": "California" },
"city": { "id": 122543, "name": "San Francisco" },
"categories": [
{ "slug": "security-and-investigations", "name": "Security and investigations" }
],
"description": "",
"logo_url": null,
"photos": []
}
}/v1/listings/{id}listings:readcurl -s https://trustburn.com/api/v1/listings/25594885 \
-H "Authorization: Bearer $TB_KEY"/v1/listings/{id}listings:writePartial update: omitted fields keep their current values. The current state is loaded first and your body applied on top, so categories, social links and opening hours survive a partial PATCH.
website cannot be changed — the domain is the listing's identity; sending it answers 422. Changing country clears state_id/city_id unless the same request supplies new ones.
curl -s -X PATCH https://trustburn.com/api/v1/listings/25594885 \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "Licensed 24/7 locksmith serving the Bay Area."}'/v1/listings/{id}/logolistings:writeEither a file (multipart/form-data, field file) or a URL ({"url": "…"}). Same format, size and blank-image checks as the cabinet upload; resized to 200×200. An existing logo is replaced. Returns the updated card.
curl -s -X POST https://trustburn.com/api/v1/listings/25594885/logo \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://northwindlocks.com/logo.png"}'
# or a file:
curl -s -X POST https://trustburn.com/api/v1/listings/25594885/logo \
-H "Authorization: Bearer $TB_KEY" \
-F "[email protected]"/v1/listings/{id}/photoslistings:writeEither one uploaded file or up to 10 URLs per call. The gallery holds 12 owner photos in total. Photos that fail a check are reported in photos_rejected on the returned card — the call itself still succeeds.
curl -s -X POST https://trustburn.com/api/v1/listings/25594885/photos \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://northwindlocks.com/img/van.jpg"]}'{
"id": 25594885,
"…": "…",
"photos_rejected": [
{ "url": "https://northwindlocks.com/img/van.jpg", "reason": "gallery_full" }
]
}/v1/listings/{id}/photos/{photoId}listings:writephotoId is the id from the card's photos[].
curl -s -X DELETE https://trustburn.com/api/v1/listings/25594885/photos/759204 \
-H "Authorization: Bearer $TB_KEY"{ "ok": true, "deleted": { "id": 759204 } }/v1/listings/{id}/reviewslistings:readReviews of a listing you own. Needs only listings:read and does not spend the company-read quota. per_page default 20, max 100. For owner replies, helpful counts, sorting and rating filters use GET /v1/companies/{id}/reviews.
curl -s "https://trustburn.com/api/v1/listings/25594885/reviews?per_page=2" \
-H "Authorization: Bearer $TB_KEY"{
"items": [
{
"id": "ok9OJ5EBI73yNbb31zYq",
"rating": 5,
"title": "Fast and fair",
"text": "Out at 2am, done in twenty minutes.",
"author_name": "abhilash",
"created_at": "2024-08-06T10:49:23Z"
}
],
"page": 1,
"per_page": 2,
"summary": { "avg": 5, "count": 1 },
"total": 1
}Companies & reviews
Read-only access to any active Trustburn company — not only yours. These are the calls that spend the daily read quota.
/v1/companies/{id}companies:readThe public card: the same facts an anonymous visitor sees on the company page. A company with no public page answers 404, same as an unknown id.
The company's contact email is deliberately not included. It is visible on the page, one at a time; a hundred of them in one JSON response is a different thing.
verified means ownership was really proved — the same test that draws the Verified badge. A claim that is only pending shows up as claimed, not verified.
curl -s https://trustburn.com/api/v1/companies/6 \
-H "Authorization: Bearer $TB_KEY"{
"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"
}
}/v1/companies?domain=companies:readSame card, found by domain. The domain is normalized exactly the way listing creation normalizes it, so https://WWW.Example.com/path and example.com find the same card. When several active cards share a domain the lowest id wins and duplicates reports how many were found.
curl -s "https://trustburn.com/api/v1/companies?domain=semalt.com" \
-H "Authorization: Bearer $TB_KEY" \
-H 'If-None-Match: "9d816d67ea958f9a5348271e022c6b88"'/v1/companies/lookupcompanies:readUp to 100 domains in one call, costing one unit of the daily read quota — which is why it returns the compact card and not the full one. For the full card follow up with GET /v1/companies/{id}, one unit each.
The response mirrors the request: one entry per submitted domain, in the order sent, with company: null where nothing was found.
curl -s -X POST https://trustburn.com/api/v1/companies/lookup \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["semalt.com", "example-not-here-xyz.com"]}'{
"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 }
]
}/v1/companies/{id}/reviewsreviews:readQuery: page, per_page (default 20, max 50 — larger values are clamped, not rejected), sort (newest, oldest, highest, lowest), min_rating and max_rating (1–5).
Paging is capped at page × per_page ≤ 10000; deeper answers 422. Narrow with the rating filters instead.
verified on a review means invited by the business — the author came in through the company's own review invitation. There is no other verification signal on a review. total respects the rating filters; summary is always the company's whole review body.
curl -s "https://trustburn.com/api/v1/companies/6/reviews?per_page=2&sort=newest" \
-H "Authorization: Bearer $TB_KEY"{
"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"
}
}/v1/companies/{id}/statscompanies:readRating, star distribution, a 12-month series and the newest review's timestamp — one call instead of paging the whole review body. Months with no reviews are present with zeros.
curl -s https://trustburn.com/api/v1/companies/6/stats \
-H "Authorization: Bearer $TB_KEY"{
"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"
}Agency upgrades
Agencies that manage many listings buy listing-years in bulk. One listing-year gives one listing, for one year:
- a dofollow link to the business’s website on its public Trustburn page — a real link, not
rel="nofollow"; - the Verified badge, with no verification step.
Nothing else changes: the listing keeps every free-plan capability and gains none of the Growth or Pro features.
What it costs
| Package | Per listing-year | Example |
|---|---|---|
| 25–99 | $15 | 25 × $15 = $375 |
| 100–499 | $12 | 100 × $12 = $1,200 |
| 500 and up | $9 | 500 × $9 = $4,500 |
The minimum order is 25. The whole order is billed at the price of the band it falls into — there is no mixing of bands. Prices are in US dollars, and the ladder is also served live in every pricing object below, so you never have to hard-code these numbers.
Units belong to the account, not to a company: buy once, spend them across every listing you manage, whenever you like. They do not expire on the shelf — the year starts when you spend one. There are no refunds, and a spent unit cannot be moved to another listing. Paying by invoice, or buying more than 500 at a time? Talk to sales — the same units are booked to your account by hand.
How it works
POST /v1/agency/checkoutreturns a Stripe Checkout URL. Send the buyer there.- When Stripe confirms the payment, the units land on the account balance. Nothing is credited before that.
POST /v1/listings/{id}/upgradespends one unit on one listing.- Call it again a year later — or any time — to renew: another unit, another year added to the current end date.
GET /v1/agency/balancetells you what is left, at any time.
You can also buy from the cabinet at /business/api, where the same balance is shown with a Buy button — useful when the person holding the credit card is not the person holding the key.
/v1/agency/balancelistings:readWhat the account has left, what it has spent, every package it bought, and the live price ladder. The balance is never a stored number — it is always the sum of units_total - units_used over the packages, so it cannot drift.
upgrades_active counts distinct listings with an unexpired agency year: a renewed listing is one listing, not two.
curl -s https://trustburn.com/api/v1/agency/balance \
-H "Authorization: Bearer $TB_KEY"{
"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"
}
}/v1/agency/checkoutlistings:writeCreates a Stripe Checkout Session for units listing-years and answers 201 with its URL. Nothing is charged and nothing is credited here — the balance moves only when Stripe confirms the payment.
The price is decided on our side from the ladder; it is never read from the request. success_url and cancel_url are optional and must be absolute https:// URLs — omitted, the buyer returns to your Trustburn cabinet.
Below 25 units the call answers 422 and includes the ladder, so a client can correct the order without a second request. No idempotency key is needed: an unpaid session costs nothing and expires by itself.
curl -s -X POST https://trustburn.com/api/v1/agency/checkout \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{"units": 100}'{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_a1b2c3",
"session_id": "cs_live_a1b2c3",
"units": 100,
"unit_price": 12,
"total": 1200
}/v1/listings/{id}/upgradelistings:writeSpends one unit on one listing: dofollow link and Verified badge for a year. No body.
Calling it again renews — another unit, another year added to the current end date, not to today, so renewing early loses nothing. A listing you do not own answers 404, the same as an unknown id.
A repeat within a few seconds is treated as a double-click: nothing changes, nothing is spent, and the answer carries "charged": false.
With an empty balance the call answers 402 no_agency_credits and hands you the checkout path and the ladder.
curl -s -X POST https://trustburn.com/api/v1/listings/25594885/upgrade \
-H "Authorization: Bearer $TB_KEY"{
"listing_id": 25594885,
"tier": "agency",
"dofollow": true,
"verified": true,
"ends_at": "2027-09-08",
"units_available": 24,
"charged": true
}When the balance is empty
{
"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"
}
}
}
Every listing card — GET /v1/listings and GET /v1/listings/{id} — also carries tier, dofollow and agency_ends_at, so you can see which of your listings are covered and which are about to run out without keeping your own ledger.
Review transfers
Move reviews a business already collected on Clutch, Trustpilot, G2, Capterra or Google onto its Trustburn profile — at that business’s request. Two steps: open a transfer and read the preview, then confirm it. Nothing reaches the profile before you confirm, and one call takes it all back.
You can also just give us the link. For platforms whose url_mode is true — today TechBehemoths and Superbcompanies — post {source, source_url, mode:"url"} and we read that public profile page ourselves: our own user agent, one request per second, no headless browser, and only ever a URL whose host belongs to the platform you named. You get 202 and an empty transfer to poll; a minute later it is a normal preview. See Fetching by URL.
Or type them in. Send {source, rows, mode:"manual"} where every row carries its own review_url — the link to that exact review on the source platform. That per-row link is required, becomes each imported review’s badge link, and makes the profile-level source_url optional. It answers 201 with a preview, exactly like sending rows without a mode.
Google has a connector. Not a scraper — the official Business Profile API, over OAuth. The owner authorises it in their cabinet, in a browser, with their own Google account; there is no endpoint here that starts that and there will not be one. Once they have, post {source:"google", mode:"google"} with no source_url and no rows and we read their Google reviews directly. Without it you get 409 google_not_connected carrying a sentence you can show them as it stands. Check connector and configured on the google row of GET /v1/reviews/transfers/sources before you offer the button. See Google.
Transferred reviews count. A transferred review is a full review here: it counts towards the rating and the review counter and sits in the feed under its original date. The one difference is a badge on each one — “Transferred from Clutch at the owner’s request” — linking to the profile you gave as source_url. The company page also carries one honest line: “Includes 12 reviews transferred from Clutch at the owner’s request.” They are deliberately left out of the page’s Review structured data, because Google’s review-snippet policy forbids marking up reviews collected from other sites.
Before you call this. The business must have asked for the transfer, and these must be genuine customer reviews of it. Every transfer is recorded against the key that opened it, and we can withdraw one — or revoke a key.
Limits and rules
source_url | Mandatory on every transfer, and it must be the company’s profile on the source platform — not a search page, not your site. It is the badge link, and the only way a reader can check the claim. |
| Size | 5,000 rows and 10 MB per transfer. |
| Frequency | 3 transfers per listing per rolling 24 hours, shared with the owner’s own cabinet. A transfer left unconfirmed still spends its slot. This does not spend a listings_per_day unit. |
| Undo | 30 days from the import, self-service. After that, support. |
| Rows we drop | Rating outside 1–5 after normalisation, body under 20 characters, no usable date, a date in the future or over 10 years old, contact details or links in the body, and duplicates — inside the file and against everything previously transferred for this company. |
| Ratings | Normalised to 1–5. 5, 4.0, 4,5, 5/5, 9/10, 8 out of 10 and 4 stars all parse; a 10-point scale is rescaled, so a 9/10 lands as 5 stars rather than an unexplained 9. |
| Dates | The original review date is kept, so the feed stays in honest order. |
| Files | CSV (comma, semicolon or tab, with or without BOM) and JSON. No XLSX — save as CSV. Trustpilot, G2 and Capterra exports are recognised by their header row, and so is our own template. |
The flow
POST /v1/listings/{id}/reviews/transferswithrows, with afile, or withmode: "url"and nothing else. Rows and files come back201with a transfer already inpreview; a URL comes back202with an empty one and you poll it.- Show that to the owner.
GET .../transfers/{tid}pages through the parsed rows if you want the full preview table. POST .../transfers/{tid}/confirmwrites them and returns the recomputed rating.DELETE .../transfers/{tid}withdraws the whole transfer within 30 days.
/v1/reviews/transfers/sourceslistings:readThe closed list of platforms a transfer may name. code is what you send as source; label is the word that appears in the badge sentence. url_mode says whether mode:"url" works for it, and reason is a sentence you can show a user unchanged — why not, or what a fetch will and will not get. Read these rather than hard-coding a list; they change when a platform’s behaviour changes.
connector marks a platform we reach through its own API instead of by reading a page — today only Google — and configured says whether ours is live. connector: true, configured: false is Google before its API access is granted: mode:"google" answers 409 and the CSV template is the way through.
curl -s https://trustburn.com/api/v1/reviews/transfers/sources \
-H "Authorization: Bearer $TB_KEY"[
{
"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
}
]/v1/reviews/transfers/template.csvlistings:readOur CSV template: the canonical header row and two filled example rows. Answers text/csv as an attachment, not JSON. Use it when the export you have is none of the recognised ones, or when the owner would rather not connect Google.
curl -s https://trustburn.com/api/v1/reviews/transfers/template.csv \
-H "Authorization: Bearer $TB_KEY" \
-o template.csvauthor_name,rating,date,title,text,review_url,source_review_id
"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/v1/listings/{id}/reviews/transferslistings:writeParses, validates and de-duplicates, then answers 201 with a transfer in preview. Writes nothing to the profile. (With mode:"url" it answers 202 with an empty transfer instead — see Fetching by URL.)
Send rows as JSON, or a file as multipart/form-data with the same scalar fields. A body carrying both uses the file. Headers we do not recognise answer 422 unknown_format with the file’s real headers — send them back as a mapping field and the same file parses.
For reviews typed by hand rather than exported, send mode:"manual" with rows that each carry their own review_url; that link is required per row and becomes the review’s badge link, so source_url is optional in this mode.
Rows we will not import are not an error: they come back in rejected_rows with the row number, the reason and an excerpt, so the owner can see exactly which ones.
curl -s -X POST https://trustburn.com/api/v1/listings/25230012/reviews/transfers \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{
"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.",
"source_review_id": "12345"
}
]
}'{
"transfer": {
"id": 5,
"status": "preview",
"channel": "api",
"source": "clutch",
"source_label": "Clutch",
"source_url": "https://clutch.co/profile/northwind-locks",
"detected_format": null,
"total": 4,
"pending": 2,
"imported": 0,
"duplicates": 1,
"rejected": 1,
"created_at": "2026-09-09T09:25:45Z",
"rejected_rows": [
{ "row_no": 2, "status": "duplicate", "reason": "duplicate_in_file", "excerpt": "We hired them for a six-month rebuild..." },
{ "row_no": 3, "status": "rejected", "reason": "contact_details_in_text", "excerpt": "Solid delivery overall. Reach them on +1 415..." }
]
}
}/v1/listings/{id}/reviews/transferslistings:writeThe same endpoint with a file. This is the shape for a Trustpilot, G2 or Capterra export, or for our template filled in by hand.
The upload ceiling on this path is 10 MB. A body over 12 MB is refused at the edge with the same payload_too_large code before it reaches us.
curl -s -X POST https://trustburn.com/api/v1/listings/25230012/reviews/transfers \
-H "Authorization: Bearer $TB_KEY" \
-F "source=trustpilot" \
-F "source_url=https://www.trustpilot.com/review/northwindlocks.com" \
-F "[email protected]"{
"transfer": {
"id": 6,
"status": "preview",
"source": "trustpilot",
"source_label": "Trustpilot",
"detected_format": "trustpilot",
"file_name": "trustpilot-export.csv",
"total": 128,
"pending": 121,
"duplicates": 5,
"rejected": 2,
"rejected_rows": [ ... ]
}
}/v1/listings/{id}/reviews/transferslistings:readThis listing’s transfer history, newest first, at most 50 — from every channel, so a transfer the owner started in their own cabinet is here too. transfers_today is the daily allowance and what is left of it; transferred_reviews counts what is live on the profile right now.
curl -s https://trustburn.com/api/v1/listings/25230012/reviews/transfers \
-H "Authorization: Bearer $TB_KEY"{
"items": [
{
"id": 5,
"status": "done",
"channel": "api",
"source": "clutch",
"source_label": "Clutch",
"total": 4,
"pending": 0,
"imported": 2,
"duplicates": 1,
"rejected": 1,
"imported_at": "2026-09-09T09:25:55Z"
}
],
"total": 1,
"transfers_today": { "used": 1, "max": 3 },
"transferred_reviews": { "count": 2, "sources": { "clutch": 2 } }
}/v1/listings/{id}/reviews/transfers/{tid}listings:readOne transfer plus a page of the rows we parsed, every status — the preview table. limit up to 200, offset to page. review_id on an imported row is the same id GET /v1/listings/{id}/reviews reports.
curl -s "https://trustburn.com/api/v1/listings/25230012/reviews/transfers/5?limit=50" \
-H "Authorization: Bearer $TB_KEY"{
"transfer": { "id": 5, "status": "done", "total": 4, "pending": 0, "imported": 2 },
"items": [
{
"id": 4909,
"row_no": 1,
"status": "imported",
"reject_reason": null,
"author_name": "Jane Cooper",
"rating": 5,
"title": "Delivered exactly what they promised",
"date": "2025-03-14",
"review_id": "h9J8haABm5K95aQI-vC_"
},
{
"id": 4911,
"row_no": 3,
"status": "rejected",
"reject_reason": "contact_details_in_text",
"author_name": "Marcus Hall",
"rating": 4,
"date": "2024-11-02",
"review_id": null
}
],
"limit": 50,
"offset": 0,
"total": 4
}/v1/listings/{id}/reviews/transfers/{tid}/confirmlistings:writeWrites every pending row into the live review index and recomputes the rating on the spot. Only a transfer in preview can be confirmed; a second call answers 409 transfer_not_confirmable.
By calling this you state that the business authorised the transfer and that these are genuine customer reviews of it.
This is a synchronous write: on a 5,000-row transfer it takes about half a minute. Give your client at least a 180-second timeout.
curl -s -X POST https://trustburn.com/api/v1/listings/25230012/reviews/transfers/5/confirm \
-H "Authorization: Bearer $TB_KEY"{
"transfer": {
"id": 5,
"status": "done",
"source": "clutch",
"source_label": "Clutch",
"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 }
}
}/v1/listings/{id}/reviews/transfers/{tid}listings:writeWithdraws a completed transfer: every review it wrote leaves the profile, the widgets and the rating, and the counters are recomputed. The transfer itself is not deleted — it moves to undone and stays in the ledger.
Self-service for 30 days after the import. After that, contact support.
curl -s -X DELETE https://trustburn.com/api/v1/listings/25230012/reviews/transfers/5 \
-H "Authorization: Bearer $TB_KEY"{
"transfer": {
"id": 5,
"status": "undone",
"total": 4,
"imported": 2,
"removed": 2,
"undone_at": "2026-09-09T09:29:48Z",
"company": { "id": 25230012, "avg_rate": 4.6, "reviews_cnt": 29 }
}
}Fetching by URL
Send the same POST with mode set to url and no rows and no file:
curl -s -X POST https://trustburn.com/api/v1/listings/25230012/reviews/transfers \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{"source":"techbehemoths","source_url":"https://techbehemoths.com/company/northwind-locks","mode":"url"}'
HTTP/1.1 202 Accepted
{ "transfer": { "id": 12, "status": "uploaded", "mode": "url", "total": 0, "fetch_note": null, "error": null } }
202, not 201, and the difference is the contract: the transfer exists and is empty. Nothing has been fetched, parsed or validated. Poll GET .../transfers/{tid} — every few seconds is plenty, it normally takes 10–60 seconds — until status leaves uploaded/parsing:
preview | Done. items, the counters and rejected_rows are what they would be for an uploaded file — the same validation, the same duplicate rules. Confirm as usual. |
failed | Read error. It is written for a person: a profile page that does not exist, a platform that refused us, a layout we could not read. A failed fetch does not spend one of the three daily slots. |
Read fetch_note and show it. It says what the fetch could not see. A TechBehemoths profile that advertises 18 reviews but only publishes 12 in its HTML — the rest arrive via their own JavaScript — says exactly that, and the owner can add the remainder from the CSV template. Without it, a partial import looks like a bug.
| Cap | 1,000 reviews and 20 pages per fetch (a file may carry 5,000). |
source_url | Must be a profile page on the platform named in source. A host that does not match is 422 on source_url — which is also what stops this endpoint from being asked to fetch arbitrary addresses. |
| Attempts | Up to three, with backoff, and only for failures worth retrying (a timeout, a 5xx). A challenge page, a 403 or a 404 is final on the first try. |
| Duplicates | A second fetch of the same profile lands as duplicates against the first, not as a second copy. |
| Not supported | 422 url_not_supported_for_source, carrying the platform’s own reason and the current url_capable_sources list. |
Google, and the platforms we cannot fetch
Google reviews come across three ways: as rows, as a file from our CSV template, or through the Google Business Profile connector — the official API, over OAuth, with the owner’s consent. We have never scraped Google and will not: its review endpoints are closed, reading them breaks its terms, and the old scraper we had is staying switched off.
The owner connects it, not you. OAuth needs a browser and a Google account that administers the Business Profile. They open their listing’s Import reviews page, press Connect, sign in to Google, and pick which of their locations this listing is. Your side of it is one field in one response — the google row of GET /v1/reviews/transfers/sources:
{ "code": "google", "label": "Google", "url_mode": false,
"connector": true, "configured": 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 is permanent; configured flips to true the day our own Google API access is live, with no version bump and no change to anything you send. Show reason as it stands rather than writing your own sentence about it.
Once the owner has connected, the call carries nothing but the mode:
curl -s -X POST https://trustburn.com/api/v1/listings/25230012/reviews/transfers \
-H "Authorization: Bearer $TB_KEY" \
-H "Content-Type: application/json" \
-d '{"source":"google","mode":"google"}'
HTTP/1.1 202 Accepted
{ "transfer": { "id": 21, "status": "uploaded", "mode": "google", "source": "google", "total": 0, "error": null } }
No source_url and no rows: the profile link every transferred review has to carry is Google’s own link for the location they picked, read off the connection rather than taken from you — a link we never verified is not one we will publish under a badge. From here it is the mode:"url" flow exactly: poll GET .../transfers/{tid} until status leaves uploaded/parsing, show the preview, confirm. It spends one of the three daily slots and is undoable for 30 days like any other transfer.
409 google_not_connected | No usable connection on that listing. configured: false in the response means our end is not live yet; true means it is and this owner simply has not connected. Either way the fix is theirs, in a browser — show them message and link them to their Import reviews page. |
409 google_no_location | Connected, but they have not chosen which Google location this listing is. One dropdown on the same page. |
409 google_no_profile_url | Google gave us no public link for that location. Rare; they should reconnect and pick it again. |
| Cap | 1,000 reviews per fetch, the same as a URL fetch. Owner replies on Google are not imported — they can answer here instead. |
Trustpilot, Clutch and DesignRush answer our reader with a bot challenge. We probed all three from our own address with the user agent above and got a challenge page every time. Getting past one would mean a spoofed browser, a proxy pool or a headless browser, and we do none of those — so for those three the answer is a file, permanently, not “yet”. Trustpilot and G2 both give the business a full export in its own vendor portal, which carries more than the public page shows anyway.
Do not hard-code any of this. GET /v1/reviews/transfers/sources carries url_mode and a reason per platform, and the reason is a sentence you can put in front of a user unchanged. It changes when a platform’s behaviour changes, without an API version bump.
Attribution
Wherever you display Trustburn review data — a rating, a star count, a review body, an aggregate — you must credit Trustburn and link back to that company's Trustburn page.
You do not have to invent the wording. Every company card and every review page hands it to you:
"attribution": {
"text": "Reviews via Trustburn",
"url": "https://trustburn.com/reviews/semalt"
}
Render it as a visible link near the data it belongs to:
<a href="https://trustburn.com/reviews/semalt">Reviews via Trustburn</a>
The link must be crawlable and clickable — not an image, not a tooltip, not nofollow-only text with no href.
Terms
- Your key is yours. Do not share it, embed it in a mobile app, or ship it in front-end JavaScript. Anything done with your key is done as you.
- No bulk extraction. The API is for serving your own product and your own listings, not for copying the Trustburn corpus. Systematically pulling companies or reviews you have no relationship with — mirroring, dataset building, model training — is not allowed, whatever the request rate.
- No reselling. Trustburn data may not be sold, sublicensed, or redistributed as a data product.
- Attribution is required wherever the data is shown. See above.
- Do not misrepresent. Do not alter review text or ratings, filter them to hide the negative ones from an aggregate you present as complete, or imply Trustburn endorses you.
- Listings must be real. Create profiles for businesses that exist, with contact details that reach them.
- We can revoke a key that breaks these rules, and we can change the quotas above. Material changes will be announced in the changelog.
These are the API-specific rules. The full agreement is the Terms of Service; where the two disagree, the Terms of Service win.
Changelog
| Date | Change |
|---|---|
| 2026-09-09 | Google Business Profile connector: mode:"google" on POST /v1/listings/{id}/reviews/transfers reads a listing’s Google reviews through Google’s own API once its owner has connected it in the cabinet (OAuth needs a browser, so there is no API path to that). Answers 202 and polls like mode:"url"; 409 google_not_connected otherwise. GET /v1/reviews/transfers/sources gained connector and configured. |
| 2026-09-09 | Review transfers: POST /v1/listings/{id}/reviews/transfers (rows or file), /confirm, /undo, the history and detail reads, plus GET /v1/reviews/transfers/sources and the CSV template. Transferred reviews count towards the rating and carry a badge linking to the source profile; source_url is mandatory, 3 transfers per listing per day, undo within 30 days. |
| 2026-09-08 | Agency listing-years: GET /v1/agency/balance, POST /v1/agency/checkout and POST /v1/listings/{id}/upgrade, with the volume ladder and the 402 no_agency_credits error. Listing cards gained tier, dofollow and agency_ends_at; the balance and a Buy button are in the cabinet at /business/api. |
| 2026-09-08 | Public company reads: GET /v1/companies/{id}, GET /v1/companies?domain=, POST /v1/companies/lookup, GET /v1/companies/{id}/reviews, GET /v1/companies/{id}/stats. New scopes companies:read and reviews:read, the daily read quota with its X-Daily-Reads-* headers, and ETag/If-None-Match on the cacheable reads. Self-service keys at /business/api, this page, and the OpenAPI 3.1 spec. |
| 2026-09-07 | v1 opened: GET /v1/me, the reference data, and the full listing surface — create, read, update, logo, photos, and reviews of your own listings. Scopes listings:read and listings:write, the per-minute limit and the daily listing quota. |
Something missing?
If an endpoint you need is not here, or one behaves differently from this page, tell us — contact support and mention the endpoint and the key prefix (never the key itself).