Esta guía está disponible en inglés.
How to Build a Reputation Management Platform
Architecture and API calls for a multi-client reputation management platform on Synup: review ingestion, reply workflows, invites, widgets and reporting.
De un vistazo
- Qué logra esta guía
- Stand up the five parts of a reputation product, multi-tenant accounts, a review inbox with an approval workflow, review generation campaigns, embeddable widgets and reporting, on top of the Synup API instead of platform integrations of your own.
- APIs utilizadas
- get
/api/v1/team/membersListar miembros del equipo - post
/api/v1/clientsCrear un cliente - post
/api/v1/clients/{id}/inviteEnviar la invitación al portal del cliente - post
/api/v1/locationsCrear una ubicación - get
/api/v1/reviewsListar reseñas - post
/api/v1/reviews/replyResponder a una reseña - post
/api/v1/reviews/notesAgregar una nota a una reseña - post
/api/v1/review-invites/campaignsCrear una campaña - patch
/api/v1/review-invites/campaigns/{id}Actualizar una campaña - post
/api/v1/review-invitesAgregar destinatarios a una campaña - post
/api/v1/review-invites/campaigns/{id}/actionEjecutar una acción del ciclo de vida de la campaña - post
/api/v1/reviews/widgetsCrear un widget de reseñas - post
/api/v1/reviews/competitorsAgregar un competidor - get
/api/v1/reviews/competitors/analysisObtener análisis competitivo de reseñas - get
/api/v1/reviews/summaryObtener resumen de reseñas - get
/api/v1/reviews/analyticsObtener análisis de reseñas
- get
- Referencia
- ClientsReviewsReview Invites
- Requisitos previos
- A Synup agency account and an API key with read and write access to Clients, Locations, Reviews and Review Invites
- The reviews guide, which this guide assumes you have read
- A verified sending domain and, for SMS, a carrier-verified number per location, set up in Synup before invite campaigns can send
- Casos de uso habituales
- An agency productising review management for its clients
- A vertical SaaS adding reputation as a paid module
- A brand building an internal tool for hundreds of franchisees
Build reputation management from scratch and every piece is an integration you own: Google's and Facebook's review APIs, an email and SMS sender with consent handling, scrapers for Yelp and the other review pages, and a data model to tie them together. Synup already collects and pushes all of that, so the same product becomes a handful of REST calls plus the parts that are genuinely yours, the approval workflow, the recipient pipeline and the dashboards. Below is how those map onto the API.
Architecture
- Your productTenants, users, UI, approval rules, dashboards
- Synup API
clients, locations, reviews, review-invitesOne key, scoped per client - SynupFetches reviews, pushes replies, sends invites, hosts widgets
- PlatformsGoogle, Facebook, Yelp and other review pages, email, SMS
The rule that keeps this simple: Synup is the system of record for reviews and campaigns, and your database stores only what Synup doesn't know about, which is your users, your tenants' mapping to Synup clients, and your workflow state (who approved which draft, when).
Tenants as clients
Each customer of your platform is a Synup client, and each of their physical locations is a Synup location.
POST /api/v1/clients needs a business name, a primary contact email and a primaryRepresentativeId,
which is one of your agency's team members from GET /api/v1/team/members. Pass a Google placeId and
its first location gets created for you. Otherwise create locations with POST /api/v1/locations as in the
listings guide.
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",
"goal": "get_reviews",
"website": "https://grovestreetdental.example"
}'const API = "https://ai.synup.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.SYNUP_API_KEY}`,
"Content-Type": "application/json",
};
async function createTenant({ businessName, email, website }) {
const res = await fetch(`${API}/clients`, {
method: "POST",
headers,
body: JSON.stringify({
businessName,
primaryContactEmail: email,
primaryRepresentativeId: process.env.SYNUP_REP_ID, // a team member id, from GET /team/members
goal: "get_reviews",
website,
}),
});
const body = await res.json();
if (res.status === 422 && body.code === "duplicate_archived") {
return { existingArchivedClientId: body.archivedClient.id }; // restore it instead of creating
}
if (!res.ok) throw new Error(`${res.status}: ${body.error}`);
return { clientId: body.data.client.id, locationId: body.data.locationId };
}import os
import requests
API = "https://ai.synup.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SYNUP_API_KEY']}"}
def create_tenant(business_name, email, website):
r = requests.post(f"{API}/clients", headers=HEADERS, timeout=30, json={
"businessName": business_name,
"primaryContactEmail": email,
"primaryRepresentativeId": os.environ["SYNUP_REP_ID"], # a team member id, from GET /team/members
"goal": "get_reviews",
"website": website,
})
body = r.json()
if r.status_code == 422 and body.get("code") == "duplicate_archived":
return {"existingArchivedClientId": body["archivedClient"]["id"]} # restore it instead of creating
r.raise_for_status()
return {"clientId": body["data"]["client"]["id"], "locationId": body["data"].get("locationId")}Store the client id against your tenant. If your customers should also log in to Synup's own portal,
POST /api/v1/clients/{id}/invite emails the primary contact a passwordless link.
The three failure code values (duplicate, duplicate_archived, invalid_rep) all come back as a 422, so
branch on code rather than on the status. A duplicate_archived carries the blocking client's id under
archivedClient, which you restore instead of creating a second record.
The inbox and the approval workflow
The inbox is GET /api/v1/reviews filtered by status=unreplied, per location, on a schedule. The
reviews guide covers that fetch loop. What a platform adds on top is the
workflow between a new review and a posted reply.
- New review
GET /reviews?status=unrepliedPolled per location; dedupe on review id - DraftTemplate by rating, or a model-written draft
- Note
POST /reviews/notesThe draft and its status, visible to the tenant in Synup too - ApproveYour UI: a queue per tenant, with edit and approve
- Post
POST /reviews/replyRecord pushed, status, externalReplyId in your workflow table
Your database holds the workflow state: review id, draft text, approver, approved-at, reply status. Synup holds
the review and the final reply. POST /api/v1/reviews/notes mirrors each draft into Synup so a tenant who opens
it directly sees the same thing. Auto-approve only what can't go wrong, like a templated thank-you for a
five-star review with no text.
Review generation
An invite campaign is a message, a landing page that screens people by rating, and a recipient list Synup sends to over email or SMS. Standing one up takes three calls.
POST /api/v1/review-invites/campaignscreates a draft for a location. Everything else is set now or later withPATCH /api/v1/review-invites/campaigns/{id}: thechannels(EMAIL,SMSorSMS_AND_EMAIL),fromEmailand subject, the SMS body,reviewSites, screening andfunnelThreshold, follow-ups, andautoSend.POST /api/v1/review-invites/campaigns/{id}/actionwithop: "publish"launches it. Publishing is gated per channel: a campaign with EMAIL needs a verified sending domain for the client, and one with SMS needs a carrier-verified number for the location. Either missing returns a422, and both are set up in Synup's UI, not through the API.POST /api/v1/review-invitesadds people. Rows are deduplicated within the batch and against the campaign, and the response counts what was added.
# 1. Create a draft campaign for a location
curl -X POST https://ai.synup.com/api/v1/review-invites/campaigns \
-H "Authorization: Bearer $SYNUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "locationId": "LOCATION_ID", "name": "Post-visit invite", "channels": "SMS_AND_EMAIL", "autoSend": true }'
# 2. Publish it (gated on a verified domain and number)
curl -X POST https://ai.synup.com/api/v1/review-invites/campaigns/CAMPAIGN_ID/action \
-H "Authorization: Bearer $SYNUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "op": "publish" }'
# 3. Add recipients from your system as visits complete
curl -X POST https://ai.synup.com/api/v1/review-invites \
-H "Authorization: Bearer $SYNUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "CAMPAIGN_ID",
"recipients": [{ "name": "Jordan Lee", "email": "jordan@example.com", "phone": "+14155550123", "contactType": "Customer" }],
"smsConsent": { "by": "front-desk@grovestreetdental.example" }
}'async function addRecipients(campaignId, recipients, consentBy) {
const res = await fetch(`${API}/review-invites`, {
method: "POST",
headers,
body: JSON.stringify({ campaignId, recipients, smsConsent: { by: consentBy } }),
});
if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
const { data } = await res.json();
return data; // { count, duplicates, alreadyPresent }
}
// Called from your point-of-sale or booking webhook once a visit completes
await addRecipients("CAMPAIGN_ID", [{ name: "Jordan Lee", email: "jordan@example.com", contactType: "Customer" }], "front-desk@grovestreetdental.example");def add_recipients(campaign_id, recipients, consent_by):
r = requests.post(f"{API}/review-invites", headers=HEADERS, timeout=30,
json={"campaignId": campaign_id, "recipients": recipients,
"smsConsent": {"by": consent_by}})
r.raise_for_status()
return r.json()["data"] # count, duplicates, alreadyPresent
# Called from your point-of-sale or booking webhook once a visit completes
add_recipients("CAMPAIGN_ID",
[{"name": "Jordan Lee", "email": "jordan@example.com", "contactType": "Customer"}],
"front-desk@grovestreetdental.example")The emailBody, reviewSites, followups and ratingMechanism fields all take the rich-text JSON the
campaign builder produces, which the API does not spell out field by field. The reliable way to get a correct
shape is to build one campaign in Synup's own campaign builder, then read it back with
GET /api/v1/review-invites/campaigns/{id} and copy the JSON those fields return. Send that shape on later
campaigns rather than composing it by hand.
Widgets
POST /api/v1/reviews/widgets configures an embeddable rating badge and review carousel for a location:
minimum rating, sources, a date window, how many reviews, layout, and a status of draft or published.
Synup hosts and renders the widget. Your platform stores the widget id per tenant and puts the embed on their
site.
curl -X POST https://ai.synup.com/api/v1/reviews/widgets \
-H "Authorization: Bearer $SYNUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"locationId": "LOCATION_ID",
"clientId": "CLIENT_ID",
"name": "Homepage carousel",
"minRating": 4,
"dateWindow": "1y",
"count": 12,
"status": "published"
}'The API returns the widget's configuration and its id, not an embed snippet. The snippet a customer pastes on their site is produced by the widget's own page in Synup, keyed by that widget id, so your product links the customer to that page (or stores the id and renders your own embed against the widget's public URL) rather than expecting the create call to hand back markup.
Benchmarks and reporting
A report page per tenant takes two calls:
GET /api/v1/reviews/summaryfor the portfolio: rating distribution, volume and trend across a client or the agency for a date range, filterable by tags.GET /api/v1/reviews/analyticsper location: KPIs with deltas, a monthly trend of volume, rating, sentiment and response rate, a per-platform breakdown, timing, an agency benchmark, themes and an AI summary.
For competitive context, POST /api/v1/reviews/competitors starts tracking a competitor by business name for a
location, and GET /api/v1/reviews/competitors/analysis returns a scorecard, strengths and gaps, review
velocity and share of voice against the tracked set. That analysis is the page tenants forward to their boss.
Multi-tenancy, keys and isolation
One agency API key, held server-side, serves every tenant, and your application decides which user sees which client. Never send the key to a browser. If a tenant wants direct API access, issue them their own key with the "Specific clients" setting locked to their client. The API returns 403 for anything else, so you don't have to proxy their calls.
Rate limits are per agency, so a noisy tenant's polling counts against everyone. Poll per location on a schedule rather than on every page load, and cache the analytics responses for an hour.
Failure cases
| Situation | What happens | What to do |
|---|---|---|
| Client name already exists | Create fails with code: duplicate | Reuse the existing client. Names are unique per agency. |
| Client exists but is archived | code: duplicate_archived with the archived id | Restore it rather than creating a second one. |
| Publish without a verified domain or number | 422 | Set them up in Synup's UI. The API can't provision either. |
| Archive an active campaign | { id, skipped: true }, not an error | Stop it first, then archive. |
| Recipient added twice | Counted in duplicates or alreadyPresent, not sent twice | Nothing. |
| Reply to a review from a custom source | respondable: false | Read-only. Reply on the platform. |