synup-js
v0.2.0
Published
Node.js SDK for the Synup API
Maintainers
Readme
synup-js
Node.js/TypeScript SDK for the Synup v4 API. Manage locations, listings, reviews, analytics, and more across 50+ directories.
Installation
npm install synup-jsQuick Start
import { SynupClient } from 'synup-js';
const client = new SynupClient({
apiKey: 'YOUR_API_KEY', // or omit to read from SYNUP_API_KEY env var
timeout: 240_000, // optional, ms (default 240000)
maxRetries: 2, // optional (default 2); retries 429 + 5xx with backoff
logger: console, // optional; any object with debug/info/warn/error
});
// Fetch first 10 locations
const result = await client.fetchAllLocations({ first: 10 });
console.log(result.locations);
// Fetch all locations (auto-paginate into a flat array)
const allLocations = await client.fetchAllLocations({ fetchAll: true });
// Or stream them with an async iterator — memory-efficient for large accounts
for await (const loc of client.iterateAllLocations({ pageSize: 100 })) {
console.log(loc.id);
}
// Get reviews for a location
const reviews = await client.fetchInteractions(16808, {
startDate: '2024-01-01',
endDate: '2024-12-31',
});
// Respond to a review
await client.respondToReview('interaction-id', 'Thank you for the feedback!');
// High-level workflow: auto-reply to recent 4+ star reviews
await client.autoReplyToReviews(16808, {
template: 'Thank you for your {rating}-star review!',
minRating: 4,
onlyUnanswered: true,
});Requirements
- Node.js 18+
- Synup API key
Configuration
Every constructor option is optional. A typical call is just new SynupClient() with SYNUP_API_KEY in the environment.
import { SynupClient, type Logger } from 'synup-js';
const client = new SynupClient({
apiKey: 'YOUR_API_KEY', // optional — reads SYNUP_API_KEY from env if omitted
baseUrl: 'https://api.synup.com', // optional — override the API base URL
timeout: 240_000, // optional — per-request timeout in ms (default 240000)
maxRetries: 2, // optional — max retries on transient failures (default 2)
logger: console, // optional — any object with debug/info/warn/error (default no-op)
});| Option | Type | Default | Notes |
|---|---|---|---|
| apiKey | string \| undefined | process.env.SYNUP_API_KEY | Throws SynupAuthenticationError if neither is provided |
| baseUrl | string | https://api.synup.com | Override for testing or self-hosted deployments |
| timeout | number | 240000 (240 s) | Applied per request via AbortController |
| maxRetries | number | 2 | Set to 0 to disable retries entirely |
| logger | Logger | no-op | See Logging below |
Environment variable
If apiKey is not passed and SYNUP_API_KEY is set in the environment, the client reads it automatically. The lookup is browser-safe — it only runs if process exists, so the SDK stays usable in browser bundles.
export SYNUP_API_KEY=your_keyconst client = new SynupClient(); // picks up SYNUP_API_KEY from envRetries
The client automatically retries transient failures with exponential backoff. No configuration is needed for the defaults.
Retried:
- HTTP
429,500,502,503,504 - Network errors and timeouts (both surface as
SynupAPIConnectionError)
Not retried:
- Any other 4xx response (400, 401, 403, 404, 422, ...)
- Errors thrown from application code
Backoff schedule (with maxRetries: 2 — 3 total attempts):
- Attempt 1: immediate
- Attempt 2: after 500 ms
- Attempt 3: after 1 000 ms
If a 429 response includes a Retry-After header, its value (integer seconds or HTTP-date) is used instead of the computed backoff. Parse failures fall back to the exponential schedule.
When retries are exhausted, the client throws the status-mapped error from the final attempt — e.g. SynupRateLimitError, SynupInternalServerError — not a generic "retries exhausted" wrapper.
Disable retries entirely:
const client = new SynupClient({ maxRetries: 0 });Logging
Pass any object matching the Logger interface and the client will log each request at debug, each retry at debug, and each final error at error. The default is a silent no-op.
import { SynupClient, type Logger, noopLogger } from 'synup-js';
const myLogger: Logger = {
debug: (msg, meta) => console.log('[debug]', msg, meta ?? ''),
info: (msg, meta) => console.log('[info]', msg, meta ?? ''),
warn: (msg, meta) => console.warn('[warn]', msg, meta ?? ''),
error: (msg, meta) => console.error('[err]', msg, meta ?? ''),
};
const client = new SynupClient({ logger: myLogger });The exported Logger interface is structurally compatible with console, pino, winston, and bunyan — you can usually pass any of them directly. noopLogger is also exported for composition.
Method Reference
Account
| Method | Description |
|--------|-------------|
| fetchPlanSites() | Get plan site allocations |
| fetchCountries() | List supported countries |
| fetchSubscriptions() | Get subscription details |
| createTemporaryCloseAutomation(opts) | Schedule a temporary location closure |
Locations
| Method | Description |
|--------|-------------|
| fetchAllLocations(opts) | List locations (paginated or fetchAll) |
| fetchLocationsPage(opts) | Fetch a single page with explicit cursor control |
| iterateAllLocations(opts) | Async iterator over every location |
| retrieve(locationId) | Convenience: fetch a single location by ID |
| fetchLocationsByIds(ids) | Fetch specific locations by ID |
| fetchLocationsByStoreCodes(codes) | Fetch locations by store code |
| fetchLocationsByTags(tags, opts) | Fetch locations filtered by tags |
| fetchLocationsByTagsPage(tags, opts) | Fetch a single page of tag-filtered locations |
| iterateLocationsByTags(tags, opts) | Async iterator over tag-filtered locations |
| fetchLocationsByFolder(opts) | Fetch locations in a folder |
| searchLocations(query, opts) | Search locations by name/address |
| searchLocationsPage(query, opts) | Fetch a single page of search results |
| iterateSearchLocations(query, opts) | Async iterator over search results |
| createLocation(data) | Create a new location |
| updateLocation(data) | Update an existing location |
| archiveLocations(ids) | Archive locations |
| activateLocations(ids) | Reactivate archived locations |
| cancelArchiveLocations(ids, scope, reason) | Cancel a pending archive |
Tags
| Method | Description |
|--------|-------------|
| fetchTags() | List all tags |
| addLocationTag(locationId, tag) | Add a tag to a location |
| removeLocationTag(locationId, tag) | Remove a tag from a location |
Folders
| Method | Description |
|--------|-------------|
| fetchFoldersFlat() | List all folders (flat list) |
| fetchFoldersTree() | List folders as a nested tree |
| fetchFolderDetails(opts) | Get folder details by name or ID |
| createFolder(name, opts) | Create a new folder |
| renameFolder(oldName, newName) | Rename a folder |
| deleteFolder(name) | Delete a folder |
| addLocationsToFolder(folderName, locationIds) | Add locations to a folder |
| removeLocationsFromFolder(locationIds) | Remove locations from their folder |
Listings
| Method | Description |
|--------|-------------|
| fetchPremiumListings(locationId) | Get premium directory listings |
| fetchVoiceListings(locationId) | Get voice search listings |
| fetchAdditionalListings(locationId) | Get additional directory listings |
| fetchDuplicateListings(locationId) | Get duplicate listings for a location |
| fetchAllDuplicateListings(opts) | Get all duplicate listings across locations |
| fetchAiListings(locationId) | Get AI-powered listing suggestions |
| markListingsAsDuplicate(locationId, ids) | Mark listings as duplicates |
| markListingsAsNotDuplicate(locationId, ids) | Mark listings as not duplicates |
Reviews
| Method | Description |
|--------|-------------|
| fetchInteractions(locationId, opts) | Fetch reviews/interactions |
| fetchInteractionsPage(locationId, opts) | Fetch a single page with explicit cursor control |
| iterateInteractions(locationId, opts) | Async iterator over reviews/interactions |
| fetchReviewDetails(interactionIds) | Get detailed review data |
| fetchReviewSettings(locationId) | Get review notification settings |
| editReviewSettings(locationId, settings) | Update review notification settings |
| fetchReviewSiteConfig() | Get review site configuration |
| respondToReview(interactionId, response) | Respond to a review |
| editReviewResponse(reviewId, responseId, text) | Edit an existing response |
| archiveReviewResponse(responseId) | Archive a review response |
| fetchReviewAnalyticsOverview(locationId, opts) | Review analytics summary |
| fetchReviewAnalyticsTimeline(locationId, opts) | Review analytics over time |
| fetchReviewAnalyticsSitesStats(locationId, opts) | Review analytics by site |
| fetchReviewPhrases(opts) | Phrase/sentiment analysis on reviews |
Keywords
| Method | Description |
|--------|-------------|
| fetchKeywords(locationId) | List tracked keywords |
| addKeywords(locationId, keywords) | Add keywords to track |
| archiveKeyword(keywordId) | Archive a tracked keyword |
| fetchKeywordsPerformance(locationId, opts) | Keyword ranking performance |
| fetchRankingAnalyticsTimeline(opts) | Ranking trends over time |
| fetchRankingSitewiseHistogram(opts) | Ranking distribution by site |
Analytics
| Method | Description |
|--------|-------------|
| fetchGoogleAnalytics(locationId, opts) | Google Business Profile insights |
| fetchBingAnalytics(locationId, opts) | Bing Places insights |
| fetchFacebookAnalytics(locationId, opts) | Facebook page insights |
Photos
| Method | Description |
|--------|-------------|
| fetchLocationPhotos(locationId) | List photos for a location |
| fetchPhotoUploadStatus(requestId) | Check photo upload status |
| addLocationPhotos(locationId, photos) | Upload photos to a location |
| removeLocationPhotos(locationId, photoIds) | Remove photos |
| starLocationPhotos(locationId, mediaIds, star) | Star/unstar photos |
Connections
| Method | Description |
|--------|-------------|
| fetchConnectionInfo(locationId) | Get connection status for a location |
| fetchConnectedAccounts(opts) | List connected accounts |
| fetchConnectedAccountDetails(accountId) | Get details for a connected account |
| fetchConnectedAccountFolders(accountId) | Get folders for a connected account |
| fetchConnectedAccountListings(accountId, opts) | Get listings for a connected account |
| fetchConnectionSuggestions(accountId, opts) | Get matching suggestions |
| getOauthConnectUrl(locationId, source, successUrl, errorUrl) | Get OAuth connect URL |
| connectGoogleAccount(successUrl, errorUrl) | Connect a Google account |
| connectFacebookAccount(successUrl, errorUrl) | Connect a Facebook account |
| oauthDisconnect(locationId, source) | Disconnect an OAuth source |
| disconnectGoogleAccount(accountId) | Disconnect a Google account |
| disconnectFacebookAccount(accountId) | Disconnect a Facebook account |
| triggerConnectedAccountMatches(accountIds) | Trigger matching for connected accounts |
| confirmConnectedAccountMatches(matchIds) | Confirm matched listings |
| connectListing(locationId, listingId, accountId) | Connect a listing manually |
| disconnectListing(locationId, source) | Disconnect a listing |
| createGmbListing(locationId, accountId) | Create a Google Business listing |
Campaigns
| Method | Description |
|--------|-------------|
| fetchReviewCampaigns(locationId, opts) | List review solicitation campaigns |
| fetchReviewCampaignCustomers(campaignId) | Get customers in a campaign |
| createReviewCampaign(opts) | Create a new campaign |
| addReviewCampaignCustomers(campaignId, customers) | Add customers to a campaign |
Grid Rank
| Method | Description |
|--------|-------------|
| fetchLocationGridReports(locationId, opts) | List grid rank reports |
| fetchGridReport(reportId) | Get a specific grid rank report |
| createGridReport(opts) | Create a new grid rank report |
Workflows
High-level compositions that combine multiple SDK calls into single, product-level operations. Added in v0.2.0.
| Method | Description |
|--------|-------------|
| autoReplyToReviews(locationId, opts) | Auto-reply to filtered reviews (supports dryRun) |
| onboardLocation(input) | Create a location and assign folder, tags, keywords in one call |
| bulkOnboardLocations(input) | Bulk-onboard from a CSV file (Node) or pre-parsed rows array |
| generateWeeklyReputationReport(locationId, opts) | Aggregate reviews, analytics, and listing health |
| auditListingsHealth(locationId) | Audit premium/voice/duplicate listings and compute a health score |
// Dry-run first to see what would be replied
const preview = await client.autoReplyToReviews(16808, {
minRating: 4,
onlyUnanswered: true,
dryRun: true,
});
// Onboard a location with everything in one call
const result = await client.onboardLocation({
name: 'Acme Coffee',
storeId: 'ACME-NYC-01',
street: '123 Main St',
city: 'New York',
stateIso: 'NY',
postalCode: '10001',
countryIso: 'US',
phone: '5551234567',
subCategoryId: 1,
folderName: 'NYC Stores',
tags: ['new', 'coffee'],
keywords: ['coffee shop near me'],
});
// Audit listings health
const audit = await client.auditListingsHealth(16808);
console.log(`Health score: ${audit.healthScore}%`);Users
| Method | Description |
|--------|-------------|
| fetchUsers() | List all users |
| fetchUsersByIds(ids) | Fetch specific users by ID |
| fetchRoles() | List available roles |
| fetchUserResources(userId) | Get resources assigned to a user |
| createUser(data) | Create a new user |
| updateUser(data) | Update an existing user |
| addUserLocations(userId, locationIds) | Assign locations to a user |
| removeUserLocations(userId, locationIds) | Remove location assignments |
| addUserFolders(userId, folderIds) | Assign folders to a user |
| removeUserFolders(userId, folderIds) | Remove folder assignments |
| addUserAndFolder(data) | Create a user with a folder in one call |
Pagination
Methods that return lists support three access patterns:
// 1. Single page with cursor control
const page = await client.fetchAllLocations({ first: 25 });
console.log(page.locations); // Location[]
console.log(page.pageInfo); // { hasNextPage, endCursor, ... }
// Next page
const next = await client.fetchAllLocations({
first: 25,
after: page.pageInfo.endCursor,
});
// 2. All pages at once (loads everything into memory)
const all = await client.fetchAllLocations({ fetchAll: true, pageSize: 100 });
// Returns Location[] (flat array)
// 3. Async iterator — streams pages, constant memory
for await (const loc of client.iterateAllLocations({ pageSize: 100 })) {
console.log(loc.id);
}The same three patterns are available for searchLocations, fetchLocationsByTags, and fetchInteractions (reviews).
Error Handling
All SDK errors extend SynupError. HTTP errors extend SynupAPIError and are further specialized by status code.
SynupError (root — any SDK error)
├── SynupAPIError (base — HTTP response received, has statusCode + responseBody)
│ ├── SynupAuthenticationError // 401
│ ├── SynupPermissionDeniedError // 403
│ ├── SynupNotFoundError // 404
│ ├── SynupValidationError // 400, 422
│ ├── SynupRateLimitError // 429 — also has .retryAfter
│ └── SynupInternalServerError // 5xx
└── SynupAPIConnectionError // no HTTP response — network, timeout, abortCatch the specific class you care about and fall through to the bases:
import {
SynupClient,
SynupError,
SynupAPIError,
SynupAuthenticationError,
SynupNotFoundError,
SynupRateLimitError,
SynupAPIConnectionError,
} from 'synup-js';
try {
await client.fetchAllLocations();
} catch (error) {
if (error instanceof SynupRateLimitError) {
// error.retryAfter is parsed from the Retry-After header (seconds)
console.log(`Rate limited, retry after ${error.retryAfter}s`);
} else if (error instanceof SynupNotFoundError) {
// 404
} else if (error instanceof SynupAuthenticationError) {
// 401
} else if (error instanceof SynupAPIError) {
// Any other HTTP error — has statusCode and responseBody
console.log(error.statusCode, error.responseBody);
} else if (error instanceof SynupAPIConnectionError) {
// Network failure or timeout (no HTTP response)
} else if (error instanceof SynupError) {
// Anything else from the SDK
}
}The client automatically retries transient failures before throwing — see Retries above for the full schedule and how to disable.
Testing
Unit Tests
Run the full unit test suite (191 tests with mocked HTTP -- no API key needed):
npm testAll HTTP calls are stubbed so these tests run fast and offline. They verify request construction, response parsing, pagination logic, and error handling for every SDK method.
Integration Tests
Run integration tests against the real Synup API (107 tests):
SYNUP_API_KEY=your_key npm run test:integrationIntegration tests exercise every SDK method against the live API using your account data. Read-only methods run unconditionally. Safe mutation tests (folder create/rename/delete, tag add/remove, keyword add/archive) run by default and clean up after themselves. Workflow tests run with dryRun: true by default so they're safe to run repeatedly — autoReplyToReviews, bulkOnboardLocations, generateWeeklyReputationReport, and auditListingsHealth all execute non-destructively.
The async iterator methods (iterateAllLocations, iterateLocationsByTags, iterateInteractions) are also exercised against the live API. The granular error hierarchy and SYNUP_API_KEY env fallback are verified via dedicated smoke tests.
Destructive Integration Tests
Some tests create and archive real locations. These are skipped by default and must be opted into:
RUN_DESTRUCTIVE=1 SYNUP_API_KEY=your_key npm run test:integrationThe destructive suite covers the full location lifecycle (create, update, archive, activate, cancel-archive) and the onboardLocation workflow end-to-end (create + folder assignment + tags + keywords, with cleanup). Test locations are archived in a finally block to avoid leaving stale data even on partial failure.
TypeScript Support
synup-js is written in TypeScript and ships with full type definitions. It works in both JavaScript and TypeScript projects with no additional configuration. Types are included in the package -- there is no separate @types/synup-js to install.
import { SynupClient, SynupAPIError } from 'synup-js';Features
- TypeScript-first -- Full type definitions for all inputs and responses
- Zero dependencies -- Uses native
fetch(Node 18+) - Auto-pagination --
fetchAll: true, async iterators, or explicit cursor control - Automatic retries -- Exponential backoff on 429/5xx/network failures, respects
Retry-After - Granular error types -- 8 exception classes for precise error handling
- Environment variable support --
SYNUP_API_KEYread automatically - Automatic ID encoding -- Pass numeric location IDs; base64 encoding is handled automatically
- High-level workflows --
autoReplyToReviews,onboardLocation,bulkOnboardLocations,generateWeeklyReputationReport,auditListingsHealth - ESM + CJS -- Works with both module systems
License
MIT
