# Error Handling

HTTP status codes, the error response shape returned by the API, and patterns for handling errors gracefully in your integration.

## HTTP Status Codes

### Success Codes

| Code | Meaning | Description |
|------|---------|-------------|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 204 | No Content | Request succeeded, no content returned |

### Client Error Codes

| Code | Meaning | Common Causes |
|------|---------|---------------|
| 400 | Bad Request | Invalid JSON, missing required fields, validation errors, unsupported attachment types, malformed PDF |
| 401 | Unauthorized | Missing, invalid, or expired bearer token |
| 403 | Forbidden | Access denied or authentication error for the requested operation |
| 404 | Not Found | Resource does not exist or has been deleted |
| 406 | Not Acceptable | Content type not supported, or a required attribute is missing |
| 422 | Unprocessable Entity | PDF document could not be rendered (e.g. for a page preview) |
| 429 | Too Many Requests | Rate limit exceeded |

### Server Error Codes

| Code | Meaning | Action |
|------|---------|--------|
| 500 | Internal Server Error | Retry with exponential backoff |
| 503 | Service Unavailable | Service is temporarily down, retry later |

## Error Response Format

Both the Sign API and the Validation API share the same error envelope (Problem+JSON-style):

```json
{
  "type": "https://api.skribble.com/problem/problem-with-message",
  "method": "POST",
  "path": "/v2/signature-requests",
  "status": 400,
  "message": "Validation failed for JSON object"
}
```

| Field | Type | Always present | Description |
|---|---|---|---|
| `type` | URI string | yes | URI identifying the problem category |
| `method` | string | yes | HTTP method of the failing request |
| `path` | string | yes | Request URI of the failing request |
| `status` | integer | yes | HTTP status code (mirrors the response status line) |
| `key` | string | no | Optional machine-readable error key. Currently only present on PDF loading, malware detection, and unsupported attachment errors |
| `message` | string | yes | Human-readable message |

### Problem types

| `type` URI | Used for |
|---|---|
| `https://api.skribble.com/problem/problem-with-message` | Generic error with a message (default) |
| `https://api.skribble.com/problem/entity-not-found` | Requested resource does not exist (404) |
| `https://api.skribble.com/problem/constraint-violation` | Request violates a domain constraint |
| `https://api.skribble.com/problem/parameterized` | Parameterized error |
| `https://api.skribble.com/problem/limit-exceeded` | A usage limit was exceeded |

`/access/login` is the only endpoint that does not return Problem JSON — on invalid credentials it returns a `text/plain` body: `Please ensure your username and api-key are correct!`

## Common Errors and Solutions

### Authentication and Authorization

#### 401 Unauthorized — token missing, invalid, or expired

Returned by the security layer before reaching the controller. Access tokens issued by `/access/login` are valid for ~20 minutes; obtain a new one when this happens.

```javascript
async function apiCall(url, options) {
  let response = await fetch(url, {
    ...options,
    headers: { ...options.headers, 'Authorization': `Bearer ${await getToken()}` }
  });

  if (response.status === 401) {
    await refreshToken();
    response = await fetch(url, {
      ...options,
      headers: { ...options.headers, 'Authorization': `Bearer ${await getToken()}` }
    });
  }

  return response;
}
```

#### 403 Forbidden — access denied

```json
{
  "type": "https://api.skribble.com/problem/problem-with-message",
  "method": "GET",
  "path": "/v2/signature-requests/4f43614a-de50-bfe1-2df3-4c881fc7840e",
  "status": 403,
  "message": "Access denied"
}
```

The caller is authenticated but is not permitted to perform the operation on this resource.

### Validation

#### 400 Bad Request — validation failed

```json
{
  "type": "https://api.skribble.com/problem/problem-with-message",
  "method": "POST",
  "path": "/v2/signature-requests",
  "status": 400,
  "message": "Validation failed for JSON object"
}
```

Check the API reference for required fields and value constraints.

#### 400 Bad Request — unsupported attachment type

```json
{
  "type": "https://api.skribble.com/problem/problem-with-message",
  "method": "POST",
  "path": "/v2/signature-requests/.../attachments",
  "status": 400,
  "key": "error.attachment.unsupported-type",
  "message": "Attachment type is not supported"
}
```

Attachment errors carry the optional `key` field — branch on `key` rather than on `message` if you need to differentiate cases programmatically.

#### 400 Bad Request — PDF document loading failed

The document is either corrupt, password-protected, or not a valid PDF. The response carries a `key` field describing the specific failure mode.

### Resource state

#### 404 Not Found — entity does not exist

```json
{
  "type": "https://api.skribble.com/problem/entity-not-found",
  "method": "GET",
  "path": "/v2/signature-requests/00000000-0000-0000-0000-000000000000",
  "status": 404,
  "message": "SignatureRequest not found"
}
```

Note the dedicated `type` URI — checking `type === 'https://api.skribble.com/problem/entity-not-found'` is the most precise way to distinguish "this entity is gone" from other 404 causes (e.g. an unmapped route).

### Content negotiation

#### 406 Not Acceptable

```json
{
  "type": "https://api.skribble.com/problem/problem-with-message",
  "method": "POST",
  "path": "/v2/signature-requests",
  "status": 406,
  "message": "Content type not supported"
}
```

Returned when the request `Content-Type` is not supported, or when a required attribute is missing. Send JSON bodies with `Content-Type: application/json`.

#### 422 Unprocessable Entity — PDF rendering failed

Returned by document page-preview endpoints when the PDF cannot be rendered. The PDF may be valid but use features the renderer cannot process. Try a different page, a different scale, or re-upload the document.

### Rate limiting

#### 429 Too Many Requests

```json
{
  "type": "https://api.skribble.com/problem/problem-with-message",
  "method": "POST",
  "path": "/v2/signature-requests",
  "status": 429,
  "message": "Too many requests"
}
```

Limits are 600 authenticated requests per 10 minutes, 15 unauthenticated requests per minute. The same status is also returned by `/signature-requests/{SR_ID}/remind` when the per-request reminder limit is reached.

Throttle client-side rather than relying on retries:

```javascript
class RateLimitedClient {
  constructor() {
    this.queue = [];
    this.processing = false;
    this.minDelay = 100; // ms between requests
  }

  async request(url, options) {
    return new Promise((resolve, reject) => {
      this.queue.push({ url, options, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const { url, options, resolve, reject } = this.queue.shift();

      try {
        const response = await fetch(url, options);

        if (response.status === 429) {
          await sleep(60_000);
          this.queue.unshift({ url, options, resolve, reject });
          continue;
        }

        resolve(response);
      } catch (error) {
        reject(error);
      }

      await sleep(this.minDelay);
    }

    this.processing = false;
  }
}
```

## Best Practices

### 1. Parse the Problem envelope

```javascript
class ApiError extends Error {
  constructor(problem) {
    super(problem.message);
    this.type = problem.type;
    this.status = problem.status;
    this.method = problem.method;
    this.path = problem.path;
    this.key = problem.key; // optional
  }

  isEntityNotFound() {
    return this.type === 'https://api.skribble.com/problem/entity-not-found';
  }
}

async function makeApiCall(url, options) {
  const response = await fetch(url, options);

  if (!response.ok) {
    // /access/login returns text/plain on 400; everything else returns Problem JSON
    const ct = response.headers.get('content-type') || '';
    if (ct.includes('application/json')) {
      throw new ApiError(await response.json());
    }
    throw new ApiError({
      type: 'https://api.skribble.com/problem/problem-with-message',
      method: options.method ?? 'GET',
      path: new URL(url).pathname,
      status: response.status,
      message: await response.text()
    });
  }

  return response.json();
}
```

Branch on `type` (and `key` when present) rather than scraping `message`. Messages are not part of the stable contract.

### 2. Log errors for debugging

```javascript
async function apiCallWithLogging(url, options) {
  const requestId = generateRequestId();

  console.log(`[${requestId}] Request: ${options.method} ${url}`);

  try {
    const response = await makeApiCall(url, options);
    console.log(`[${requestId}] Success`);
    return response;
  } catch (error) {
    console.error(`[${requestId}] Error`, {
      type: error.type,
      status: error.status,
      key: error.key,
      message: error.message
    });
    throw error;
  }
}
```

### 3. Circuit breaker for high-availability systems

```javascript
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 30000;
    this.failures = 0;
    this.state = 'CLOSED';
    this.nextRetry = null;
  }

  async call(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() > this.nextRetry) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is open');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextRetry = Date.now() + this.resetTimeout;
    }
  }
}
```

### 4. Graceful degradation

```javascript
async function getSignatureStatus(requestId) {
  try {
    return await api.get(`/signature-requests/${requestId}`);
  } catch (error) {
    if (error instanceof NetworkError || error.status >= 500) {
      const cached = await cache.get(`request:${requestId}`);
      if (cached) {
        return { ...cached, _stale: true };
      }
      return { status_overall: 'UNKNOWN', _error: true };
    }
    throw error;
  }
}
```

## Monitoring and Alerting

### Health check endpoint

Use the health endpoint to monitor API availability:

```javascript
async function checkApiHealth() {
  try {
    const response = await fetch(`${BASE_URL}/management/health`);
    const health = await response.json();
    return health.status === 'UP';
  } catch {
    return false;
  }
}

// Periodic health check
setInterval(async () => {
  const isHealthy = await checkApiHealth();
  if (!isHealthy) {
    alertOps('Skribble API health check failed');
  }
}, 60000);
```

### Error rate monitoring

Track error rates per status and per problem `type` to detect issues early:

```javascript
const errorMetrics = {
  total: 0,
  byStatus: {},
  byType: {},
  byEndpoint: {}
};

function recordError(endpoint, status, type) {
  errorMetrics.total++;
  errorMetrics.byStatus[status] = (errorMetrics.byStatus[status] || 0) + 1;
  errorMetrics.byType[type] = (errorMetrics.byType[type] || 0) + 1;
  errorMetrics.byEndpoint[endpoint] = (errorMetrics.byEndpoint[endpoint] || 0) + 1;
}
```
