Esta guía está disponible en inglés.
How to Build a Multi-Location Marketing Dashboard
Build a multi-location marketing dashboard from the API's rollup endpoints, one call per panel, covering listings, reviews, rank, AI visibility and duplicates.
De un vistazo
- Qué logra esta guía
- Assemble a portfolio dashboard where each panel is one rollup call, and a per-location table joins them.
- APIs utilizadas
- get
/api/v1/listings/summaryObtener un resumen de listados - get
/api/v1/reviews/summaryObtener resumen de reseñas - get
/api/v1/seo/rollupObtener el resumen de posicionamiento de todas las ubicaciones - get
/api/v1/aeo/rollupObtener el resumen AEO de todas las ubicaciones - get
/api/v1/analytics/summaryObtener un resumen de análisis de perfil - get
/api/v1/listings/duplicates/rollupObtener el resumen de listados duplicados - get
/api/v1/connections/summaryObtener un resumen de cuentas conectadas - get
/api/v1/clients/summaryObtener resumen del cliente
- get
- Referencia
- Listados publicadosReviewsSEOAEOAnálisis de perfilDuplicate ListingsConnectionsClients
- Requisitos previos
- An API key
- A client with locations already set up and reporting
- Casos de uso habituales
- One screen for a brand with hundreds of locations
- An agency overview across a client's locations
- A prioritised "fix first" operational view
A multi-location dashboard is a fan-in. Each panel on the screen is one rollup call that already aggregates every location, so you are not looping over locations in the browser, you are placing a handful of summary calls and joining them by location id. Build it panel by panel.
One call per panel
Every domain has a rollup that returns the whole client at once. Wire each to a panel:
| Panel | Call | Gives you |
|---|---|---|
| Listing health | GET /api/v1/listings/summary | Synced-versus-total publishers, issues, a fixFirst list |
| Reviews | GET /api/v1/reviews/summary | Rating, volume, response rate, best and worst locations |
| Local rank | GET /api/v1/seo/rollup | Average rank and top-three percentage per location |
| AI visibility | GET /api/v1/aeo/rollup | Per-engine AEO scores and systemic fixes |
| Profile analytics | GET /api/v1/analytics/summary | Views, calls, directions, a per-location leaderboard |
| Duplicates | GET /api/v1/listings/duplicates/rollup | Potential duplicates by band and publisher |
| Connections | GET /api/v1/connections/summary | How many locations are connected to Google and Facebook |
The single header number for the whole client is GET /api/v1/clients/summary: locations, reviews and rank in
one small response.
Load the header, then the panels
# Header
curl "https://ai.synup.com/api/v1/clients/summary?clientId=CLIENT_ID" \
-H "Authorization: Bearer $SYNUP_API_KEY"
# Panels, in parallel
curl "https://ai.synup.com/api/v1/listings/summary?clientId=CLIENT_ID&perPage=200" -H "Authorization: Bearer $SYNUP_API_KEY"
curl "https://ai.synup.com/api/v1/reviews/summary?clientId=CLIENT_ID" -H "Authorization: Bearer $SYNUP_API_KEY"
curl "https://ai.synup.com/api/v1/seo/rollup?clientId=CLIENT_ID" -H "Authorization: Bearer $SYNUP_API_KEY"
curl "https://ai.synup.com/api/v1/aeo/rollup?clientId=CLIENT_ID" -H "Authorization: Bearer $SYNUP_API_KEY"const API = "https://ai.synup.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.SYNUP_API_KEY}` };
async function get(path) {
const res = await fetch(`${API}${path}`, { headers });
if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
return (await res.json()).data;
}
async function dashboard(clientId) {
const q = `clientId=${clientId}`;
const [header, listings, reviews, seo, aeo] = await Promise.all([
get(`/clients/summary?${q}`),
get(`/listings/summary?${q}&perPage=200`),
get(`/reviews/summary?${q}`),
get(`/seo/rollup?${q}`),
get(`/aeo/rollup?${q}`),
]);
return { header, listings, reviews, seo, aeo };
}import os, requests
from concurrent.futures import ThreadPoolExecutor
API = "https://ai.synup.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SYNUP_API_KEY']}"}
def get(path):
r = requests.get(f"{API}{path}", headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json()["data"]
def dashboard(client_id):
q = f"clientId={client_id}"
paths = [f"/clients/summary?{q}", f"/listings/summary?{q}&perPage=200",
f"/reviews/summary?{q}", f"/seo/rollup?{q}", f"/aeo/rollup?{q}"]
with ThreadPoolExecutor(max_workers=5) as pool:
header, listings, reviews, seo, aeo = pool.map(get, paths)
return {"header": header, "listings": listings, "reviews": reviews, "seo": seo, "aeo": aeo}Join by location id, and lead with what to fix
Each rollup's per-location rows carry the same locationId, so a per-location table is a join across the panels:
listing health, review response rate and average rank in one row. The summaries do the prioritising for you.
getListingsSummary returns an insights.fixFirst list and a headline; getReviewsSummary names the best
and worst locations; getAeoRollup returns systemicFixes that apply across many locations at once. Put those
at the top. A dashboard that opens on "here are the three things to fix" beats one that opens on a wall of green.
Next
To make each customer's dashboard their own tenant, that is the white-label platform guide. Operating a single large account (bulk updates, tagging, sync at scale) is managing thousands of locations, and the per-domain detail behind each panel lives in the listings, reviews, rank and AEO guides.