# Errors

The iClosed API uses standard HTTP status codes and returns a JSON body on every error response.

## Error Response Shapes

Errors come in two shapes depending on the type of failure.

### Authentication / server errors (`401`, `403`, `404`, `500`)

```json
{
  "message": "API key is required",
  "code": "MISSING_API_KEY"
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `message` | string | Human-readable description |
| `code` | string | Optional machine-readable code (present on some 401s) |


### Validation errors (`400`)

```json
{
  "message": {
    "status": 400,
    "details": {
      "formErrors": [],
      "fieldErrors": {
        "email": ["Invalid email format"],
        "phoneNumber": ["Must be E.164 format"]
      }
    },
    "endpoint": "/v1/contacts",
    "method": "POST"
  }
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `message.status` | number | Always `400` |
| `message.details.formErrors` | array | Global validation errors not tied to a specific field |
| `message.details.fieldErrors` | object | Map of field name → array of error messages |
| `message.endpoint` | string | The endpoint that returned the error |
| `message.method` | string | The HTTP method used |


### Rate limit errors (`429`)

```json
{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests, please try again later.",
  "retryAfter": 3,
  "limit": {
    "points": 20,
    "windowSec": 1
  }
}
```

## HTTP Status Codes

| Code | Meaning | Common Cause | Action |
|  --- | --- | --- | --- |
| `200` | OK | Request processed normally | Read response body |
| `201` | Created | Resource created successfully | Read `data` in response body |
| `400` | Bad Request | Missing required field, wrong type, or failed validation | Read `message.details.fieldErrors` |
| `401` | Unauthorized | Missing, expired, or revoked API key | Check `Authorization` header |
| `403` | Forbidden | Key exists but lacks permission | Check key scope or plan |
| `404` | Not Found | Resource ID doesn't exist or wrong endpoint URL | Verify IDs and path |
| `429` | Too Many Requests | Rate limit exceeded | Read `retryAfter` and wait |
| `500` | Internal Server Error | Unexpected server-side failure | Retry with backoff; contact Support if persistent |


## Application-Level Error Codes

### Authentication

| `message` value | `code` | Cause | Resolution |
|  --- | --- | --- | --- |
| `"API key is required"` | `MISSING_API_KEY` | `Authorization` header missing or token doesn't start with `iclosed_` | Add `Authorization: Bearer iclosed_<key>` |
| `"Invalid API key"` | — | Key unknown, revoked, or malformed | Create a new key in Settings |
| `"API key expired"` | — | Key has passed its expiration date | Rotate to a new key |


### Contact Errors

| Error | Cause | Resolution |
|  --- | --- | --- |
| `email` field error: `"Invalid email format"` | Email value fails format validation | Provide a valid email address |
| `phoneNumber` field error: `"Must be E.164 format"` | Phone not in E.164 (e.g. `+15551234567`) | Use international format with country code |
| `"Resource not found!"` | `contactId` doesn't exist in this account | Create the contact first with `POST /v1/contacts` |


### Invitee Answer Errors

| Error | Cause | Resolution |
|  --- | --- | --- |
| `"INVALID_INVITEE_QUESTION_TYPE"` | `inviteeQuestionAnswers[].type` doesn't match any question on the event | Align `type` with event invitee questions. Valid values: `EMAIL`, `PHONE_NO`, `NAME`, `FIRST_NAME`, `LAST_NAME` |
| `"Record not found!"` | `linkPrefix` doesn't match any live event | Verify format is `username/event-name` and the event is active |


### Booking Errors

| Error | Cause | Resolution |
|  --- | --- | --- |
| Slot unavailable (400) | `dateTime` in `POST /v1/eventCalls` is no longer open | Re-fetch `POST /v1/events/eventDates` and let user pick again |
| `"Resource not found!"` | `eventCallId` doesn't exist | Verify the call ID is correct |


### Rate Limit

| `code` value | Cause | Resolution |
|  --- | --- | --- |
| `RATE_LIMIT_EXCEEDED` | More than 20 requests/second to this endpoint | Read `retryAfter` in the response body and wait before retrying |


## Handling Errors in Code

### Python

```python
import time
import requests

def make_request(method, url, headers, **kwargs):
    response = getattr(requests, method)(url, headers=headers, **kwargs)

    if response.status_code == 400:
        errors = response.json().get('message', {}).get('details', {})
        print("Validation errors:", errors.get('fieldErrors'))
        return None

    if response.status_code == 401:
        body = response.json()
        print(f"Auth error: {body.get('message')} (code: {body.get('code')})")
        return None

    if response.status_code == 429:
        retry_after = response.json().get('retryAfter', 5)
        time.sleep(retry_after)
        return make_request(method, url, headers, **kwargs)  # retry once

    response.raise_for_status()
    return response.json()
```

### Node.js

```javascript
async function makeRequest(url, options = {}) {
  const response = await fetch(url, options);

  if (response.status === 400) {
    const { message } = await response.json();
    console.error('Validation errors:', message?.details?.fieldErrors);
    return null;
  }

  if (response.status === 401) {
    const { message, code } = await response.json();
    console.error(`Auth error: ${message} (${code})`);
    return null;
  }

  if (response.status === 429) {
    const { retryAfter = 5 } = await response.json();
    await new Promise(r => setTimeout(r, retryAfter * 1000));
    return makeRequest(url, options); // retry once
  }

  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
```

## See Also

- [Authentication](/docs/authentication) — API key setup and error responses
- [Rate Limiting](/docs/rate-limiting) — 429 handling and backoff strategy
- [API Reference](https://api-docs-iclosed.redocly.app/openapi/v1/openapi) — Per-endpoint error schemas