Help us improve
Share bugs, ideas, or general feedback.
From superops-ai
Provides GraphQL API patterns for SuperOps.ai: Bearer token auth, query/mutation building, cursor pagination, rate limiting, and error handling.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin superopsHow this skill is triggered — by the user, by Claude, or both
Slash command
/superops-ai:api-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
SuperOps.ai uses a GraphQL API for all integrations. Unlike REST APIs, GraphQL allows you to request exactly the data you need in a single request. This skill covers authentication, query patterns, mutations, pagination, rate limiting, and error handling.
Provides Autotask REST API patterns: header authentication, zone detection, queries with 14 operators, pagination, rate limiting, error handling, and retries.
Provides ConnectWise Manage PSA REST API patterns: authentication with public/private keys and clientId, pagination via page/pageSize, conditions query syntax, 60/min rate limiting, and error handling.
Manages SuperOps.ai client accounts via GraphQL: create, update, search, delete; handle sites, contacts, custom fields, stages, and statuses. For MSP PSA workflows.
Share bugs, ideas, or general feedback.
SuperOps.ai uses a GraphQL API for all integrations. Unlike REST APIs, GraphQL allows you to request exactly the data you need in a single request. This skill covers authentication, query patterns, mutations, pagination, rate limiting, and error handling.
SuperOps.ai uses Bearer token authentication. You need:
POST /msp
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
CustomerSubDomain: yourcompany
Note: You can only have one active API token. Regenerating creates a new token and invalidates the old one.
export SUPEROPS_API_KEY="your-api-token"
export SUPEROPS_SUBDOMAIN="yourcompany"
export SUPEROPS_REGION="us" # or "eu"
SuperOps.ai provides region-specific endpoints:
| Platform | Region | Endpoint |
|---|---|---|
| MSP | US | https://api.superops.ai/msp |
| MSP | EU | https://euapi.superops.ai/msp |
| IT | US | https://api.superops.ai/it |
| IT | EU | https://euapi.superops.ai/it |
{
"query": "query or mutation string",
"variables": {
"variableName": "value"
}
}
query getClientList($input: ListInfoInput!) {
getClientList(input: $input) {
clients {
accountId
name
status
}
listInfo {
totalCount
hasNextPage
endCursor
}
}
}
Variables:
{
"input": {
"first": 50,
"filter": {
"status": "Active"
}
}
}
mutation createTicket($input: CreateTicketInput!) {
createTicket(input: $input) {
ticketId
ticketNumber
subject
status
}
}
Variables:
{
"input": {
"subject": "Issue with email",
"client": {
"accountId": "client-uuid"
},
"priority": "HIGH"
}
}
SuperOps.ai uses cursor-based pagination for large result sets.
| Parameter | Type | Description |
|---|---|---|
first | Int | Number of items to return (max 500) |
after | String | Cursor for next page |
before | String | Cursor for previous page |
last | Int | Number of items from end |
{
"data": {
"getAssetList": {
"assets": [...],
"listInfo": {
"totalCount": 1250,
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjQ5"
}
}
}
}
query getAssetListPaginated($input: ListInfoInput!) {
getAssetList(input: $input) {
assets {
assetId
name
}
listInfo {
totalCount
hasNextPage
endCursor
}
}
}
First page:
{
"input": {
"first": 100
}
}
Next page:
{
"input": {
"first": 100,
"after": "YXJyYXljb25uZWN0aW9uOjk5"
}
}
async function fetchAllAssets(filter = {}) {
const allAssets = [];
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
const variables = {
input: {
first: 100,
filter,
...(cursor && { after: cursor })
}
};
const response = await graphqlRequest(GET_ASSETS_QUERY, variables);
const { assets, listInfo } = response.data.getAssetList;
allAssets.push(...assets);
hasNextPage = listInfo.hasNextPage;
cursor = listInfo.endCursor;
}
return allAssets;
}
X-RateLimit-Limit: 800
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1708012345
{
"errors": [
{
"message": "Rate limit exceeded. Please retry after 30 seconds.",
"extensions": {
"code": "RATE_LIMITED",
"retryAfter": 30
}
}
]
}
async function requestWithRetry(query, variables, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await graphqlRequest(query, variables);
if (response.errors?.some(e => e.extensions?.code === 'RATE_LIMITED')) {
const retryAfter = response.errors[0].extensions.retryAfter || 30;
const jitter = Math.random() * 1000;
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);
}
}
}
| Operator | Description | Example |
|---|---|---|
eq | Equals | { "status": "Active" } |
ne | Not equals | { "status": { "ne": "Closed" } } |
in | In array | { "status": ["Open", "In Progress"] } |
contains | Contains substring | { "name": { "contains": "Acme" } } |
startsWith | Starts with | { "name": { "startsWith": "A" } } |
gt | Greater than | { "createdTime": { "gt": "2024-01-01" } } |
gte | Greater than or equal | { "priority": { "gte": "HIGH" } } |
lt | Less than | { "createdTime": { "lt": "2024-02-01" } } |
lte | Less than or equal | { "count": { "lte": 10 } } |
{
"input": {
"filter": {
"and": [
{ "status": ["Open", "In Progress"] },
{ "priority": { "in": ["Critical", "High"] } },
{
"or": [
{ "client": { "accountId": "client-1" } },
{ "client": { "accountId": "client-2" } }
]
}
]
}
}
}
{
"input": {
"orderBy": {
"field": "createdTime",
"direction": "DESC"
}
}
}
All dates and times must be in UTC with ISO 8601 format:
2024-02-15T10:30:00Z
{
"filter": {
"createdTime": {
"gte": "2024-02-01T00:00:00Z",
"lte": "2024-02-29T23:59:59Z"
}
}
}
// Get tickets from last 7 days
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const variables = {
input: {
filter: {
createdTime: {
gte: sevenDaysAgo.toISOString()
}
}
}
};
{
"data": null,
"errors": [
{
"message": "Client not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["getClient"],
"extensions": {
"code": "NOT_FOUND",
"field": "accountId"
}
}
]
}
| Code | Description | Resolution |
|---|---|---|
UNAUTHENTICATED | Invalid/missing token | Check API token |
FORBIDDEN | Insufficient permissions | Check user role |
NOT_FOUND | Entity doesn't exist | Verify ID |
BAD_REQUEST | Invalid input | Check query/variables |
RATE_LIMITED | Too many requests | Implement backoff |
INTERNAL_ERROR | Server error | Retry with backoff |
async function safeGraphQLRequest(query, variables) {
try {
const response = await graphqlRequest(query, variables);
if (response.errors) {
for (const error of response.errors) {
switch (error.extensions?.code) {
case 'UNAUTHENTICATED':
throw new AuthenticationError(error.message);
case 'FORBIDDEN':
throw new PermissionError(error.message);
case 'NOT_FOUND':
throw new NotFoundError(error.message, error.path);
case 'RATE_LIMITED':
throw new RateLimitError(error.message, error.extensions.retryAfter);
default:
throw new APIError(error.message, error.extensions?.code);
}
}
}
return response.data;
} catch (error) {
// Handle network errors
if (error.code === 'ECONNREFUSED') {
throw new NetworkError('Unable to connect to SuperOps.ai API');
}
throw error;
}
}
In SuperOps.ai GraphQL:
nullnull as input can reset a fieldundefined (don't include field) to leave unchanged// This will CLEAR the assignee
{ "assignee": null }
// This will leave assignee unchanged
{ /* assignee field omitted */ }
# Good - specific fields
query {
getClientList(input: { first: 50 }) {
clients {
accountId
name
status
}
}
}
# Avoid - requesting everything
query {
getClientList(input: { first: 50 }) {
clients {
accountId
name
status
emailDomains
website
phone
industry
# ... many more fields
}
}
}
# Good - reusable with variables
query getClient($id: ID!) {
getClient(input: { accountId: $id }) {
name
status
}
}
# Avoid - hardcoded values
query {
getClient(input: { accountId: "abc123" }) {
name
status
}
}
# Good - single request for related data
query getDashboard($clientId: ID!) {
client: getClient(input: { accountId: $clientId }) {
name
}
tickets: getTicketList(input: {
filter: { client: { accountId: $clientId }, status: "Open" }
}) {
listInfo { totalCount }
}
assets: getAssetList(input: {
filter: { client: { accountId: $clientId }, status: "Online" }
}) {
listInfo { totalCount }
}
}
const cache = new Map();
async function getClientList() {
const cacheKey = 'clients';
const cached = cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
const data = await fetchAllClients();
cache.set(cacheKey, {
data,
expires: Date.now() + 5 * 60 * 1000 // 5 minutes
});
return data;
}
const SUPEROPS_API = process.env.SUPEROPS_REGION === 'eu'
? 'https://euapi.superops.ai/msp'
: 'https://api.superops.ai/msp';
async function graphqlRequest(query, variables = {}) {
const response = await fetch(SUPEROPS_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SUPEROPS_API_KEY}`,
'CustomerSubDomain': process.env.SUPEROPS_SUBDOMAIN
},
body: JSON.stringify({ query, variables })
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
// Usage
const GET_TICKETS = `
query getTicketList($input: ListInfoInput!) {
getTicketList(input: $input) {
tickets {
ticketId
ticketNumber
subject
status
priority
}
listInfo {
totalCount
hasNextPage
}
}
}
`;
const result = await graphqlRequest(GET_TICKETS, {
input: {
first: 50,
filter: { status: ["Open", "In Progress"] }
}
});