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": { ... }
}
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code (uppercase, snake_case) |
message | string | Human-readable error description |
details | object | null | Additional 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
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Authentication required or token invalid |
TOKEN_EXPIRED | 401 | JWT token has expired |
TOKEN_INVALID | 401 | JWT token is malformed or invalid |
SESSION_EXPIRED | 401 | User session has expired |
Authorization Errors
| Code | HTTP Status | Description |
|---|---|---|
FORBIDDEN | 403 | User lacks required permission |
NOT_PROJECT_MEMBER | 403 | User is not a member of the project |
NOT_TEAM_MEMBER | 403 | User is not a member of the team |
INSUFFICIENT_ROLE | 403 | User's role hierarchy is insufficient |
Resource Errors
| Code | HTTP Status | Description |
|---|---|---|
NOT_FOUND | 404 | Resource does not exist |
TASK_NOT_FOUND | 404 | Task does not exist |
PROJECT_NOT_FOUND | 404 | Project does not exist |
TEAM_NOT_FOUND | 404 | Team does not exist |
USER_NOT_FOUND | 404 | User does not exist |
Validation Errors
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR | 422 | Request validation failed |
INVALID_FORMAT | 400 | Data format is invalid |
MISSING_FIELD | 400 | Required field is missing |
INVALID_VALUE | 400 | Field value is invalid |
Conflict Errors
| Code | HTTP Status | Description |
|---|---|---|
CONFLICT | 409 | Resource state conflict |
DUPLICATE_ENTRY | 409 | Resource already exists |
ALREADY_MEMBER | 409 | User is already a member |
CANNOT_DELETE | 409 | Resource cannot be deleted |
Rate Limiting Errors
| Code | HTTP Status | Description |
|---|---|---|
RATE_LIMITED | 429 | Rate limit exceeded |
Server Errors
| Code | HTTP Status | Description |
|---|---|---|
INTERNAL_ERROR | 500 | Unexpected server error |
SERVICE_UNAVAILABLE | 503 | Service 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
- Check the response headers - They may contain useful debugging info
- Use the request ID - Include it when contacting support
- Review the details object - It often contains specific information about what went wrong
- Check rate limit headers -
X-RateLimit-RemainingandX-RateLimit-Reset