From syncro-msp
Provides Syncro MSP API integration patterns for authentication with API keys, pagination, rate limiting, error handling, request/response examples in JS, and best practices.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin syncroThis skill uses the workspace's default tool permissions.
The Syncro MSP API provides access to tickets, customers, assets, invoices, and more. This skill covers authentication, pagination, rate limiting, error handling, and best practices for API integration.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
The Syncro MSP API provides access to tickets, customers, assets, invoices, and more. This skill covers authentication, pagination, rate limiting, error handling, and best practices for API integration.
Syncro uses Bearer token authentication with API keys:
GET /api/v1/tickets
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
export SYNCRO_API_KEY="your-api-key-here"
export SYNCRO_SUBDOMAIN="your-subdomain" # e.g., "acme" for acme.syncromsp.com
The API base URL uses your subdomain:
https://{subdomain}.syncromsp.com/api/v1/
Example:
https://acme.syncromsp.com/api/v1/tickets
Syncro uses page-based pagination:
GET /api/v1/tickets?page=1
GET /api/v1/tickets?page=2
GET /api/v1/tickets?page=3
| Parameter | Description | Default |
|---|---|---|
page | Page number (1-based) | 1 |
Responses include pagination information:
{
"tickets": [...],
"meta": {
"total_entries": 156,
"total_pages": 7,
"page": 1,
"per_page": 25
}
}
async function fetchAllTickets(filter = {}) {
const allItems = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://${SUBDOMAIN}.syncromsp.com/api/v1/tickets?page=${page}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
allItems.push(...data.tickets);
hasMore = page < data.meta.total_pages;
page++;
// Respect rate limits
await sleep(350); // ~170 req/min to stay under 180/min
}
return allItems;
}
Syncro enforces a rate limit of 180 requests per minute per IP address.
When rate limited, you receive:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
"error": "Rate limit exceeded. Please wait before making more requests."
}
async function requestWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 30;
const jitter = Math.random() * 1000;
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000 + jitter);
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries - 1) throw error;
// Exponential backoff with jitter
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
await sleep(delay);
}
}
}
For bulk operations, throttle requests:
async function batchProcess(items, processFunc, delayMs = 350) {
const results = [];
for (const item of items) {
const result = await processFunc(item);
results.push(result);
await sleep(delayMs);
}
return results;
}
Single resource:
GET /api/v1/tickets/12345
Authorization: Bearer YOUR_API_KEY
List with filters:
GET /api/v1/tickets?customer_id=123&status=open&page=1
Authorization: Bearer YOUR_API_KEY
POST /api/v1/tickets
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"customer_id": 12345,
"subject": "New support request",
"status": "New",
"priority": "Medium"
}
PUT /api/v1/tickets/12345
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"status": "Resolved",
"priority": "Low"
}
DELETE /api/v1/contacts/67890
Authorization: Bearer YOUR_API_KEY
| Parameter | Description | Example |
|---|---|---|
page | Page number | page=2 |
query | Search term | query=email |
customer_id | Filter by customer | customer_id=123 |
date_from | Start date | date_from=2024-01-01 |
date_to | End date | date_to=2024-01-31 |
mine | Current user only | mine=true |
status | Status filter | status=open |
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Process response |
| 201 | Created | Resource created |
| 400 | Bad Request | Check request format |
| 401 | Unauthorized | Verify API key |
| 403 | Forbidden | Check permissions |
| 404 | Not Found | Resource doesn't exist |
| 422 | Unprocessable | Validation failed |
| 429 | Rate Limited | Wait and retry |
| 500 | Server Error | Retry with backoff |
{
"error": "Validation failed",
"errors": [
{
"field": "customer_id",
"message": "is required"
}
]
}
async function makeRequest(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
switch (response.status) {
case 401:
throw new Error('Invalid API key. Check your credentials.');
case 403:
throw new Error('Permission denied. Check API key permissions.');
case 404:
throw new Error('Resource not found.');
case 422:
const messages = errorData.errors?.map(e => `${e.field}: ${e.message}`).join(', ');
throw new Error(`Validation failed: ${messages}`);
case 429:
throw new Error('Rate limited. Try again later.');
default:
throw new Error(`API error: ${response.status}`);
}
}
return response.json();
}
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/tickets | List tickets |
| POST | /api/v1/tickets | Create ticket |
| GET | /api/v1/tickets/{id} | Get ticket |
| PUT | /api/v1/tickets/{id} | Update ticket |
| POST | /api/v1/tickets/{id}/comment | Add comment |
| POST | /api/v1/tickets/{id}/timer | Timer operations |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/customers | List customers |
| POST | /api/v1/customers | Create customer |
| GET | /api/v1/customers/{id} | Get customer |
| PUT | /api/v1/customers/{id} | Update customer |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/contacts | List contacts |
| POST | /api/v1/contacts | Create contact |
| GET | /api/v1/contacts/{id} | Get contact |
| PUT | /api/v1/contacts/{id} | Update contact |
| DELETE | /api/v1/contacts/{id} | Delete contact |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/customer_assets | List assets |
| POST | /api/v1/customer_assets | Create asset |
| GET | /api/v1/customer_assets/{id} | Get asset |
| PUT | /api/v1/customer_assets/{id} | Update asset |
| DELETE | /api/v1/customer_assets/{id} | Delete asset |
| GET | /api/v1/customer_assets/{id}/patches | Get patches |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/invoices | List invoices |
| POST | /api/v1/invoices | Create invoice |
| GET | /api/v1/invoices/{id} | Get invoice |
| PUT | /api/v1/invoices/{id} | Update invoice |
| POST | /api/v1/invoices/{id}/email | Email invoice |
| POST | /api/v1/invoices/{id}/payments | Record payment |
# List tickets
curl -X GET "https://acme.syncromsp.com/api/v1/tickets?page=1" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
# Create ticket
curl -X POST "https://acme.syncromsp.com/api/v1/tickets" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 12345,
"subject": "New ticket",
"status": "New",
"priority": "Medium"
}'