curl --request GET \
--url https://app.reputably.net/api/entities/Report/{id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.reputably.net/api/entities/Report/{id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.reputably.net/api/entities/Report/{id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.reputably.net/api/entities/Report/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.reputably.net/api/entities/Report/{id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.reputably.net/api/entities/Report/{id}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.reputably.net/api/entities/Report/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"id": "<string>",
"workspace_id": "<string>",
"template": "<string>",
"period_start": "<string>",
"period_end": "<string>",
"status": "<string>",
"pdf_url": "<string>",
"error_message": "<string>",
"started_at": "<string>",
"completed_at": "<string>",
"render_token": "<string>",
"report_data": {},
"business_location_id": "<string>",
"source": "<string>",
"scheduled_report_id": "<string>",
"period_label": "<string>",
"public_share_id": "<string>",
"is_public": true,
"share_view_count": 123,
"share_created_at": "<string>"
}Get one report
Reads a single report by id. Ids do not cross workspaces.
curl --request GET \
--url https://app.reputably.net/api/entities/Report/{id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.reputably.net/api/entities/Report/{id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.reputably.net/api/entities/Report/{id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.reputably.net/api/entities/Report/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.reputably.net/api/entities/Report/{id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.reputably.net/api/entities/Report/{id}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.reputably.net/api/entities/Report/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"id": "<string>",
"workspace_id": "<string>",
"template": "<string>",
"period_start": "<string>",
"period_end": "<string>",
"status": "<string>",
"pdf_url": "<string>",
"error_message": "<string>",
"started_at": "<string>",
"completed_at": "<string>",
"render_token": "<string>",
"report_data": {},
"business_location_id": "<string>",
"source": "<string>",
"scheduled_report_id": "<string>",
"period_label": "<string>",
"public_share_id": "<string>",
"is_public": true,
"share_view_count": 123,
"share_created_at": "<string>"
}Authorizations
An agent key (rpk_...) created in Settings → API & MCP, sent as Authorization: Bearer rpk_.... Bearer only: a cookie session can never drive this API. An OAuth 2.1 access token obtained from the same host works identically and lands on the same ceiling.
Headers
Which workspace to read. Omit it and you get the account’s home workspace, which on an agency account is often not where the live businesses are. A workspace this credential cannot read is refused with 403 rather than quietly answered from the default.
Path Parameters
The report id.
Response
The report.
Unique id.
Which template to render. PR-9 ships only 'monthly_reputation_report'; PR-13 adds 'competitor_comparison'. Stored as a string (not enum) so adding new templates doesn't require a migration.
queued | rendering | completed | failed
Relative path under /uploads/reports/<workspace_id>/.pdf. Public — filename includes a UUID so URLs are unguessable.
Truncated render-failure reason. Surfaced to the user inline on the failed row.
Short-lived JWT (~5min) the worker mints for the requested_by user so Puppeteer can authenticate against the SPA preview route. Cleared on completion.
Aggregated data the report renders from. Populated by the worker (server/lib/reportData.js) BEFORE Puppeteer launches, then never touched again — reports are immutable historical artifacts. Top-level keys are namespaced so future PRs extend cleanly: report_data.workspace, .agency, .period, .previousPeriod, .reputationScore, .reviews, .mentions, .responses are PR-10. PR-11 adds .business_location and .insights ({ topThemes, notableReviews, notableMentions, recommendations } — any of which may be null when its LLM call failed). PR-13's competitor template will add .competitors. Treat unknown keys as forward-compat; never assume the full shape.
BusinessLocation this report scopes its Reviews data to. PR-11+ requires this for new reports (enforced at the route, not the schema, so existing PR-10 rows with null values stay valid). Reports without it are labelled 'Workspace Rollup (Historical)' in the UI and cannot be shared.
When source='scheduled', points at the ScheduledReport that triggered this run. Lets the UI link back to the schedule's settings + retry buttons.
PR-14: human-readable period label, computed once at create time via server/lib/periodLabel.js. 'April 2026' for full calendar months, 'Q1 2026' for full calendar quarters, 'Mar 15 – Apr 28, 2026' otherwise. Stored on the row so the Reports list can render without recomputing per row. Older rows (pre-PR-14) have null and the UI falls back to computing from period_start/end on read.
Unguessable URL-safe token (12 chars, ~10²¹ search space). Generated when the agency owner enables sharing; cleared on disable; regenerated on re-enable so old URLs stay 404. Null when not currently shared.
Per-report public-share toggle. Independent of public_share_id presence — a report can have an old share_id with is_public=false (revoked); the public route requires both.
Incremented on each public-page render. Async write — never blocks the response. No IP tracking in PR-11.