# AI HTTP API

Source: https://modulify.ai/docs/built-in/ai-http-api

The OpenAI compatible endpoints your site calls for chat, text and embeddings, how they authenticate, every error they return and the limits that apply.

Your site's AI is an OpenAI compatible API. Any library that can talk to OpenAI works with it once you point it at your site's base URL and key, so there is no Modulify package to install. See [AI](https://modulify.ai/docs/built-in/ai) for the tab, the prices and how credits are taken. This page is for knowing exactly what the endpoints accept and return.

## Authentication

Every site has two [managed secrets](https://modulify.ai/docs/data/secrets) for AI, created on their own the next time the preview starts or the site is published:

- `MODULIFY_AI_URL`, the base URL, which ends in `/v1`. Every path below goes after it.
- `MODULIFY_AI_KEY`, the site's own key. Keys start with `mai_`.

Send the key as a bearer token on every request.

```bash
curl "$MODULIFY_AI_URL/chat/completions" \
  -H "Authorization: Bearer $MODULIFY_AI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "modulify/fast", "messages": [{"role": "user", "content": "Say hello"}]}'
```

Read both values on the server only: in an API route, a route handler, a server action, `getServerSideProps` or `getStaticProps`. The key spends your workspace credits, so never send it to the browser and never put it in client code.

A key belongs to exactly one site. Rotating it in the **AI** tab stops the old key within a minute, and your published site keeps the old key until you publish again, so its AI answers `401` until then.

## Endpoints

| Method | Path | Body | Returns |
|---|---|---|---|
| POST | `/chat/completions` | An OpenAI chat completion request | A chat completion, or a stream of chunks |
| POST | `/embeddings` | `{ model, input, dimensions, encoding_format }` | A list of embeddings |
| GET | `/models` | None | Every model with its price |

Responses use the OpenAI shapes, not the `{ success, message, data }` envelope of the other Modulify APIs. Every answer carries an `X-Request-Id` header, and a successful one names the model that answered in `X-Modulify-Ai-Model`.

### /chat/completions

Takes the OpenAI chat format: `messages`, `model`, `stream`, `temperature`, `top_p`, `max_tokens` or `max_completion_tokens`, `stop`, `seed`, `presence_penalty`, `frequency_penalty`, `response_format` (including `json_schema`), `tools`, `tool_choice`, `parallel_tool_calls`, `reasoning_effort` and `user`. Images go in as `image_url` parts of a message, as a link or a data URL. Any other field is dropped before the request leaves Modulify.

- `messages` needs at least one message, or the answer is `400` with "Send at least one message in messages."
- `model` is optional and defaults to `modulify/fast`.
- `n` can only be `1`. Anything else answers `400`.
- `tools` takes function tools only. Any other type answers `400` with `unsupported_tools`, and so does sending tools to a model that cannot use them, such as `perplexity/sonar`.
- `perplexity/sonar` lists the pages it used in `annotations`, on the message or, in a stream, on the `delta` of a chunk. Each one holds a `url_citation` with its `url` and `title`.
- A `max_tokens` above what the model can write is lowered to that model's maximum.
- Some models think before they answer, and that thinking counts toward `max_tokens`. Leave at least a few hundred tokens, or the answer can come back empty with `finish_reason` set to `length`.

With `stream: true` the answer is a stream of server sent events. Each `data:` line holds one chunk in the OpenAI format, a line starting with `:` is a keep alive you can ignore, the last chunk carries `usage`, and the stream ends with `data: [DONE]`. If the provider stops answering partway through, the stream ends with a chunk holding an `error` object instead.

```typescript
import OpenAI from 'openai'

const ai = new OpenAI({
    baseURL: process.env.MODULIFY_AI_URL,
    apiKey: process.env.MODULIFY_AI_KEY
})

const completion = await ai.chat.completions.create({
    model: 'modulify/fast',
    messages: [{ role: 'user', content: 'Write a one line welcome for a pottery shop.' }],
    max_tokens: 200
})

const text = completion.choices[0]?.message?.content || ''
```

For a chat interface that streams into the page, the AI SDK works the same way.

```typescript
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
import { streamText } from 'ai'

const modulify = createOpenAICompatible({
    name: 'modulify',
    baseURL: process.env.MODULIFY_AI_URL || '',
    apiKey: process.env.MODULIFY_AI_KEY
})

const result = streamText({
    model: modulify('modulify/fast'),
    prompt: 'Suggest three names for a coffee bar.'
})
```

### /embeddings

Turns text into vectors for search and "chat with your documents" features. `input` is a string or a list of strings, and `model` defaults to `modulify/embed`. An empty `input` answers `400` with "Send the text to embed in input."

```typescript
const result = await ai.embeddings.create({
    model: 'modulify/embed',
    input: ['Opening hours', 'Shipping and returns']
})

const vectors = result.data.map(item => item.embedding)
```

Keep using the same embedding model for everything you store and search, because vectors from two different models cannot be compared. `modulify/embed` never switches to another model for that reason.

### /models

Lists every model with its kind, context length, the most it can write in one answer and its list price in US dollars per million tokens, before Modulify's margin. The **AI** tab shows the same models priced in credits for your plan. This endpoint needs no key.

| Name | Kind | What it does |
|---|---|---|
| `modulify/fast` | chat | Quick, low cost answers. Moves to another provider on its own when one is down |
| `modulify/smart` | chat | Stronger reasoning and writing. Moves to another provider on its own when one is down |
| `modulify/embed` | embedding | Embeddings for search, always the same model |

Besides these three names, you can ask for a specific model by the id `/models` lists, such as `anthropic/claude-sonnet-5.5`, `openai/gpt-6.1-sol` or `google/gemini-3.8-flash`. A specific model stays on that model.

## Errors

Errors use the OpenAI error shape, so OpenAI libraries raise them as their usual error types.

```json
{
    "error": {
        "message": "AI is paused because the workspace has no credits left. Add credits and it starts again on its own.",
        "type": "insufficient_credits",
        "code": "no_credits",
        "param": null
    }
}
```

| Status | Code | Meaning |
|---|---|---|
| `400` | `invalid_body`, `missing_messages`, `missing_input`, `unsupported_n`, `unsupported_tools`, `wrong_model_kind` | The request is not valid. Fix it before trying again |
| `401` | `missing_api_key`, `invalid_api_key` | No key, a key that is not valid, or a key that was replaced |
| `402` | `no_credits` | The workspace has no credits left. AI starts again on its own once credits are added |
| `402` | `monthly_cap_reached` | The workspace reached its monthly AI cap or its monthly spending cap. The workspace owner raises it in **Settings** > **Spending** |
| `403` | `ai_off` | AI is turned off for this site in the **AI** tab |
| `404` | `model_not_found` | No model has that name. Check `/models` |
| `413` | `body_too_large` | The request body is over 16 MB |
| `429` | `rate_limited` | The site sent too many requests in the last minute. Wait for the `Retry-After` seconds |
| `502`, `503` | `upstream_unavailable`, `meter_unavailable` | Every provider failed to answer, or the site's AI is still starting. Try again after `Retry-After` |

A request the provider refuses, for example one longer than the model's context, comes back with the provider's status and its message, under the code `upstream_error`.

Show visitors a friendly message when a call fails, and log the error so it appears in [Logs](https://modulify.ai/docs/editor/logs). Never retry in a tight loop: wait for `Retry-After` on `429`, `502` and `503`.

## Limits

- Each site can send 600 requests a minute. The next one answers `429` until the minute rolls on.
- A request body can be up to 16 MB, images included.
- The **History** tab keeps each request for 30 days.

## Next

- [AI](https://modulify.ai/docs/built-in/ai): The tab, the prices in credits, pausing and the workspace caps.

- [Emails](https://modulify.ai/docs/built-in/emails): Send email from your site the same way, with nothing to set up.