> For the complete documentation index, see [llms.txt](https://docs.pullbay.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.pullbay.com/documentation/concepts/errors-and-retries.md).

# Errors and Retries

Understand Pullbay API error codes, HTTP status codes, and how to implement robust retry logic. This comprehensive guide covers error handling, retryable vs. non-retryable errors, and production-ready code examples.

## Error Response Format

All Pullbay API errors follow a consistent JSON format, making it easy to parse and handle errors programmatically:

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "Your account has insufficient credits. Please add more credits or upgrade your plan.",
    "request_id": "req_abc123xyz789"
  }
}
```

### Error Response Fields

| Field            | Type   | Description                                                                           |
| ---------------- | ------ | ------------------------------------------------------------------------------------- |
| **`code`**       | String | Machine-readable error code for programmatic handling (e.g., `invalid_parameter`)     |
| **`message`**    | String | Human-readable error description explaining what went wrong                           |
| **`request_id`** | String | Unique identifier for this request. **Save this for debugging and support requests.** |

### Always Include the request\_id

When contacting Pullbay support, provide the `request_id` from the error response. This allows our support team to instantly locate your request in our logs and debug issues much faster.

## HTTP Status Codes Reference

Pullbay uses standard HTTP status codes to indicate request outcome. Here's the complete reference:

| Status Code | Name                  | Retryable? | Meaning                                      | What to Do                                      |
| ----------- | --------------------- | ---------- | -------------------------------------------- | ----------------------------------------------- |
| **200**     | OK                    | N/A        | Request successful, data returned            | Process the response data                       |
| **400**     | Bad Request           | **No**     | Invalid request format or parameters         | Fix your request and retry immediately          |
| **401**     | Unauthorized          | **No**     | Invalid or missing API key                   | Verify your API key, check authentication       |
| **402**     | Payment Required      | **No**     | Account has insufficient credits             | Add credits to your account via dashboard       |
| **403**     | Forbidden             | **No**     | API key lacks permission for this endpoint   | Use a different API key with proper permissions |
| **404**     | Not Found             | **No**     | Endpoint doesn't exist or resource not found | Check endpoint URL, verify resource ID          |
| **429**     | Too Many Requests     | **Yes**    | Rate limit exceeded                          | Implement backoff, wait for reset (see headers) |
| **500**     | Internal Server Error | **Yes**    | Pullbay server error (temporary)             | Wait a few seconds and retry                    |
| **502**     | Bad Gateway           | **Yes**    | Upstream service unavailable                 | Wait and retry (usually resolves quickly)       |
| **503**     | Service Unavailable   | **Yes**    | Pullbay service maintenance or overload      | Wait longer before retrying                     |

### Non-Retryable Status Codes (4xx except 429)

Do **not** retry requests that return `400`, `401`, `402`, `403`, or `404`. These indicate problems with your request that won't be fixed by waiting:

```python
# ❌ Don't retry these - fix your request first
if response.status_code in [400, 401, 402, 403, 404]:
    error = response.json()["error"]
    print(f"Non-retryable error: {error['code']} - {error['message']}")
    # Fix the underlying issue and retry with corrected request
```

### Retryable Status Codes (429, 5xx)

These errors are usually temporary and will likely succeed if retried:

```python
# ✓ Retry these errors
if response.status_code in [429, 500, 502, 503]:
    print("Transient error - implement backoff and retry")
```

## Common Error Codes by Category

### Authentication Errors

These errors indicate problems with your API credentials.

| Code                    | HTTP Status | Description                                  | Solution                                            |
| ----------------------- | ----------- | -------------------------------------------- | --------------------------------------------------- |
| `invalid_api_key`       | 401         | API key is malformed or doesn't exist        | Check your API key format and value                 |
| `expired_api_key`       | 401         | API key has been revoked or expired          | Generate a new API key in dashboard                 |
| `missing_authorization` | 401         | Authorization header is missing or malformed | Include `Authorization: Bearer YOUR_API_KEY` header |

**Example Error:**

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The API key 'test_invalid123' is not valid. Check your API key in the dashboard.",
    "request_id": "req_abc123"
  }
}
```

**Fix:**

```python
# Verify Authorization header format
headers = {
    "Authorization": "Bearer test_abc123xyz789"  # Correct format
}

# NOT: "Authorization: test_abc123xyz789" (missing "Bearer ")
# NOT: "Authorization: Bearer" (missing key)
```

***

### Request Parameter Errors

These errors indicate problems with the parameters you sent.

| Code                | HTTP Status | Description                               | Solution                                          |
| ------------------- | ----------- | ----------------------------------------- | ------------------------------------------------- |
| `invalid_parameter` | 400         | Parameter has invalid value or wrong type | Check parameter value type and format             |
| `missing_parameter` | 400         | Required parameter is missing             | Include all required parameters                   |
| `invalid_cursor`    | 400         | Pagination cursor is malformed or expired | Start from beginning; don't modify cursor strings |

**Example Error - Missing Parameter:**

```json
{
  "error": {
    "code": "missing_parameter",
    "message": "The 'app_id' parameter is required",
    "request_id": "req_def456"
  }
}
```

**Fix:**

```python
# ❌ Missing required parameter
response = requests.get(
    "https://api.pullbay.com/v1/app-store/reviews/all",
    params={"limit": 50},  # Missing app_id!
    headers={"Authorization": f"Bearer {API_KEY}"}
)

# ✓ Include all required parameters
response = requests.get(
    "https://api.pullbay.com/v1/app-store/reviews/all",
    params={
        "app_id": "com.example.app",  # Required!
        "limit": 50
    },
    headers={"Authorization": f"Bearer {API_KEY}"}
)
```

**Example Error - Invalid Parameter:**

```json
{
  "error": {
    "code": "invalid_parameter",
    "message": "The 'rating' parameter must be a number between 1 and 5",
    "request_id": "req_ghi789"
  }
}
```

**Fix:**

```python
# ❌ Invalid parameter value
params = {"rating": "five"}  # String instead of number!

# ✓ Use correct type and valid value
params = {"rating": 5}  # Integer between 1 and 5
```

***

### Account & Credit Errors

These errors relate to your account status and available credits.

| Code                   | HTTP Status | Description                               | Solution                                     |
| ---------------------- | ----------- | ----------------------------------------- | -------------------------------------------- |
| `insufficient_credits` | 402         | Account has run out of API credits        | Add credits via dashboard or upgrade plan    |
| `rate_limit_exceeded`  | 429         | Request rate limit exceeded for your plan | Implement backoff and retry after reset time |

**Example Error - Insufficient Credits:**

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "Your account has 0 credits remaining. Please add credits to continue.",
    "request_id": "req_jkl012"
  }
}
```

**Fix:**

```python
if response.status_code == 402:
    error = response.json()["error"]
    print(f"Out of credits: {error['message']}")
    print("Visit dashboard to add credits or upgrade plan")
    # Don't retry - user action required
```

**Example Error - Rate Limited:**

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. You have used 60 requests this minute. Limit: 60. Resets at: 1712973600",
    "request_id": "req_mno345"
  }
}
```

**Fix:**

```python
if response.status_code == 429:
    reset_time = int(response.headers.get("X-RateLimit-Reset", 0))
    wait_seconds = max(reset_time - time.time(), 1)
    print(f"Rate limited. Waiting {wait_seconds:.1f}s...")
    time.sleep(wait_seconds + 1)
    # Retry the request
```

***

### Service & Upstream Errors

These errors indicate problems on Pullbay's side (temporary or otherwise).

| Code                  | HTTP Status | Description                                      | Solution                                           |
| --------------------- | ----------- | ------------------------------------------------ | -------------------------------------------------- |
| `service_unavailable` | 503         | Pullbay service is down or overloaded            | Implement exponential backoff; retry later         |
| `upstream_error`      | 500         | Upstream service error (data source unavailable) | Retry with backoff; may indicate data source issue |
| `timeout`             | 500         | Request timed out before completing              | Retry with longer timeout or smaller batch size    |

**Example Error - Timeout:**

```json
{
  "error": {
    "code": "timeout",
    "message": "Request timed out after 60 seconds while fetching data",
    "request_id": "req_pqr678"
  }
}
```

**Fix:**

```python
if error_code == "timeout":
    print("Request timed out")
    # Try again with:
    # 1. Smaller limit to reduce data size
    # 2. Longer timeout (if available)
    # 3. Exponential backoff before retrying
    time.sleep(2 ** attempt)  # Exponential backoff
```

***

## Retryable vs. Non-Retryable Errors

### Non-Retryable Errors (Fix Your Request First)

**Do not retry** these errors. They indicate problems with your request that won't be fixed by waiting:

| Error Type           | Examples                                 | Why Don't Retry                       |
| -------------------- | ---------------------------------------- | ------------------------------------- |
| Authentication       | `invalid_api_key`, `expired_api_key`     | Key is permanently invalid            |
| Bad Request          | `invalid_parameter`, `missing_parameter` | Request format is wrong               |
| Not Found            | `404` (endpoint doesn't exist)           | Resource or endpoint doesn't exist    |
| Forbidden            | `403` (insufficient permissions)         | Your credentials lack required access |
| Insufficient Credits | `402`, `insufficient_credits`            | Account action required               |

**Example:**

```python
RETRYABLE_CODES = {429, 500, 502, 503}
NON_RETRYABLE_CODES = {400, 401, 402, 403, 404}

if response.status_code in NON_RETRYABLE_CODES:
    error = response.json()["error"]
    print(f"Non-retryable error: {error['code']}")
    print(f"Message: {error['message']}")
    print(f"Request ID: {error['request_id']}")
    # Fix the underlying issue; don't retry
    raise Exception(f"Non-retryable error: {error['code']}")
```

### Retryable Errors (Wait and Retry)

**Do retry** these errors with appropriate backoff logic:

| Error Type     | Status Codes                | Why Retry                                             |
| -------------- | --------------------------- | ----------------------------------------------------- |
| Rate Limited   | `429`                       | Temporary; will resolve when rate limit window resets |
| Server Error   | `500`, `502`, `503`         | Temporary; server may recover                         |
| Network Issues | Timeouts, connection errors | Usually transient                                     |

**Example:**

```python
RETRYABLE_CODES = {429, 500, 502, 503}

if response.status_code in RETRYABLE_CODES:
    print(f"Retryable error {response.status_code} - will retry with backoff")
    # Implement exponential backoff and retry
```

## Complete Python Retry Implementation

Here's a production-ready Python implementation with comprehensive error handling:

```python
import time
import random
import requests
from datetime import datetime

API_KEY = "test_abc123xyz789"
BASE_URL = "https://api.pullbay.com/v1"

# Define retryable and non-retryable status codes
RETRYABLE_CODES = {429, 500, 502, 503}
NON_RETRYABLE_CODES = {400, 401, 402, 403, 404}

class APIClientWithRetry:
    def __init__(self, api_key, max_retries=5, base_delay=1):
        self.api_key = api_key
        self.max_retries = max_retries
        self.base_delay = base_delay

    def request(self, method, endpoint, params=None, json_data=None):
        """
        Make API request with comprehensive retry logic.

        Args:
            method: HTTP method ('GET', 'POST', etc.)
            endpoint: API endpoint (e.g., '/reviews/all')
            params: Query parameters (for GET)
            json_data: JSON body (for POST)

        Returns:
            Parsed JSON response on success

        Raises:
            Exception: For non-retryable errors or exhausted retries
        """
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "User-Agent": "MyApp/1.0"
        }

        if json_data:
            headers["Content-Type"] = "application/json"

        for attempt in range(self.max_retries + 1):
            try:
                response = requests.request(
                    method=method,
                    url=f"{BASE_URL}{endpoint}",
                    params=params,
                    json=json_data,
                    headers=headers,
                    timeout=30
                )

                # Success - return data
                if response.status_code == 200:
                    return response.json()

                # Non-retryable error
                elif response.status_code in NON_RETRYABLE_CODES:
                    error = response.json().get("error", {})
                    request_id = error.get("request_id", "unknown")
                    raise Exception(
                        f"Non-retryable error {response.status_code}: "
                        f"{error.get('code')} - {error.get('message')} "
                        f"(Request ID: {request_id})"
                    )

                # Retryable error - implement backoff
                elif response.status_code in RETRYABLE_CODES:
                    if attempt < self.max_retries:
                        wait_seconds = self._calculate_backoff(attempt, response)
                        print(
                            f"⚠️  Retryable error {response.status_code} "
                            f"on attempt {attempt + 1}. "
                            f"Waiting {wait_seconds:.1f}s before retry..."
                        )
                        time.sleep(wait_seconds)
                        continue
                    else:
                        error = response.json().get("error", {})
                        raise Exception(
                            f"Max retries ({self.max_retries}) exceeded. "
                            f"Last error: {response.status_code} - "
                            f"{error.get('message')}"
                        )

                else:
                    # Unexpected status code
                    raise Exception(
                        f"Unexpected status code {response.status_code}: "
                        f"{response.text}"
                    )

            except requests.Timeout:
                # Timeout is retryable
                if attempt < self.max_retries:
                    wait_seconds = self._calculate_backoff(attempt, None)
                    print(
                        f"⚠️  Request timeout on attempt {attempt + 1}. "
                        f"Waiting {wait_seconds:.1f}s before retry..."
                    )
                    time.sleep(wait_seconds)
                    continue
                else:
                    raise Exception("Max retries exceeded: Timeout")

            except requests.ConnectionError as e:
                # Connection error is retryable
                if attempt < self.max_retries:
                    wait_seconds = self._calculate_backoff(attempt, None)
                    print(
                        f"⚠️  Connection error on attempt {attempt + 1}: {e}. "
                        f"Waiting {wait_seconds:.1f}s before retry..."
                    )
                    time.sleep(wait_seconds)
                    continue
                else:
                    raise

        raise Exception("All retry attempts exhausted")

    def _calculate_backoff(self, attempt, response):
        """Calculate backoff delay with optional rate-limit awareness."""
        # If rate limited, use X-RateLimit-Reset if available
        if response and response.status_code == 429:
            reset_timestamp = int(response.headers.get("X-RateLimit-Reset", 0))
            if reset_timestamp > 0:
                wait_seconds = max(reset_timestamp - time.time(), 1)
                return wait_seconds + 1  # Add 1 second buffer

        # Exponential backoff with jitter: 2^attempt * base_delay + jitter
        exponential_delay = (2 ** attempt) * self.base_delay
        jitter = random.uniform(0, exponential_delay * 0.1)  # 10% jitter
        return exponential_delay + jitter

    def get(self, endpoint, params=None):
        """GET request with retry logic."""
        return self.request("GET", endpoint, params=params)

    def post(self, endpoint, json_data=None):
        """POST request with retry logic."""
        return self.request("POST", endpoint, json_data=json_data)


# Usage Examples

client = APIClientWithRetry(api_key=API_KEY, max_retries=5)

# Example 1: Simple GET request
try:
    reviews = client.get(
        "/reviews/all",
        params={"app_id": "com.example.app", "limit": 100}
    )
    print(f"✓ Successfully fetched {len(reviews['data'])} reviews")
except Exception as e:
    print(f"✗ Failed: {e}")

# Example 2: GET request with error handling
try:
    reviews = client.get(
        "/reviews/all",
        params={"app_id": "com.example.app", "limit": 100}
    )
    remaining = reviews["meta"].get("credits_used")
    request_id = reviews["meta"]["request_id"]
    print(f"Request ID: {request_id}, Credits used: {remaining}")
except Exception as e:
    print(f"Error occurred: {e}")

# Example 3: POST request with complex data
try:
    result = client.post(
        "/reviews/search",
        json_data={
            "app_id": "com.example.app",
            "filters": {
                "rating": {"min": 4, "max": 5},
                "date_range": {"from": "2024-01-01"}
            }
        }
    )
    print(f"✓ Search succeeded: {len(result['data'])} results")
except Exception as e:
    print(f"✗ Search failed: {e}")
```

## Handling Partial Failures

Some requests may partially succeed, returning partial data with a status message. These responses include `meta.status = "partial"`:

```json
{
  "data": [
    {"id": "review_1", "rating": 5},
    {"id": "review_2", "rating": 4},
    {"id": "review_3", "rating": 3}
  ],
  "pagination": {
    "next_cursor": "eyJvZmZzZXQiOiAzfQ==",
    "has_more": true
  },
  "meta": {
    "status": "partial",
    "message": "Request timed out after fetching 3 pages (timeout on page 4)",
    "credits_used": 3
  }
}
```

### Handling Partial Results

```python
def handle_response(response):
    """Handle successful responses including partial ones."""
    data = response.json()

    # Check if response is partial
    if data["meta"].get("status") == "partial":
        print(f"⚠️  Partial response: {data['meta']['message']}")
        print(f"Received {len(data['data'])} results")

        # Process available data
        for item in data["data"]:
            process_item(item)

        # Resume from last cursor for remaining data
        if data["pagination"]["has_more"]:
            next_cursor = data["pagination"]["next_cursor"]
            print(f"Resume with cursor: {next_cursor}")
            # Make another request with this cursor
    else:
        print("✓ Complete response")
        # Process all data
```

## Debugging Tips

{% stepper %}
{% step %}

#### Always Save the request\_id

Every response (success or error) includes a `request_id`. Save this in your logs:

```python
response = client.get("/reviews/all", params={"app_id": "com.example.app"})
request_id = response["meta"]["request_id"]
print(f"Request ID: {request_id}")  # Save this in logs
```

{% endstep %}

{% step %}

#### Verify Your Parameters

Double-check parameters against documentation:

```python
# Common mistakes:
# - Missing required parameters (app_id, etc.)
# - Wrong parameter types (string vs. integer)
# - Invalid parameter values (rating must be 1-5)

params = {
    "app_id": "com.example.app",  # Required, string
    "limit": 100,                  # Optional, integer max 500
    "rating": 4,                   # Optional, integer 1-5
    "country": "us"                # Optional, valid country code
}
```

{% endstep %}

{% step %}

#### Check Your API Key

Ensure your API key is:

* Correct (copy-paste from dashboard, no typos)
* Not revoked (check in dashboard)
* Included in the Authorization header correctly

```python
# ✓ Correct format
headers = {"Authorization": "Bearer test_abc123xyz789"}

# ❌ Common mistakes:
# {"Authorization": "test_abc123xyz789"}  # Missing "Bearer "
# {"Authorization": "Bearer"}             # Missing key
# {"Api-Key": "test_abc123xyz789"}        # Wrong header name
```

{% endstep %}

{% step %}

#### Verify Your Account Status

Check:

* Credit balance (insufficient credits → error 402)
* API key not revoked
* Plan allows this endpoint/action

Visit your Pullbay dashboard to check account health.
{% endstep %}

{% step %}

#### Check Pullbay Service Health

If you're seeing `500`, `502`, or `503` errors:

* Check the Pullbay status page (linked in dashboard)
* These are usually temporary; retry with backoff
* Contact support if errors persist
  {% endstep %}

{% step %}

#### Monitor Your Rate Limits

Check rate limit status in response headers:

```python
response = requests.get(...)
remaining = response.headers.get("X-RateLimit-Remaining")
limit = response.headers.get("X-RateLimit-Limit")
reset = response.headers.get("X-RateLimit-Reset")

print(f"Rate limit: {remaining}/{limit}, resets at {reset}")
```

{% endstep %}

{% step %}

#### Enable Debug Logging

Log all requests and responses for troubleshooting:

```python
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def make_request(endpoint, params):
    logger.debug(f"Requesting: {endpoint} with params: {params}")
    response = requests.get(f"{BASE_URL}{endpoint}", params=params)
    logger.debug(f"Response status: {response.status_code}")
    logger.debug(f"Response body: {response.text}")
    return response
```

{% endstep %}
{% endstepper %}

## Frequently Asked Questions

<details>

<summary>Are credits charged for failed requests?</summary>

**Not for rate-limited requests:** `429` errors do not consume API credits.

**Yes for successful requests:** Even if the response is a `400` or `401` error indicating a malformed request, credits may be consumed. Check your request to avoid unnecessary charges.

**For other errors:** Transient errors (`500`, `502`, `503`) may or may not consume credits depending on how far the request progressed. Check the `meta.credits_used` field in the response.

</details>

<details>

<summary>How do I resume a request that timed out?</summary>

When a request times out, the response includes a `next_cursor` in `pagination`:

```python
# Original request timed out
response = client.get("/reviews/all", params={"app_id": "com.example.app"})

# meta.status = "partial" means timeout occurred
if response["meta"]["status"] == "partial":
    next_cursor = response["pagination"]["next_cursor"]

    # Resume with next cursor
    remaining = client.get(
        "/reviews/all",
        params={
            "app_id": "com.example.app",
            "cursor": next_cursor  # Resume from where it stopped
        }
    )
```

</details>

<details>

<summary>What does 'upstream_error' mean?</summary>

`upstream_error` indicates that Pullbay's service couldn't reach the data source (e.g., app store servers). This is usually temporary:

1. The upstream service may be temporarily offline
2. Pullbay's connection to it may have failed
3. The data source may be rate-limiting Pullbay

**Solution:** Retry with exponential backoff. If the error persists, contact Pullbay support with the `request_id`.

</details>

<details>

<summary>Can I increase my retry attempts?</summary>

Yes, most HTTP clients support configurable retry settings. In our example:

```python
client = APIClientWithRetry(api_key=API_KEY, max_retries=10)  # Increase retries
```

However, note:

* More retries = longer wait times during outages
* Retrying too aggressively can look like a DDoS
* For rate limits, the `X-RateLimit-Reset` header is more accurate than arbitrary retries

</details>

<details>

<summary>Should I implement circuit breakers?</summary>

For applications with many API clients, circuit breakers prevent cascading failures:

* Track failures over time
* If failure rate exceeds threshold, "trip" the circuit (stop retrying temporarily)
* Wait a longer interval before allowing traffic again

This is beyond scope here, but libraries like `pybreaker` (Python) implement this pattern.

</details>

<details>

<summary>How do I know if an error is permanent or temporary?</summary>

* **Permanent (don't retry):** `4xx` status codes except `429`
* **Temporary (do retry):** `429`, `5xx`, network errors
* **Status field:** Check `meta.status` for "partial" (indicates partial timeout)

When in doubt, implement exponential backoff with a maximum retry count.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.pullbay.com/documentation/concepts/errors-and-retries.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
