GuideGuías/Construir con Synup/Reputation management platform

Esta guía está disponible en inglés.

How do I build a reputation management platform on an API?

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
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

  1. Your productTenants, users, UI, approval rules, dashboards
  2. Synup APIclients, locations, reviews, review-invitesOne key, scoped per client
  3. SynupFetches reviews, pushes replies, sends invites, hosts widgets
  4. PlatformsGoogle, Facebook, Yelp and other review pages, email, SMS
What Synup does and what you build

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.

  1. New reviewGET /reviews?status=unrepliedPolled per location; dedupe on review id
  2. DraftTemplate by rating, or a model-written draft
  3. NotePOST /reviews/notesThe draft and its status, visible to the tenant in Synup too
  4. ApproveYour UI: a queue per tenant, with edit and approve
  5. PostPOST /reviews/replyRecord pushed, status, externalReplyId in your workflow table
Draft, approve, post

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.

  1. POST /api/v1/review-invites/campaigns creates a draft for a location. Everything else is set now or later with PATCH /api/v1/review-invites/campaigns/{id}: the channels (EMAIL, SMS or SMS_AND_EMAIL), fromEmail and subject, the SMS body, reviewSites, screening and funnelThreshold, follow-ups, and autoSend.
  2. POST /api/v1/review-invites/campaigns/{id}/action with op: "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 a 422, and both are set up in Synup's UI, not through the API.
  3. POST /api/v1/review-invites adds 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/summary for the portfolio: rating distribution, volume and trend across a client or the agency for a date range, filterable by tags.
  • GET /api/v1/reviews/analytics per 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

SituationWhat happensWhat to do
Client name already existsCreate fails with code: duplicateReuse the existing client. Names are unique per agency.
Client exists but is archivedcode: duplicate_archived with the archived idRestore it rather than creating a second one.
Publish without a verified domain or number422Set them up in Synup's UI. The API can't provision either.
Archive an active campaign{ id, skipped: true }, not an errorStop it first, then archive.
Recipient added twiceCounted in duplicates or alreadyPresent, not sent twiceNothing.
Reply to a review from a custom sourcerespondable: falseRead-only. Reply on the platform.