> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hawkings.education/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors & retries

> Error taxonomy, idempotency, automatic retries.

The SDK throws typed errors. Catch the parent class to handle anything,
or catch a specific subclass for fine-grained logic.

<CodeGroup>
  ```ts TypeScript theme={null}
  import Hawkings, {
    HawkingsError,
    AuthenticationError,
    RateLimitError,
    InvalidRequestError,
    APIError,
  } from "@hawkings/sdk";

  try {
    await hk.courses.create({ name: "" });
  } catch (err) {
    if (err instanceof InvalidRequestError) {
      console.error(err.fields); // { name: "is required" }
    }
  }
  ```

  ```php PHP theme={null}
  use Hawkings\Exception\ApiError;
  use Hawkings\Exception\AuthenticationError;
  use Hawkings\Exception\RateLimitError;
  use Hawkings\Exception\InvalidRequestError;

  try {
      $hk->courses->create(['name' => '']);
  } catch (InvalidRequestError $e) {
      error_log(print_r($e->fields, true)); // ['name' => 'is required']
  } catch (ApiError $e) {
      // catch-all for any Hawkings SDK error
  }
  ```
</CodeGroup>

In PHP the exception hierarchy is rooted at `\Hawkings\Exception\ApiError`,
with subclasses for each error type (`AuthenticationError`,
`PermissionError`, `NotFoundError`, `InvalidRequestError`,
`RateLimitError`, `IdempotencyError`, `NetworkError`). Catch `ApiError`
to handle anything.

## Error taxonomy

| Class                 | HTTP | When it fires                                                | Retryable? |
| --------------------- | ---- | ------------------------------------------------------------ | ---------- |
| `AuthenticationError` | 401  | Bad/expired API key, missing scope.                          | No         |
| `PermissionError`     | 403  | Key valid, but no access to this resource.                   | No         |
| `NotFoundError`       | 404  | Resource doesn't exist or is in another workspace.           | No         |
| `InvalidRequestError` | 422  | Validation failed. Inspect `err.fields`.                     | No         |
| `RateLimitError`      | 429  | Too many requests. `err.retry_after` is the seconds to wait. | Yes (auto) |
| `IdempotencyError`    | 409  | Same `idempotency_key` reused with a different payload.      | No         |
| `APIError`            | 5xx  | Hawkings is having a moment. Auto-retried with backoff.      | Yes (auto) |
| `NetworkError`        | n/a  | DNS, TLS, timeout. Auto-retried with backoff.                | Yes (auto) |

## Automatic retries

The SDK retries 429 and 5xx responses automatically with exponential
backoff and jitter. Defaults:

<CodeGroup>
  ```ts TypeScript theme={null}
  new Hawkings({
    max_retries: 3,         // default
    retry_delay_ms: 500,     // base; doubles each attempt
    retry_on: ["429", "5xx", "network"],
  });
  ```

  ```php PHP theme={null}
  new \Hawkings\Client([
      'max_retries' => 3,         // default
      'retry_delay_ms' => 500,    // base; doubles each attempt
      'retry_on' => ['429', '5xx', 'network'],
  ]);
  ```
</CodeGroup>

Disable for a specific call:

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.courses.create({ name: "..." }, { max_retries: 0 });
  ```

  ```php PHP theme={null}
  $hk->courses->create(['name' => '...'], ['max_retries' => 0]);
  ```
</CodeGroup>

## Idempotency keys

Any `create*`, `update*`, or AI generation call can carry an
`Idempotency-Key`. Same key + same body = same response, even if you
call it 100 times. Different body with the same key throws
`IdempotencyError`.

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.submissions.create(
    { assignmentId, studentId, content },
    { idempotency_key: "subm-2026-05-10-abc" },
  );
  ```

  ```php PHP theme={null}
  $hk->submissions->create(
      ['assignment_id' => $assignmentId, 'student_id' => $studentId, 'content' => $content],
      ['idempotency_key' => 'subm-2026-05-10-abc'],
  );
  ```
</CodeGroup>

Idempotency records live for 24 hours.

## Error envelope

Every error response carries a structured body:

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_field",
    "message": "name is required.",
    "fields": { "name": "is required" },
    "request_id": "req_01HX9..."
  }
}
```

When opening a support ticket, attach `request_id` — it's all we need to
trace a request end-to-end.
