Este guia está disponível em inglês.
How to Add Local Marketing Features to Your SaaS
Add listings management, reviews, Google posts, profile analytics, rank tracking and AI visibility to a SaaS product with the Synup API, tenant by tenant.
Em resumo
- O que este guia realiza
- Map your tenants onto Synup clients and locations, run an onboarding flow that connects Google, and expose listings health, reviews, posts, analytics, rankings and AI visibility inside your own product.
- APIs utilizadas
- get
/api/v1/team/membersListar membros da equipe - post
/api/v1/clientsCriar um cliente - post
/api/v1/clients/{id}/inviteEnviar o convite do portal do cliente - post
/api/v1/locationsCriar um local - get
/api/v1/connections/google/connect-urlObter uma URL de conexão do Google - get
/api/v1/listings/summaryObter um resumo de listagens - get
/api/v1/listingsObter a listagem de um local - get
/api/v1/reviewsListar avaliações - post
/api/v1/reviews/replyResponder a uma avaliação - post
/api/v1/postsCriar uma publicação - get
/api/v1/analytics/summaryObter um resumo de análise de perfil - get
/api/v1/analyticsObter análises do perfil - post
/api/v1/seo/keywordsAdicionar palavras-chave monitoradas a um local - get
/api/v1/seo/keywordsObter a visão geral de classificação de um local - get
/api/v1/seo/rollupObter o resumo de classificação de todos os locais - post
/api/v1/aeo/reports/enqueueEnfileirar a geração de relatório AEO - get
/api/v1/aeo/reportsObter o relatório AEO de um local - get
/api/v1/aeo/rollupObter o resumo AEO de todos os locais
- get
- Referência
- ClientsLocationsListagens publicadasReviewsPostsAnálise de perfilSEOAEO
- Pré-requisitos
- A Synup agency account with an API key that has read and write access to every resource you plan to expose
- A stable id per tenant and per tenant location in your own database
- A server-side place to hold the key, which must never reach a browser
- Casos de uso típicos
- Vertical SaaS for dentists, restaurants, gyms, salons or home services adding a marketing tab
- A website builder or CRM that wants to publish business details to Google, Apple and Bing
- An agency platform selling local marketing as a module
Your tenant is a Synup client. Your tenant's location is a Synup location. Store those two ids on your side and every local marketing feature, listings, reviews, posts, analytics, rankings, AI visibility, hangs off them as one or two API calls. Nothing to integrate with Google, Apple, Bing or the directories yourself. Below is that mapping, the onboarding call that sets it up, and each feature in the order customers ask for it.
The mapping
- Your account
Synup clientOne per paying customer; stores clientId - Your location
Synup locationOne per physical place; stores locationId, sets storeCode - Your userStays yours. Authorisation is your job, the key is one agency key
- Feature tabs
listings, reviews, posts, analytics, seo, aeoOne or two calls each
Hold one agency API key on your server and enforce, in your application, which user may act on which client. Rate limits are per agency, so cache read responses per tenant for an hour and poll on schedules rather than on page loads.
Onboarding a tenant
The onboarding flow creates the client, creates each location, and hands the customer a Google connect link. Everything but the Google consent screen is automatic.
# 1. Create the client (primaryRepresentativeId is one of your team members: GET /api/v1/team/members)
curl -X POST https://ai.synup.com/api/v1/clients \
-H "Authorization: Bearer $SYNUP_API_KEY" -H "Content-Type: application/json" \
-d '{ "businessName": "Grove Street Dental", "primaryContactEmail": "owner@grovestreetdental.example", "primaryRepresentativeId": "TEAM_MEMBER_ID" }'
# 2. Create a location under it
curl -X POST https://ai.synup.com/api/v1/locations \
-H "Authorization: Bearer $SYNUP_API_KEY" -H "Content-Type: application/json" \
-d '{ "clientId": "CLIENT_ID", "name": "Grove Street Dental Mission", "countryIso": "US", "street": "88 Grove St", "city": "San Francisco", "stateIso": "CA", "postalCode": "94105", "phone": "+1 415 555 0142", "categoryName": "Dentist" }'
# 3. Get the Google connect link to show the customer
curl "https://ai.synup.com/api/v1/connections/google/connect-url?locationId=LOCATION_ID&returnUrl=/connected" \
-H "Authorization: Bearer $SYNUP_API_KEY"const API = "https://ai.synup.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.SYNUP_API_KEY}`,
"Content-Type": "application/json",
};
async function call(path, init = {}) {
const res = await fetch(`${API}${path}`, { ...init, headers });
const body = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${body.error}`);
return body.data;
}
async function onboard(tenant) {
const { client } = await call("/clients", {
method: "POST",
body: JSON.stringify({
businessName: tenant.name,
primaryContactEmail: tenant.ownerEmail,
primaryRepresentativeId: process.env.SYNUP_REP_ID,
}),
});
const locations = [];
for (const site of tenant.sites) {
const { locationId } = await call("/locations", {
method: "POST",
body: JSON.stringify({
clientId: client.id,
name: site.name,
countryIso: site.country,
street: site.street,
city: site.city,
stateIso: site.state,
postalCode: site.postalCode,
phone: site.phone,
categoryName: site.category,
}),
});
await call(`/locations/${locationId}`, { method: "PATCH", body: JSON.stringify({ storeCode: site.id }) });
const { url } = await call(`/connections/google/connect-url?locationId=${locationId}&returnUrl=/connected`);
locations.push({ siteId: site.id, locationId, googleConnectUrl: url });
}
return { clientId: client.id, locations };
}import os
import requests
API = "https://ai.synup.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SYNUP_API_KEY']}"}
def call(method, path, json=None, params=None):
r = requests.request(method, f"{API}{path}", headers=HEADERS, json=json, params=params, timeout=30)
body = r.json()
if not r.ok:
raise RuntimeError(f"{r.status_code}: {body.get('error')}")
return body["data"]
def onboard(tenant):
client = call("POST", "/clients", {
"businessName": tenant["name"],
"primaryContactEmail": tenant["owner_email"],
"primaryRepresentativeId": os.environ["SYNUP_REP_ID"],
})["client"]
locations = []
for site in tenant["sites"]:
location_id = call("POST", "/locations", {
"clientId": client["id"], "name": site["name"], "countryIso": site["country"],
"street": site["street"], "city": site["city"], "stateIso": site["state"],
"postalCode": site["postal_code"], "phone": site["phone"], "categoryName": site["category"],
})["locationId"]
call("PATCH", f"/locations/{location_id}", {"storeCode": site["id"]})
url = call("GET", "/connections/google/connect-url",
params={"locationId": location_id, "returnUrl": "/connected"})["url"]
locations.append({"site_id": site["id"], "location_id": location_id, "google_connect_url": url})
return {"client_id": client["id"], "locations": locations}Show the connect link in your onboarding UI and poll the connections list until Google appears. The
Google Business Profile guide has the polling loop. If you also want
customers in Synup's own portal, POST /api/v1/clients/{id}/invite emails them a passwordless link.
Feature 1: Listings health
The first screen customers want is "is my business information right everywhere". GET /api/v1/listings/summary
answers it for a whole client in one call: a health percentage, a per-location table, the weakest publishers and
ranked "needs attention" cards, each with a filter key you can turn into a drill-down. Per location,
GET /api/v1/listings lists every publisher's status and the Google profile completeness score.
Editing the profile is the listings guide.
curl "https://ai.synup.com/api/v1/listings/summary?clientId=CLIENT_ID" \
-H "Authorization: Bearer $SYNUP_API_KEY"Feature 2: Reviews
Reviews are two calls. GET /api/v1/reviews with status=unreplied is the inbox, and
POST /api/v1/reviews/reply posts the reply. The reviews guide covers filters,
notes and analytics, and the
reputation platform guide covers invites and widgets if reviews
become a module of their own.
Feature 3: Posts
POST /api/v1/posts publishes an announcement, event or offer to a location's connected platforms. It
publishes real content immediately unless scheduledFor is set or draft is true. Event and offer types are
Google-only. A platform with no active connection doesn't fail the call. The post is saved as a draft and the
response names the missing platforms, so your UI can prompt the customer to connect.
curl -X POST https://ai.synup.com/api/v1/posts \
-H "Authorization: Bearer $SYNUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"locationId": "LOCATION_ID",
"name": "September whitening offer",
"postType": "offer",
"platforms": ["google"],
"messageGoogle": "20% off teeth whitening this September. Book online.",
"ctaType": "book",
"ctaUrl": "https://grovestreetdental.example/book",
"eventTitle": "September whitening offer",
"eventStartAt": "2026-09-01T09:00:00Z",
"eventEndAt": "2026-09-30T17:00:00Z",
"scheduledFor": "2026-09-01T09:00:00Z"
}'async function schedulePost(locationId, post) {
const data = await call("/posts", { method: "POST", body: JSON.stringify({ locationId, ...post }) });
// data.status: draft | scheduled | active | error
// data.missingPlatforms: platforms with no connection; data.missingFields: what a draft still needs
return data;
}def schedule_post(location_id, post):
data = call("POST", "/posts", {"locationId": location_id, **post})
# data["status"]: draft | scheduled | active | error
# data.get("missingPlatforms"), data.get("missingFields")
return dataFeature 4: Profile analytics
Two analytics calls, one wide and one deep. GET /api/v1/analytics/summary rolls up profile views,
actions, website visits, calls, direction requests and button clicks across a client's locations, with a
per-location leaderboard, period comparison and daily, weekly or monthly granularity.
GET /api/v1/analytics is the per-location version, and it adds Google search keywords and the
maps-versus-search impression split.
curl "https://ai.synup.com/api/v1/analytics/summary?clientId=CLIENT_ID&comparison=previous_period&granularity=weekly" \
-H "Authorization: Bearer $SYNUP_API_KEY"Feature 5: Local rank tracking
POST /api/v1/seo/keywords starts tracking up to 25 keywords per location on a grid of search points around
the business (3, 5 or 7 per side, a configurable distance apart, set the first time a location is configured).
A scan runs in the background for each new keyword. GET /api/v1/seo/keywords returns the tracked
keywords, the monthly rank trend, the grid and the stat cards once results land, and GET /api/v1/seo/rollup
gives one row per location for a client dashboard.
curl -X POST https://ai.synup.com/api/v1/seo/keywords \
-H "Authorization: Bearer $SYNUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "locationId": "LOCATION_ID", "keywords": ["dentist san francisco", "teeth whitening near me"], "gridSize": 5, "distanceKm": 1.5 }'
# later
curl "https://ai.synup.com/api/v1/seo/keywords?locationId=LOCATION_ID" \
-H "Authorization: Bearer $SYNUP_API_KEY"async function trackKeywords(locationId, keywords) {
return call("/seo/keywords", {
method: "POST",
body: JSON.stringify({ locationId, keywords, gridSize: 5, distanceKm: 1.5 }),
}); // { keywords: [{ id, keyword }] }; results land asynchronously
}
async function rankOverview(locationId) {
return call(`/seo/keywords?locationId=${locationId}`); // keywords, months, grid, stats, statCards
}def track_keywords(location_id, keywords):
return call("POST", "/seo/keywords",
{"locationId": location_id, "keywords": keywords, "gridSize": 5, "distanceKm": 1.5})
def rank_overview(location_id):
return call("GET", "/seo/keywords", params={"locationId": location_id})Feature 6: AI visibility (AEO)
AI visibility is a background report. POST /api/v1/aeo/reports/enqueue starts one, measuring how ChatGPT,
Gemini and Perplexity describe and recommend a location for its tracked prompts. There's no job id, so poll
GET /api/v1/aeo/reports and watch generating until it flips to false and generatedAt updates.
GET /api/v1/aeo/rollup lists the latest overall and per-engine scores for every location in a client, with
portfolio history and the systemic fixes shared across locations.
curl -X POST https://ai.synup.com/api/v1/aeo/reports/enqueue \
-H "Authorization: Bearer $SYNUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "locationId": "LOCATION_ID" }'
# poll until generating is false
curl "https://ai.synup.com/api/v1/aeo/reports?locationId=LOCATION_ID" \
-H "Authorization: Bearer $SYNUP_API_KEY"Report generation runs several model calls per engine and prompt, so treat it as a monthly job, not a page-load one. Enqueue it on onboarding and on a schedule, and show the rollup in between.
Rollout order
Ship listings health first. It needs no customer action beyond onboarding, and it's the screen that proves the integration is working. Reviews come next, since the Google connection from onboarding already brings them in. Posts, analytics, rankings and AEO each add one tab and one or two calls, all reading from the same client and location ids you stored on day one.
Edge cases across the features
| Situation | What happens | What to do |
|---|---|---|
| A tenant renames their business to a name another client has | Create fails with code: duplicate | Client names are unique per agency. Suffix yours or reuse. |
403 on a tenant's location | The key doesn't cover that client | Check the key's client access setting. |
| A post to a platform with no connection | Saved as a draft, missingPlatforms lists it | Prompt to connect, then publish with POST /posts/{id}/publish. |
| More than 25 keywords for a location | Capped; extras aren't tracked | Let the customer choose their 25. |
| AEO enqueue on a native-only location | Not available | Report generation needs a location that predates the legacy cache; show the rollup only. |
| Analytics with no connected source | Empty series | Show the Google connect prompt instead of an empty chart. |