> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipstar.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling Guide

> How to handle errors from the Shipstar API

## 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:

```json theme={null}
{
  "detail": "Error message describing the issue"
}
```

For example, generation requests fail with `402` when your team's monthly
credit allowance is exhausted (see [Plans & Credits](/plans-and-credits)):

```json theme={null}
{
  "detail": "Monthly credit limit reached (1000/1000 used). Upgrade your plan to keep generating content."
}
```

## HTTP Status Codes

### Client Errors (4xx)

| Status | Name                 | Description                                      |
| ------ | -------------------- | ------------------------------------------------ |
| 400    | Bad Request          | Invalid request parameters or validation failure |
| 401    | Unauthorized         | Missing or invalid API key / JWT token           |
| 402    | Payment Required     | Insufficient credits for the operation           |
| 403    | Forbidden            | Not authorized to access this resource           |
| 404    | Not Found            | Resource doesn't exist                           |
| 409    | Conflict             | Resource already exists (duplicate)              |
| 422    | Unprocessable Entity | Input validation error                           |
| 429    | Too Many Requests    | Rate limit exceeded                              |

### Server Errors (5xx)

| Status | Name                  | Description                           |
| ------ | --------------------- | ------------------------------------- |
| 500    | Internal Server Error | Unexpected server error               |
| 502    | Bad Gateway           | External service error (GitHub, etc.) |
| 503    | Service Unavailable   | Service temporarily unavailable       |

## Common Errors and Solutions

<AccordionGroup>
  <Accordion title="401 Unauthorized">
    ```json theme={null}
    { "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`

    ```javascript theme={null}
    // 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
    }
    ```
  </Accordion>

  <Accordion title="402 Insufficient Credits">
    ```json theme={null}
    {
      "detail": "Monthly credit limit reached (1000/1000 used). Upgrade your plan to keep generating content."
    }
    ```

    **Causes:**

    * Your team's monthly credit allowance is exhausted — generation requests
      charge credits per content type (see [Plans & Credits](/plans-and-credits))

    **Solutions:**

    * Wait for credits to reset at the next billing cycle
    * Upgrade your plan for more credits
    * Check your usage in the dashboard sidebar or under **Account → Plan**

    ```javascript theme={null}
    if (response.status === 402) {
      const { detail } = await response.json();
      console.log(detail); // human-readable message with used/limit
    }
    ```
  </Accordion>

  <Accordion title="403 Forbidden">
    ```json theme={null}
    { "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
  </Accordion>

  <Accordion title="404 Not Found">
    ```json theme={null}
    { "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

    ```javascript theme={null}
    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();
    }
    ```
  </Accordion>

  <Accordion title="409 Conflict">
    ```json theme={null}
    { "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
  </Accordion>

  <Accordion title="429 Too Many Requests">
    ```json theme={null}
    {
      "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](#rate-limiting) below.
  </Accordion>

  <Accordion title="502 Bad Gateway">
    ```json theme={null}
    { "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
  </Accordion>
</AccordionGroup>

## Implementing Error Handling

### Basic Error Handling

<CodeGroup>
  ```javascript JavaScript theme={null}
  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();
  }
  ```

  ```python Python theme={null}
  import requests

  def shipstar_request(endpoint, method='GET', json=None):
      response = requests.request(
          method,
          f'https://app.shipstar.ai{endpoint}',
          headers={
              'Authorization': f'Bearer {os.environ["SHIPSTAR_API_KEY"]}',
              'Content-Type': 'application/json'
          },
          json=json
      )

      if not response.ok:
          error = response.json()
          detail = error.get('detail', 'Unknown error')

          if response.status_code == 401:
              raise Exception('Authentication failed. Check your API key.')
          elif response.status_code == 402:
              raise Exception(f'Insufficient credits: {detail}')
          elif response.status_code == 404:
              raise Exception(f'Not found: {detail}')
          elif response.status_code == 429:
              raise Exception('Rate limit exceeded. Try again later.')
          else:
              raise Exception(f'API error ({response.status_code}): {detail}')

      return response.json()
  ```
</CodeGroup>

### With Retry Logic

```javascript theme={null}
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:

```javascript theme={null}
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 Category      | Rate Limit          |
| ---------------------- | ------------------- |
| Authentication (login) | 10 requests/minute  |
| Registration           | 5 requests/minute   |
| Email verification     | 10 requests/hour    |
| Password reset         | 5 requests/hour     |
| Token refresh          | 30 requests/minute  |
| General API endpoints  | 100 requests/minute |

### Handling Rate Limits

```javascript theme={null}
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

<CardGroup cols={2}>
  <Card title="Handle All Statuses" icon="list-check">
    Account for pending, processing, completed, and failed states in your polling logic
  </Card>

  <Card title="Use Retries" icon="rotate">
    Implement exponential backoff for 429 and 5xx errors
  </Card>

  <Card title="Log Errors" icon="file-lines">
    Log errors with context (endpoint, content ID, status) for debugging
  </Card>

  <Card title="Set Timeouts" icon="clock">
    Don't poll indefinitely — set a maximum wait time for content generation
  </Card>
</CardGroup>

### Logging Example

```javascript theme={null}
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;
  }
}
```
