# Model Endpoints

> FlyMy.AI model endpoints: synchronous prediction, streaming, async tasks, authentication, request and response formats.

Every model on FlyMy.AI is addressed by its **endpoint id**, written as
`{owner}/{model}` - for example `flymyai/google-gemini-25-flash`. The endpoint
id maps directly into the URL path.

```
https://api.flymy.ai/api/v1/{owner}/{model}/{route}
```

## Authentication

All routes take an API key in the `X-API-KEY` header:

```bash
-H "X-API-KEY: fly-***"
```

Create keys in your FlyMy.AI account settings. A key can call any public model
plus every model you own.

## Routes

| Route | Purpose |
| --- | --- |
| `POST /predict` | Run the model and wait for the complete result |
| `POST /predict/stream/` | Run the model and receive the result incrementally over SSE |
| `POST /predict/async/` | Queue the run and return its `prediction_id` immediately |
| `GET /predict/async/result/?request_id=<prediction_id>` | Fetch the result of a queued run; `POST` here returns `405 Method Not Allowed` |
| `POST /predict/cancel/` | Cancel a running prediction by its `prediction_id` |
| `GET /openapi.json` | The model's own schema: exact inputs, types, and output fields |

Not every model supports every route. Text generation models are
streaming-only and reject `/predict`; see [Using LLMs](https://docs.flymy.ai/guides/use-llms.md).

## Retrying an async run safely

Send an `Idempotency-Key` header with `POST /predict/async/` when a retry must
not start a second paid run. The key is 1-255 printable ASCII characters with no
leading or trailing spaces; use a new key for each run you intend.

```bash
curl -X POST https://api.flymy.ai/api/v1/flymyai/nano-banana/predict/async/ \
  -H "X-API-KEY: fly-***" \
  -H "Idempotency-Key: icon-run-7f3c2a" \
  -F "prompt=A blue circle, flat icon"
```

- **Same key, same input:** `202` with the first run's `prediction_id`. Nothing
  runs or is charged again.
- **Same key, other input:** `409` with `code: idempotency_key_reused`, before
  anything runs.
- **The first request is still being submitted:** `409` with
  `code: idempotency_key_in_flight`. Send the same request again a moment later.
- **The first submission broke off after the run may have started:** `409` with
  `code: tool_outcome_unknown`. The run may exist, so check your results before
  starting it again under a new key.

Without the header every request queues a new run. `/predict` and
`/predict/stream/` do not keep keys. The Agents MCP `run_model` sends its
`operation_key` as this header for image and video models.

## Discovering a model's inputs

Rather than guessing parameter names, read the schema the model publishes:

```bash
curl -H "X-API-KEY: fly-***" \
  https://api.flymy.ai/api/v1/flymyai/google-gemini-25-flash/openapi.json
```

The input fields live under `components.schemas.DynamicInputModel`, and the
output fields under `DynamicOutputModel`. A field's `description` states its
limits when the model has them, such as allowed durations, file types or a
minimum video size. An output field marked
`"isStream": true` means the value is delivered progressively and the model
should be called through `/predict/stream/`.

## Request format

Requests are sent as **form data**, not JSON - this is what allows file inputs
such as images and audio to travel in the same request as text parameters.

```bash
curl -X POST \
  https://api.flymy.ai/api/v1/flymyai/google-gemini-25-flash/predict/stream/ \
  -H "X-API-KEY: fly-***" \
  -F "prompt=Hello"
```

A JSON body returns `422 Field required` even when the JSON itself is valid.

A list input goes as one field per item, and an object as its JSON text:

```bash
curl -X POST \
  https://api.flymy.ai/api/v1/flymyai/gemini-3_8-flash-vlm/predict \
  -H "X-API-KEY: fly-***" \
  -F "prompt=How many photos show a dog?" \
  -F "media_url=https://example.com/1.jpg" \
  -F "media_url=https://example.com/2.jpg" \
  -F 'response_schema={"type":"object","properties":{"count":{"type":"integer"}}}'
```

The Python SDK (`flymyai` 1.2.0rc10 and later) sends lists and objects this
way for you.

## Response format

### Synchronous

`/predict` returns a single JSON document with the model's outputs under
`output_data`.

### Streaming

`/predict/stream/` returns a Server-Sent Events stream. The first event
acknowledges the request and carries the `prediction_id` used for cancellation:

```
event: {"prediction_id":"f9e443caf50e..."}
```

Content events follow, each carrying a fragment of the output:

```
data: {"output_data":{"output":["first fragment "]},"status":200}
```

The stream closes with a final event whose `output_data` is empty and which
carries usage information. For token-priced models this is `stream_details`
with the token counts the charge is computed from:

```
data: {"output_data":{},"status":200,"stream_details":{"input_tokens":15,"output_tokens":49}}
```

Disable client-side buffering when consuming a stream, otherwise your HTTP
client may hold the fragments and hand you the whole response at the end. With
curl, pass `-N`.

## Errors

Transport-level failures use standard HTTP status codes. Some conditions are
reported **inside** a 200 response body with their own `status_code` field, so
check the payload rather than the HTTP status alone:

```json
{"detail":"Predict is not available for current pipeline. Use /predict/stream route instead!","status_code":421}
```

| Status | Meaning |
| --- | --- |
| `403 API key does not have access to the project` | Model is private and you are not its owner |
| `403 Insufficient funds` | Wallet balance is negative |
| `403 Project is not active` | The project has no capacity allocated |
| `404 Project not found` | The endpoint id does not exist |
| `422 Field required` | A required parameter is missing, or the body was sent as JSON |

When a model refuses the request itself, the message stays in `detail` and,
if the model says why, `reason` comes next to it - for example a Vision prompt
over its 100,000-character limit:

```json
{"output_data":{"detail":"Vision API returned HTTP 400","reason":"... String should have at most 100000 characters [string_too_long]"},"status":400}
```

Keep matching on the status and `detail`; `reason` is for people and logs, and
a refusal without one looks as before.

See [Errors](https://docs.flymy.ai/errors.md) for the full list.

## Related

- [Using LLMs](https://docs.flymy.ai/guides/use-llms.md) - text generation, streaming, token billing
- [Queues](https://docs.flymy.ai/api/queue.md) - long-running and batched work
- [Model usage data](https://docs.flymy.ai/api/usage.md) - per-request usage and price
- [api.flymy.ai/docs](https://api.flymy.ai/docs) - interactive OpenAPI reference

## Typed decisions

For fast classification, routing, triage, fixed-enum extraction and yes/no gates, use [TypeSafe Jev 1.13](https://docs.flymy.ai/guides/typesafe-jev). It accepts `state` and typed `questions` through its JSON Decisions endpoint and returns Choice, Noul probability or Score answers. It is not a chat model and does not generate prose, code or media. Both MCP servers expose `make_typed_decision`; agents discover `typesafe_jev.decide`.
