Skip to main content

Error Handling

All Pinboard API errors follow a consistent JSON format, making it easy to handle errors programmatically.

Error Response Format

All API errors return a JSON object with the following structure:

{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": { ... }
}
}
FieldTypeDescription
codestringMachine-readable error code (uppercase, snake_case)
messagestringHuman-readable error description
detailsobject | nullAdditional context about the error

HTTP Status Codes

400 Bad Request

The request was malformed or contained invalid data.

{
"error": {
"code": "BAD_REQUEST",
"message": "Invalid request format",
"details": {
"field": "email",
"reason": "Invalid email format"
}
}
}

401 Unauthorized

Authentication is required or the provided credentials are invalid.

{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired token",
"details": null
}
}

403 Forbidden

The authenticated user doesn't have permission to access the resource.

{
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to access this resource",
"details": {
"required_permission": "project.tasks.create"
}
}
}

404 Not Found

The requested resource doesn't exist.

{
"error": {
"code": "NOT_FOUND",
"message": "Task not found",
"details": {
"resource_type": "task",
"resource_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
}

409 Conflict

The request conflicts with the current state of the resource.

{
"error": {
"code": "CONFLICT",
"message": "A user with this email already exists",
"details": {
"field": "email",
"value": "user@example.com"
}
}
}

422 Unprocessable Entity

The request was well-formed but contained validation errors.

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": {
"errors": [
{
"field": "title",
"message": "Title is required",
"type": "missing"
},
{
"field": "due_date",
"message": "Due date must be in the future",
"type": "invalid_value"
}
]
}
}
}

429 Too Many Requests

The rate limit has been exceeded.

{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please try again later.",
"details": {
"retry_after": 60,
"limit": 100,
"window": "minute"
}
}
}

500 Internal Server Error

An unexpected error occurred on the server.

{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"details": {
"request_id": "req_abc123"
}
}
}

Error Codes Reference

Authentication Errors

CodeHTTP StatusDescription
UNAUTHORIZED401Authentication required or token invalid
TOKEN_EXPIRED401JWT token has expired
TOKEN_INVALID401JWT token is malformed or invalid
SESSION_EXPIRED401User session has expired

Authorization Errors

CodeHTTP StatusDescription
FORBIDDEN403User lacks required permission
NOT_PROJECT_MEMBER403User is not a member of the project
NOT_TEAM_MEMBER403User is not a member of the team
INSUFFICIENT_ROLE403User's role hierarchy is insufficient

Resource Errors

CodeHTTP StatusDescription
NOT_FOUND404Resource does not exist
TASK_NOT_FOUND404Task does not exist
PROJECT_NOT_FOUND404Project does not exist
TEAM_NOT_FOUND404Team does not exist
USER_NOT_FOUND404User does not exist

Validation Errors

CodeHTTP StatusDescription
VALIDATION_ERROR422Request validation failed
INVALID_FORMAT400Data format is invalid
MISSING_FIELD400Required field is missing
INVALID_VALUE400Field value is invalid

Conflict Errors

CodeHTTP StatusDescription
CONFLICT409Resource state conflict
DUPLICATE_ENTRY409Resource already exists
ALREADY_MEMBER409User is already a member
CANNOT_DELETE409Resource cannot be deleted

Rate Limiting Errors

CodeHTTP StatusDescription
RATE_LIMITED429Rate limit exceeded

Server Errors

CodeHTTP StatusDescription
INTERNAL_ERROR500Unexpected server error
SERVICE_UNAVAILABLE503Service temporarily unavailable

Handling Errors in Code

TypeScript/JavaScript

interface ApiError {
error: {
code: string;
message: string;
details?: Record<string, unknown>;
};
}

async function apiRequest<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${API_URL}${path}`, options);

if (!response.ok) {
const errorData: ApiError = await response.json();

switch (errorData.error.code) {
case 'UNAUTHORIZED':
case 'TOKEN_EXPIRED':
// Redirect to login
window.location.href = '/login';
break;

case 'RATE_LIMITED':
// Wait and retry
const retryAfter = errorData.error.details?.retry_after as number || 60;
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return apiRequest(path, options);

case 'VALIDATION_ERROR':
// Show validation errors to user
const errors = errorData.error.details?.errors as ValidationError[];
showValidationErrors(errors);
break;

default:
// Show generic error message
showErrorToast(errorData.error.message);
}

throw new Error(errorData.error.message);
}

return response.json();
}

Python

import requests
from dataclasses import dataclass
from typing import Optional, Dict, Any

@dataclass
class ApiError(Exception):
code: str
message: str
details: Optional[Dict[str, Any]] = None
status_code: int = 500

def __str__(self):
return f"[{self.code}] {self.message}"

def api_request(method: str, path: str, **kwargs) -> dict:
"""Make an API request with error handling."""
response = requests.request(method, f"{API_URL}{path}", **kwargs)

if not response.ok:
error_data = response.json().get("error", {})
raise ApiError(
code=error_data.get("code", "UNKNOWN_ERROR"),
message=error_data.get("message", "An error occurred"),
details=error_data.get("details"),
status_code=response.status_code
)

return response.json()

# Usage
try:
task = api_request("GET", "/api/v1/tasks/123")
except ApiError as e:
if e.code == "NOT_FOUND":
print("Task not found")
elif e.code == "FORBIDDEN":
print("You don't have access to this task")
else:
print(f"Error: {e.message}")

Best Practices

1. Always Check Error Codes

Use the code field for programmatic error handling, not the message:

// Good
if (error.code === 'NOT_FOUND') {
// Handle not found
}

// Bad - messages can change
if (error.message.includes('not found')) {
// Handle not found
}

2. Log Request IDs

Include the request_id from error details when reporting issues:

console.error(`API Error [${error.details?.request_id}]: ${error.message}`);

3. Implement Retry Logic

For transient errors (rate limiting, server errors), implement exponential backoff:

async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;

const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}

4. Provide User-Friendly Messages

Map error codes to user-friendly messages:

const errorMessages: Record<string, string> = {
'UNAUTHORIZED': 'Please log in to continue',
'FORBIDDEN': 'You don\'t have permission to do this',
'NOT_FOUND': 'The item you\'re looking for doesn\'t exist',
'RATE_LIMITED': 'Too many requests. Please wait a moment.',
'INTERNAL_ERROR': 'Something went wrong. Please try again later.',
};

function getUserMessage(code: string, fallback: string): string {
return errorMessages[code] || fallback;
}

Debugging Tips

  1. Check the response headers - They may contain useful debugging info
  2. Use the request ID - Include it when contacting support
  3. Review the details object - It often contains specific information about what went wrong
  4. Check rate limit headers - X-RateLimit-Remaining and X-RateLimit-Reset