synup-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # synup-js
2
+
3
+ Node.js/TypeScript SDK for the [Synup](https://synup.com) v4 API. Manage locations, listings, reviews, analytics, and more across 50+ directories.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install synup-js
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { SynupClient } from 'synup-js';
15
+
16
+ const client = new SynupClient({
17
+ apiKey: 'YOUR_API_KEY', // Settings -> Integrations -> Generate
18
+ });
19
+
20
+ // Fetch first 10 locations
21
+ const result = await client.fetchAllLocations({ first: 10 });
22
+ console.log(result.locations);
23
+
24
+ // Fetch all locations (auto-paginate)
25
+ const allLocations = await client.fetchAllLocations({ fetchAll: true });
26
+
27
+ // Get reviews for a location
28
+ const reviews = await client.fetchInteractions(16808, {
29
+ startDate: '2024-01-01',
30
+ endDate: '2024-12-31',
31
+ });
32
+
33
+ // Respond to a review
34
+ await client.respondToReview('interaction-id', 'Thank you for the feedback!');
35
+ ```
36
+
37
+ ## Requirements
38
+
39
+ - Node.js 18+
40
+ - Synup API key
41
+
42
+ ## Method Reference
43
+
44
+ ### Account
45
+
46
+ | Method | Description |
47
+ |--------|-------------|
48
+ | `fetchPlanSites()` | Get plan site allocations |
49
+ | `fetchCountries()` | List supported countries |
50
+ | `fetchSubscriptions()` | Get subscription details |
51
+ | `createTemporaryCloseAutomation(opts)` | Schedule a temporary location closure |
52
+
53
+ ### Locations
54
+
55
+ | Method | Description |
56
+ |--------|-------------|
57
+ | `fetchAllLocations(opts)` | List locations (paginated or fetchAll) |
58
+ | `fetchLocationsByIds(ids)` | Fetch specific locations by ID |
59
+ | `fetchLocationsByStoreCodes(codes)` | Fetch locations by store code |
60
+ | `fetchLocationsByTags(tags, opts)` | Fetch locations filtered by tags |
61
+ | `fetchLocationsByFolder(opts)` | Fetch locations in a folder |
62
+ | `searchLocations(query, opts)` | Search locations by name/address |
63
+ | `createLocation(data)` | Create a new location |
64
+ | `updateLocation(data)` | Update an existing location |
65
+ | `archiveLocations(ids)` | Archive locations |
66
+ | `activateLocations(ids)` | Reactivate archived locations |
67
+ | `cancelArchiveLocations(ids, scope, reason)` | Cancel a pending archive |
68
+
69
+ ### Tags
70
+
71
+ | Method | Description |
72
+ |--------|-------------|
73
+ | `fetchTags()` | List all tags |
74
+ | `addLocationTag(locationId, tag)` | Add a tag to a location |
75
+ | `removeLocationTag(locationId, tag)` | Remove a tag from a location |
76
+
77
+ ### Folders
78
+
79
+ | Method | Description |
80
+ |--------|-------------|
81
+ | `fetchFoldersFlat()` | List all folders (flat list) |
82
+ | `fetchFoldersTree()` | List folders as a nested tree |
83
+ | `fetchFolderDetails(opts)` | Get folder details by name or ID |
84
+ | `createFolder(name, opts)` | Create a new folder |
85
+ | `renameFolder(oldName, newName)` | Rename a folder |
86
+ | `deleteFolder(name)` | Delete a folder |
87
+ | `addLocationsToFolder(folderName, locationIds)` | Add locations to a folder |
88
+ | `removeLocationsFromFolder(locationIds)` | Remove locations from their folder |
89
+
90
+ ### Listings
91
+
92
+ | Method | Description |
93
+ |--------|-------------|
94
+ | `fetchPremiumListings(locationId)` | Get premium directory listings |
95
+ | `fetchVoiceListings(locationId)` | Get voice search listings |
96
+ | `fetchAdditionalListings(locationId)` | Get additional directory listings |
97
+ | `fetchDuplicateListings(locationId)` | Get duplicate listings for a location |
98
+ | `fetchAllDuplicateListings(opts)` | Get all duplicate listings across locations |
99
+ | `fetchAiListings(locationId)` | Get AI-powered listing suggestions |
100
+ | `markListingsAsDuplicate(locationId, ids)` | Mark listings as duplicates |
101
+ | `markListingsAsNotDuplicate(locationId, ids)` | Mark listings as not duplicates |
102
+
103
+ ### Reviews
104
+
105
+ | Method | Description |
106
+ |--------|-------------|
107
+ | `fetchInteractions(locationId, opts)` | Fetch reviews/interactions |
108
+ | `fetchReviewDetails(interactionIds)` | Get detailed review data |
109
+ | `fetchReviewSettings(locationId)` | Get review notification settings |
110
+ | `editReviewSettings(locationId, settings)` | Update review notification settings |
111
+ | `fetchReviewSiteConfig()` | Get review site configuration |
112
+ | `respondToReview(interactionId, response)` | Respond to a review |
113
+ | `editReviewResponse(reviewId, responseId, text)` | Edit an existing response |
114
+ | `archiveReviewResponse(responseId)` | Archive a review response |
115
+ | `fetchReviewAnalyticsOverview(locationId, opts)` | Review analytics summary |
116
+ | `fetchReviewAnalyticsTimeline(locationId, opts)` | Review analytics over time |
117
+ | `fetchReviewAnalyticsSitesStats(locationId, opts)` | Review analytics by site |
118
+ | `fetchReviewPhrases(opts)` | Phrase/sentiment analysis on reviews |
119
+
120
+ ### Keywords
121
+
122
+ | Method | Description |
123
+ |--------|-------------|
124
+ | `fetchKeywords(locationId)` | List tracked keywords |
125
+ | `addKeywords(locationId, keywords)` | Add keywords to track |
126
+ | `archiveKeyword(keywordId)` | Archive a tracked keyword |
127
+ | `fetchKeywordsPerformance(locationId, opts)` | Keyword ranking performance |
128
+ | `fetchRankingAnalyticsTimeline(opts)` | Ranking trends over time |
129
+ | `fetchRankingSitewiseHistogram(opts)` | Ranking distribution by site |
130
+
131
+ ### Analytics
132
+
133
+ | Method | Description |
134
+ |--------|-------------|
135
+ | `fetchGoogleAnalytics(locationId, opts)` | Google Business Profile insights |
136
+ | `fetchBingAnalytics(locationId, opts)` | Bing Places insights |
137
+ | `fetchFacebookAnalytics(locationId, opts)` | Facebook page insights |
138
+
139
+ ### Photos
140
+
141
+ | Method | Description |
142
+ |--------|-------------|
143
+ | `fetchLocationPhotos(locationId)` | List photos for a location |
144
+ | `fetchPhotoUploadStatus(requestId)` | Check photo upload status |
145
+ | `addLocationPhotos(locationId, photos)` | Upload photos to a location |
146
+ | `removeLocationPhotos(locationId, photoIds)` | Remove photos |
147
+ | `starLocationPhotos(locationId, mediaIds, star)` | Star/unstar photos |
148
+
149
+ ### Connections
150
+
151
+ | Method | Description |
152
+ |--------|-------------|
153
+ | `fetchConnectionInfo(locationId)` | Get connection status for a location |
154
+ | `fetchConnectedAccounts(opts)` | List connected accounts |
155
+ | `fetchConnectedAccountDetails(accountId)` | Get details for a connected account |
156
+ | `fetchConnectedAccountFolders(accountId)` | Get folders for a connected account |
157
+ | `fetchConnectedAccountListings(accountId, opts)` | Get listings for a connected account |
158
+ | `fetchConnectionSuggestions(accountId, opts)` | Get matching suggestions |
159
+ | `getOauthConnectUrl(locationId, source, successUrl, errorUrl)` | Get OAuth connect URL |
160
+ | `connectGoogleAccount(successUrl, errorUrl)` | Connect a Google account |
161
+ | `connectFacebookAccount(successUrl, errorUrl)` | Connect a Facebook account |
162
+ | `oauthDisconnect(locationId, source)` | Disconnect an OAuth source |
163
+ | `disconnectGoogleAccount(accountId)` | Disconnect a Google account |
164
+ | `disconnectFacebookAccount(accountId)` | Disconnect a Facebook account |
165
+ | `triggerConnectedAccountMatches(accountIds)` | Trigger matching for connected accounts |
166
+ | `confirmConnectedAccountMatches(matchIds)` | Confirm matched listings |
167
+ | `connectListing(locationId, listingId, accountId)` | Connect a listing manually |
168
+ | `disconnectListing(locationId, source)` | Disconnect a listing |
169
+ | `createGmbListing(locationId, accountId)` | Create a Google Business listing |
170
+
171
+ ### Campaigns
172
+
173
+ | Method | Description |
174
+ |--------|-------------|
175
+ | `fetchReviewCampaigns(locationId, opts)` | List review solicitation campaigns |
176
+ | `fetchReviewCampaignCustomers(campaignId)` | Get customers in a campaign |
177
+ | `createReviewCampaign(opts)` | Create a new campaign |
178
+ | `addReviewCampaignCustomers(campaignId, customers)` | Add customers to a campaign |
179
+
180
+ ### Grid Rank
181
+
182
+ | Method | Description |
183
+ |--------|-------------|
184
+ | `fetchLocationGridReports(locationId, opts)` | List grid rank reports |
185
+ | `fetchGridReport(reportId)` | Get a specific grid rank report |
186
+ | `createGridReport(opts)` | Create a new grid rank report |
187
+
188
+ ### Users
189
+
190
+ | Method | Description |
191
+ |--------|-------------|
192
+ | `fetchUsers()` | List all users |
193
+ | `fetchUsersByIds(ids)` | Fetch specific users by ID |
194
+ | `fetchRoles()` | List available roles |
195
+ | `fetchUserResources(userId)` | Get resources assigned to a user |
196
+ | `createUser(data)` | Create a new user |
197
+ | `updateUser(data)` | Update an existing user |
198
+ | `addUserLocations(userId, locationIds)` | Assign locations to a user |
199
+ | `removeUserLocations(userId, locationIds)` | Remove location assignments |
200
+ | `addUserFolders(userId, folderIds)` | Assign folders to a user |
201
+ | `removeUserFolders(userId, folderIds)` | Remove folder assignments |
202
+ | `addUserAndFolder(data)` | Create a user with a folder in one call |
203
+
204
+ ## Pagination
205
+
206
+ Methods that return lists support cursor-based pagination:
207
+
208
+ ```typescript
209
+ // Single page
210
+ const page = await client.fetchAllLocations({ first: 25 });
211
+ console.log(page.locations); // Location[]
212
+ console.log(page.pageInfo); // { hasNextPage, endCursor, ... }
213
+
214
+ // Next page
215
+ const next = await client.fetchAllLocations({
216
+ first: 25,
217
+ after: page.pageInfo.endCursor,
218
+ });
219
+
220
+ // All pages at once
221
+ const all = await client.fetchAllLocations({ fetchAll: true, pageSize: 100 });
222
+ // Returns Location[] (flat array)
223
+ ```
224
+
225
+ ## Error Handling
226
+
227
+ ```typescript
228
+ import { SynupClient, SynupAPIError } from 'synup-js';
229
+
230
+ try {
231
+ await client.fetchAllLocations();
232
+ } catch (error) {
233
+ if (error instanceof SynupAPIError) {
234
+ console.log(error.statusCode); // 401, 404, 500, etc.
235
+ console.log(error.responseBody); // Raw API response
236
+ }
237
+ }
238
+ ```
239
+
240
+ ## Testing
241
+
242
+ ### Unit Tests
243
+
244
+ Run the full unit test suite (191 tests with mocked HTTP -- no API key needed):
245
+
246
+ ```bash
247
+ npm test
248
+ ```
249
+
250
+ All 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.
251
+
252
+ ### Integration Tests
253
+
254
+ Run integration tests against the real Synup API (94 tests):
255
+
256
+ ```bash
257
+ SYNUP_API_KEY=your_key npm run test:integration
258
+ ```
259
+
260
+ Integration 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.
261
+
262
+ ### Destructive Integration Tests
263
+
264
+ Some tests create and archive real locations. These are skipped by default and must be opted into:
265
+
266
+ ```bash
267
+ RUN_DESTRUCTIVE=1 SYNUP_API_KEY=your_key npm run test:integration
268
+ ```
269
+
270
+ The destructive suite covers the full location lifecycle: create, update, archive, activate, and cancel-archive. Test locations are archived at the end to avoid leaving stale data.
271
+
272
+ ## TypeScript Support
273
+
274
+ 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.
275
+
276
+ ```typescript
277
+ import { SynupClient, SynupAPIError } from 'synup-js';
278
+ ```
279
+
280
+ ## Features
281
+
282
+ - **TypeScript-first** -- Full type definitions for all inputs and responses
283
+ - **Zero dependencies** -- Uses native `fetch` (Node 18+)
284
+ - **Auto-pagination** -- Pass `fetchAll: true` to any paginated method
285
+ - **Automatic ID encoding** -- Pass numeric location IDs; base64 encoding is handled automatically
286
+ - **ESM + CJS** -- Works with both module systems
287
+
288
+ ## License
289
+
290
+ MIT