# Requests

A request is one call of one model. It has an id (`req_…`), a status, its cost in API credits (1000 = 1 USD), and, when it
succeeds, the generated files. The same object comes back from every endpoint below, for every model.

## Find a model

The catalog of all 1,496 models is public JSON (no key needed):

```bash
curl "https://api.doitong.ru/v1/models?category=text-to-video&lab=Google&limit=20"
```

| Query parameter | Meaning |
|---|---|
| `q` | Words that must all appear in the name, id, maker or tags, for example `kling 3`. |
| `category` | One category id, for example `text-to-video` or `upscale`. `GET https://api.doitong.ru/v1/categories` lists all 21 with their counts. |
| `lab` | The model's maker, by name or id, for example `Google` or `black-forest-labs`. |
| `family` | A `family` id from a previous row: every variant of one model (pro, fast, image-to-video, …). |
| `mode` | `exact` (the price is computed from the input), `measured` (from the length or size of the input files), `estimate` (an estimated price per run) or `metered` (charged by actual usage, up to a maximum per run). |
| `limit`, `cursor` | Page size (1 to 100, default 50) and the `next_cursor` of the previous page. |

Each row has `id`, `name`, `lab`, `category`, `family`, `summary`, `tags`, `output_kind`, `docs_url` and a `price`
object (see [Pricing](https://doitong.com/developers/docs/pricing#price-objects)). `GET https://api.doitong.ru/v1/models/{model_id}` adds the
full input and output JSON Schema, the canonical parameter names, the price rules and an example input. An unknown
`category` or `mode` is `400 invalid_parameter`.

## Run a model

```bash
curl -X POST "https://api.doitong.ru/v1/run/{model_id}?wait=20" \
  -H "Authorization: Bearer $DOITONG_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1842-cover" \
  -d '{"prompt": "..."}'
```

- The body is the model input, a JSON object. Each model page lists its parameters.
- The input is checked before anything is charged. An invalid input returns `422 invalid_input` with the offending
  `param`, and costs nothing.
- `?wait=N` (up to 20 seconds) holds the call until the request finishes or the time is up.
- `?webhook_url=https://…` sends a signed POST to that URL when the request finishes (see [Webhooks](https://doitong.com/developers/docs/webhooks)).
- `?storage=permanent` or `?storage=temporary` chooses how long the output files are kept; without it the account
  setting applies (see [Where files live](#storage)).

The response is `202 Accepted` while the request is still working, and `200 OK` when it finished within the wait (or
when it is a replay, see below). The `Location` and `Doitong-Request-Id` headers carry the request URL and id.

## Statuses

Lifecycle: `queued` → `running` → `succeeded` | `failed` | `canceled` (final; failed and canceled are refunded).

| Status | Meaning | Final |
|---|---|---|
| `queued` | Accepted and charged, waiting for a worker. `queue_position` may show the place in line. | no |
| `running` | The model is generating. | no |
| `succeeded` | Done. `outputs` lists the files. | yes |
| `failed` | The model or a limit stopped it. `error` says why. The API credits are refunded. | yes |
| `canceled` | Canceled before it produced a result. The API credits are refunded. | yes |

A request that runs longer than its time limit fails with code `timeout` and is refunded: 15 minutes for images,
60 minutes for video, 30 minutes for audio and everything else.

## The request object

```json
{
  "id": "req_8k2m4q7x1z9c3v5b6n0p2r4t",
  "object": "request",
  "model": "flux/schnell",
  "status": "succeeded",
  "created_at": "2026-10-20T12:00:00Z",
  "started_at": "2026-10-20T12:00:01Z",
  "completed_at": "2026-10-20T12:00:02Z",
  "queue_position": null,
  "cost": { "credits": 3, "usd": "0.003", "charged": 3, "refunded": 0 },
  "input": { "prompt": "..." },
  "output": { "images": [ { "url": "https://content.doitong.com/dev-api/…/0-output.jpeg", "width": 1024, "height": 768 } ] },
  "outputs": [
    { "type": "image", "url": "https://content.doitong.com/dev-api/…/0-output.jpeg", "content_type": "image/jpeg",
      "width": 1024, "height": 768, "file_size": 183920, "stored": true }
  ],
  "error": null,
  "metrics": { "inference_seconds": 1.2 },
  "storage": { "mode": "permanent", "bytes": 183920, "expires_at": null, "expired": false },
  "urls": {
    "self": "https://api.doitong.ru/v1/requests/req_8k2m4q7x1z9c3v5b6n0p2r4t",
    "cancel": "https://api.doitong.ru/v1/requests/req_8k2m4q7x1z9c3v5b6n0p2r4t/cancel"
  }
}
```

- `outputs` is the same for every model: one entry per generated file with `type` (`image`, `video`, `audio`,
  `file` or `3d`), a `url` on our storage, `content_type` and, when known, `file_size`, `width`, `height`,
  `duration`.
- `output` is the model's own result object (its fields are listed on each model page) with the same URLs.
- `storage` says how the files are kept: `mode` (`permanent` or `temporary`), `bytes` stored, `expires_at` (when a
  temporary file is deleted, `null` for permanent ones) and `expired` (`true` once the files are gone).
- `cost.credits` is the price of the call in API credits (`cost.usd` in USD), `cost.charged` what was debited from
  the API balance, `cost.refunded` what came back. For a `metered` model `cost.credits` is the maximum held at the
  start (`cost.held`); once the run succeeds `cost.settled` says what the usage came to and the rest of the maximum is
  in `cost.refunded` (see [Charged by usage](https://doitong.com/developers/docs/pricing#metered)).
- When a request fails, `error` is `{ "type": …, "code": …, "message": … }`.

## Read a request

```bash
curl "https://api.doitong.ru/v1/requests/req_8k2m4q7x1z9c3v5b6n0p2r4t?wait=20" \
  -H "Authorization: Bearer $DOITONG_API_KEY"
```

- `?wait=N` (up to 20 seconds) long-polls: the call returns as soon as the request is final, or after N seconds with
  the current state. Looping on this is the simplest way to wait for a result.
- `?logs=1` adds the model's progress log lines (the last 50) as `logs`.

You can only read requests of your own account; any other id is `404 request_not_found`.

## List requests

```bash
curl "https://api.doitong.ru/v1/requests?limit=20&status=succeeded&model=flux/schnell" \
  -H "Authorization: Bearer $DOITONG_API_KEY"
```

Newest first. `limit` is 1 to 100 (default 20), `status` and `model` filter. The response is
`{ "object": "list", "data": [ … ], "has_more": true, "next_cursor": "req_…" }`; pass `cursor=<next_cursor>` for
the next page.

## Cancel a request

```bash
curl -X POST "https://api.doitong.ru/v1/requests/req_8k2m4q7x1z9c3v5b6n0p2r4t/cancel" \
  -H "Authorization: Bearer $DOITONG_API_KEY"
```

- Canceled before the model started: the status becomes `canceled`, the API credits are refunded, and you get `200`.
- Already generating: the cancel is passed on and you get `202` with `"cancel_requested": true`. If the model
  finishes anyway, the request succeeds normally and is charged.
- Already final: `409 not_cancelable` (a request that is already `canceled` simply returns `200`).

## Delete the files of a request

```bash
curl -X DELETE "https://api.doitong.ru/v1/requests/req_8k2m4q7x1z9c3v5b6n0p2r4t/files" \
  -H "Authorization: Bearer $DOITONG_API_KEY"
```

Deletes our copies of the request's output files now and stops their storage charge. The response is the request
object with `storage.expired: true`; its URLs stay in the record but no longer work. Calling it again changes nothing
(`200`). A request that is still `queued` or `running` has no files yet: `409 request_active`. Needs `models:run`.

## Idempotency: retry without paying twice

Networks fail. To retry a run safely, send an `Idempotency-Key` header (any string up to 255 characters, unique per
operation, for example your own order id):

- Same key and the same body as before: you get the original request back (`200`, header
  `Idempotent-Replayed: true`), and nothing is charged again.
- Same key and body after a run that failed before it reached the model (`upstream_error` or `internal_error` on
  submit, `insufficient_credits`, `budget_exceeded`, `too_many_inflight`): nothing was produced and nothing is
  charged, so the retry runs the model again instead of returning the failure.
- Same key with a different body: `409 idempotency_mismatch`.
- Keys are scoped to the API key that sent them.

## Where files live

Every output file is copied to our storage before the request is marked `succeeded`, so the URLs in `outputs` are on
our own hosts and you can link to them directly. How long they stay is your choice:

| Mode | How long | Price |
|---|---|---|
| `permanent` (default) | Until you delete them | 100 API credits ($0.10) per GB per month, charged daily from the API balance |
| `temporary` | 7 days, then deleted | Free |

- The account setting decides for new requests: the "Keep files forever" switch in the
  [dashboard](https://doitong.com/developers/dashboard#balance), or `PATCH https://api.doitong.ru/v1/account/storage` with
  `{"mode": "temporary"}`. `GET https://api.doitong.ru/v1/account` shows it under `storage`, with the bytes you keep and the
  monthly estimate.
- `?storage=permanent|temporary` on a run (the `storage` argument of the MCP `run` tool) overrides it for that run.
- Changing the setting never touches files you already have; delete those with
  [`DELETE /requests/{id}/files`](#delete-files).
- When the API balance cannot pay for storage, nothing is charged and you get an email: top up within
  30 days, or the kept files become temporary and are deleted 7 days later.

In the rare case that our copy fails, the entry has `"stored": false` and the URL is the original, short-lived one:
download it soon.
