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

# Conventions

> Base URL, content type, error envelope, rate limits, idempotency, pagination, timestamps

## Base URL

```
https://api.myustadia.com
```

Substitute your assigned host if you are on a dedicated deployment. All endpoint paths in this documentation are relative to the base URL.

## Content type

All request and response bodies are JSON. Set `Content-Type: application/json` on every `POST`, `PUT`, and `PATCH`.

## Error envelope

Every error response uses this shape:

```json theme={null}
{
  "detail": {
    "error": {
      "type": "not_found_error",
      "message": "course not found",
      "code": "course_not_found"
    }
  }
}
```

| Field     | Description                                                                                                                                               |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`    | High-level category. One of: `authentication_error`, `invalid_request_error`, `not_found_error`, `rate_limit_error`, `quota_exceeded`, `server_error`     |
| `message` | Human-readable description                                                                                                                                |
| `code`    | Machine-readable sub-code (optional). Examples: `course_not_found`, `preset_not_found`, `idempotency_key_required`, `retry_after_60`, `regen_cap_reached` |
| `param`   | Field name if the error is parameter-specific (optional)                                                                                                  |

See the [errors page](/errors) for the full code reference.

## Rate limits

Default: **60 requests per minute per account**.

Exceeding the limit returns `429`:

```json theme={null}
{
  "detail": {
    "error": {
      "type": "rate_limit_error",
      "code": "retry_after_60"
    }
  }
}
```

The `Retry-After` response header gives the number of seconds to wait. Implement exponential backoff with jitter for production traffic.

## Idempotency

`POST /api/v1/courses/{course_id}/launch` requires an `Idempotency-Key` header. The key is a unique string per intended launch (UUIDs work well).

* First call with a given key starts the generation.
* Subsequent calls with the same key while the course is already past `awaiting_brief` return the existing course without re-launching.
* A missing key returns `400` with `code: idempotency_key_required`.

Use a fresh key for each new intended launch. Do not reuse keys across courses or across retries that you intend to be distinct operations.

## Pagination

List endpoints currently return arrays without cursor pagination:

* `GET /api/v1/courses` accepts `?external_ref=` to narrow results.
* `GET /api/v1/presets/{id}/courses` takes no query parameters and returns all courses for the preset.

Cursor pagination will be added before the per-account course count makes flat lists impractical. The endpoint signature is forward-compatible.

## Timestamps

All timestamps are ISO-8601 strings in UTC, e.g. `2026-06-23T10:30:00Z`. Do not assume a local timezone in your parser.

## external\_ref idempotency

`external_ref` is an optional caller-supplied string (max 255 chars) on course create. When provided, it acts as an idempotency key scoped to your account: if you POST a course with the same `external_ref` a second time, the API returns the existing course instead of creating a duplicate. The response shape is identical to a fresh create.

Use `external_ref` to tie a course to an entity in your own system (a company ID, an onboarding session, a campaign run). It also powers per-ref breakdowns in the billing report.

```bash theme={null}
curl -X POST "https://api.myustadia.com/api/v1/presets/$PRESET_ID/courses" \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"external_ref": "acme-emea-q3-2026", "language": "fr"}'
```

A `null` or absent `external_ref` disables idempotency: each call creates a new course.

## Cancel SLA

`POST /api/v1/courses/{id}/cancel` returns `200` immediately after setting the cancel signal. The worker picks up the signal within \~30 seconds (its heartbeat cycle), then finalizes the course to `cancelled` at the **next pipeline-stage boundary**. A long in-flight stage (for example video assembly) can push the actual transition well past 30 seconds, so do not assume a fixed deadline. Always poll `GET /api/v1/courses/{id}/status` and wait for `status: "cancelled"` before treating the course's quota as freed.

Do not reuse the course's `external_ref` for a fresh course while the original is still in a non-terminal state.

## Concurrency

Accounts have a per-account parallel generation cap. Submitting a launch when the cap is reached returns `429 quota_exceeded`. Poll status on your in-flight courses and retry after one reaches a terminal state (`ready`, `error`, or `cancelled`).

Completion order is not guaranteed: two courses launched at the same time may finish in any order depending on source complexity and module count. Drive your downstream logic from per-course status, not from submission order.

## Source limits

The following hard limits apply to file and URL sources on course and preset create:

| Limit                                             | Value |
| ------------------------------------------------- | ----- |
| Max URLs per request                              | 10    |
| Max files per request                             | 5     |
| Max size per file                                 | 25 MB |
| Max total file bytes                              | 50 MB |
| Max PDF pages (across all PDFs in one submission) | 300   |

Accepted file types for **course** sources: `.pdf`, `.pptx`, `.docx`, `.txt`.

Accepted file types for **preset** brand sources (superset): `.pdf`, `.pptx`, `.docx`, `.txt`, `.md`, `.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.gif`.

Exceeding any limit returns `400 invalid_request_error`.

## Skipping optional brief questions

Brief questions can be `blocking` (must be answered to proceed) or optional. To skip an optional question without providing an answer, set `"skip": true` in the answers payload:

```json theme={null}
{
  "answers": [
    { "question_id": "q_tone_preference", "skip": true }
  ]
}
```

Attempting to skip a blocking question returns `422`. Optional questions that are skipped do not re-appear in subsequent `brief/next` calls.

## Versioning

The current API version is **v1** (the `/api/v1/...` prefix). The conceptual model is sometimes referred to as "v2" because it succeeded an earlier preset-less v1; the URL path is `v1` for historical reasons.

Breaking changes to existing endpoints will introduce a new path prefix (`/api/v2/...`) and the old prefix will remain operational for at least 12 months.
