> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.slotflow.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.slotflow.dev/_mcp/server.

# Error Handling

> All Slotflow API error codes, when they occur, and how your agent should handle them.

Every error response from the Slotflow API follows a consistent format:

```json
{
  "error": {
    "code": "SLOT_UNAVAILABLE",
    "message": "The requested slot is no longer available.",
    "status": 409
  }
}
```

* `code` — machine-readable error identifier (use this in your agent's logic)
* `message` — human-readable description (useful for logging, not for parsing)
* `status` — HTTP status code (matches the response status)

## Error reference

### UNAUTHORIZED (401)

**When:** Missing or invalid API key in the `Authorization` header.

```json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid API key.",
    "status": 401
  }
}
```

**Agent strategy:** Check that you're passing the API key correctly. This is never a transient error — don't retry.

```javascript
if (error.code === "UNAUTHORIZED") {
  throw new Error("Slotflow API key is invalid. Check SLOTFLOW_API_KEY env var.");
}
```

### NOT_FOUND (404)

**When:** The resource doesn't exist, belongs to another organization, or (for humans) has been soft-deleted.

```json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Human not found.",
    "status": 404
  }
}
```

**Agent strategy:** Verify the ID is correct. If the human was recently deleted (soft delete), they won't appear in API responses anymore.

### SLOT_UNAVAILABLE (409)

**When:** Another booking was created for the same time slot between your agent's `GET /slots` call and your `POST /bookings` call. This is a race condition — it's expected in high-volume environments.

```json
{
  "error": {
    "code": "SLOT_UNAVAILABLE",
    "message": "The requested slot is no longer available.",
    "status": 409
  }
}
```

**Agent strategy:** This is the most important error to handle. Retry with the next available slot:

```javascript
async function bookWithRetry(humanId, duration, attendee, metadata, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    // Always re-query slots — previous results are stale
    const { slots } = await getSlots(humanId, duration);
    if (slots.length === 0) return null;

    const res = await createBooking({
      human_id: humanId,
      starts_at: slots[0].starts_at,
      duration,
      ...attendee,
      metadata,
    });

    if (res.status === 201) return await res.json();
    if (res.status !== 409) break; // Only retry on slot conflicts
  }
  return null;
}
```

### INVALID_DURATION (422)

**When:** The `duration` parameter doesn't match any of the human's configured `meeting_durations`.

```json
{
  "error": {
    "code": "INVALID_DURATION",
    "message": "Duration 45 is not allowed. Allowed: 30, 60 minutes.",
    "status": 422
  }
}
```

**Agent strategy:** Fetch the human to check their allowed durations, then retry with a valid one:

```javascript
if (error.code === "INVALID_DURATION") {
  const human = await getHuman(humanId);
  const validDurations = human.availability_rules.meeting_durations;
  // Use the closest valid duration
}
```

### OUTSIDE_WORKING_HOURS (422)

**When:** The booking slot falls outside the human's working days or hours, and no open override covers it.

```json
{
  "error": {
    "code": "OUTSIDE_WORKING_HOURS",
    "message": "Slot falls outside working hours.",
    "status": 422
  }
}
```

**Agent strategy:** This should rarely happen if your agent uses the `GET /slots` endpoint first. The slots endpoint only returns valid times. If you see this error, it means the agent is trying to book a time directly without checking slots.

### SCHEDULE_BLOCKED (422)

**When:** The booking slot overlaps with a block override (vacation, recurring meeting, etc.).

```json
{
  "error": {
    "code": "SCHEDULE_BLOCKED",
    "message": "Slot falls within a schedule block.",
    "status": 422
  }
}
```

**Agent strategy:** Same as OUTSIDE_WORKING_HOURS — use `GET /slots` first, which already filters out blocked times.

### LIMIT_REACHED (402)

**When:** Your organization has used all its monthly booking quota.

```json
{
  "error": {
    "code": "LIMIT_REACHED",
    "message": "Monthly booking limit reached. Upgrade your plan to continue.",
    "status": 402
  }
}
```

**How limits work by plan:**

| Plan                                   | Behavior when limit hit                                                 |
| -------------------------------------- | ----------------------------------------------------------------------- |
| **Free**                               | Hard block. Returns LIMIT_REACHED.                                      |
| **Starter/Growth** (auto-purchase OFF) | Hard block. Returns LIMIT_REACHED.                                      |
| **Starter/Growth** (auto-purchase ON)  | Auto-buys a \$19/500-booking overage pack via Stripe. Booking succeeds. |

**Agent strategy:** On free plans, inform the user their limit is reached. On paid plans, suggest enabling auto-purchase in the dashboard.

### VALIDATION_ERROR (422)

**When:** Missing or malformed fields in the request body.

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "name and timezone are required.",
    "status": 422
  }
}
```

**Agent strategy:** Check the `message` field — it tells you which fields are missing or invalid. This is a developer error, not a runtime error.

## Best practices for AI agents

1. **Always use `GET /slots` before `POST /bookings`** — this prevents most OUTSIDE_WORKING_HOURS and SCHEDULE_BLOCKED errors
2. **Handle 409 with retry logic** — slot conflicts are expected, not exceptional
3. **Check error `code`, not `message`** — messages may change, codes are stable
4. **Log errors with context** — include the human_id, requested time, and error code for debugging
5. **Don't retry on 401, 404, or 422** — these are not transient errors