Skip to main content

Overview

This guide covers how to handle errors from the Shipstar API, including common error codes, response formats, and best practices for building resilient integrations.

Error Response Format

All API errors return a JSON response with a detail field:
{
  "detail": "Error message describing the issue"
}
Some errors return structured detail objects:
{
  "detail": {
    "error": "insufficient_credits",
    "message": "You have reached your monthly credit limit.",
    "plan": "free",
    "monthly_limit": 1000,
    "used": 1000,
    "remaining": 0
  }
}

HTTP Status Codes

Client Errors (4xx)

StatusNameDescription
400Bad RequestInvalid request parameters or validation failure
401UnauthorizedMissing or invalid API key / JWT token
402Payment RequiredInsufficient credits for the operation
403ForbiddenNot authorized to access this resource
404Not FoundResource doesn’t exist
409ConflictResource already exists (duplicate)
422Unprocessable EntityInput validation error
429Too Many RequestsRate limit exceeded

Server Errors (5xx)

StatusNameDescription
500Internal Server ErrorUnexpected server error
502Bad GatewayExternal service error (GitHub, etc.)
503Service UnavailableService temporarily unavailable

Common Errors and Solutions

{ "detail": "Invalid or expired API token." }
Causes:
  • Missing Authorization header
  • API key has been deactivated or expired
  • JWT session token has expired
Solutions:
  • Ensure header format is Authorization: Bearer YOUR_KEY
  • Check for typos in the API key
  • Create a new API key in the Dashboard
  • For JWT tokens, refresh via /api/internal/auth/jwt/refresh
// Correct format
headers: {
  'Authorization': 'Bearer your_api_key_here'
}

// Common mistakes
headers: {
  'Authorization': 'your_api_key_here',  // Missing "Bearer "
  'Api-Key': 'your_api_key_here'         // Wrong header name
}
{
  "detail": {
    "error": "insufficient_credits",
    "message": "You have reached your monthly credit limit.",
    "plan": "free",
    "monthly_limit": 1000,
    "used": 1000,
    "remaining": 0
  }
}
Causes:
  • Monthly credit allocation exhausted
Solutions:
  • Wait for credits to reset at the next billing cycle
  • Upgrade your plan for more credits
  • Check your usage in the Dashboard under Subscription
if (response.status === 402) {
  const { detail } = await response.json();
  console.log(`Credits exhausted: ${detail.used}/${detail.monthly_limit}`);
  console.log(`Plan: ${detail.plan}`);
}
{ "detail": "Not authorized" }
Causes:
  • Accessing a resource that belongs to another project or team
  • Attempting an admin-only operation
Solutions:
  • Check that your API key belongs to the correct project
  • Verify your account permissions
{ "detail": "Content not found" }
Causes:
  • Content ID doesn’t exist
  • Public slug doesn’t match any published content
  • Resource has been deleted
Solutions:
  • Verify the content ID or slug
  • Check that the content hasn’t been deleted or unpublished
async function getContent(contentId) {
  const response = await fetch(
    `${API_URL}/api/internal/sources/content/${contentId}`,
    { headers }
  );

  if (response.status === 404) {
    return null; // Content doesn't exist
  }

  return response.json();
}
{ "detail": "Resource already exists" }
Causes:
  • Attempting to create a duplicate resource
Solutions:
  • Check if the resource already exists before creating
  • Use update endpoints instead of create
{
  "detail": "Rate limit exceeded. Please try again later.",
  "retry_after": 60
}
Causes:
  • Exceeded requests per minute limit
Solutions:
  • Implement exponential backoff
  • Respect the retry_after value
  • Cache responses where possible
See Rate Limiting below.
{ "detail": "External service error" }
Causes:
  • GitHub API is unavailable or returning errors
  • Other external service failure
Solutions:
  • Retry after a short delay
  • Check if GitHub is experiencing an outage

Implementing Error Handling

Basic Error Handling

async function shipstarRequest(endpoint, options = {}) {
  const response = await fetch(`https://app.shipstar.ai${endpoint}`, {
    ...options,
    headers: {
      'Authorization': `Bearer ${process.env.SHIPSTAR_API_KEY}`,
      'Content-Type': 'application/json',
      ...options.headers
    }
  });

  if (!response.ok) {
    const error = await response.json();
    const detail = typeof error.detail === 'string'
      ? error.detail
      : error.detail?.message || JSON.stringify(error.detail);

    switch (response.status) {
      case 401:
        throw new Error('Authentication failed. Check your API key.');
      case 402:
        throw new Error(`Insufficient credits: ${detail}`);
      case 404:
        throw new Error(`Not found: ${detail}`);
      case 429:
        throw new Error('Rate limit exceeded. Try again later.');
      default:
        throw new Error(`API error (${response.status}): ${detail}`);
    }
  }

  return response.json();
}

With Retry Logic

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Don't retry client errors (except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        const error = await response.json();
        throw new Error(error.detail);
      }

      // Retry on rate limit
      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
        await sleep(retryAfter * 1000);
        continue;
      }

      // Retry on server errors with exponential backoff
      if (response.status >= 500) {
        await sleep(Math.pow(2, attempt) * 1000);
        continue;
      }

      return response.json();

    } catch (error) {
      // Retry on network errors
      if (error.name === 'TypeError' && attempt < maxRetries - 1) {
        await sleep(Math.pow(2, attempt) * 1000);
        continue;
      }
      throw error;
    }
  }

  throw new Error('Max retries exceeded');
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Handling Content Generation Failures

Content generation is asynchronous, so you need to handle failures in your polling logic:
async function generateContent(contentType, options = {}) {
  // Trigger generation
  const { content_id } = await shipstarRequest(
    `/api/internal/sources/github/${contentType}`,
    { method: 'POST', body: JSON.stringify(options) }
  );

  // Poll for result
  for (let i = 0; i < 30; i++) {
    await sleep(2000);

    const content = await shipstarRequest(
      `/api/internal/sources/content/${content_id}`
    );

    switch (content.status) {
      case 'completed':
        return content;
      case 'failed':
        throw new Error(`Generation failed: ${content.error_message}`);
      case 'pending':
      case 'processing':
        continue; // Keep polling
    }
  }

  throw new Error('Generation timed out after 60 seconds');
}

Rate Limiting

Shipstar applies rate limits to protect the service. Limits vary by endpoint:
Endpoint CategoryRate Limit
Authentication (login)10 requests/minute
Registration5 requests/minute
Email verification10 requests/hour
Password reset5 requests/hour
Token refresh30 requests/minute
General API endpoints100 requests/minute

Handling Rate Limits

class ShipstarClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://app.shipstar.ai';
  }

  async request(endpoint, options = {}) {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });

    if (response.status === 429) {
      const retryAfter = parseInt(
        response.headers.get('Retry-After') || '60'
      );
      console.log(`Rate limited. Retrying in ${retryAfter}s...`);
      await sleep(retryAfter * 1000);
      return this.request(endpoint, options); // Retry once
    }

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`${response.status}: ${error.detail}`);
    }

    return response.json();
  }
}

Best Practices

Handle All Statuses

Account for pending, processing, completed, and failed states in your polling logic

Use Retries

Implement exponential backoff for 429 and 5xx errors

Log Errors

Log errors with context (endpoint, content ID, status) for debugging

Set Timeouts

Don’t poll indefinitely — set a maximum wait time for content generation

Logging Example

async function generateWithLogging(contentType, options) {
  const startTime = Date.now();
  try {
    const content = await generateContent(contentType, options);
    console.log('Generation succeeded:', {
      contentId: content.id,
      outputType: content.output_type,
      duration: Date.now() - startTime
    });
    return content;
  } catch (error) {
    console.error('Shipstar API Error:', {
      contentType,
      options,
      error: error.message,
      timestamp: new Date().toISOString()
    });
    throw error;
  }
}