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.
On this page
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 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 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 withmai_.
Send the key as a bearer token on every request.
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.
messagesneeds at least one message, or the answer is400with "Send at least one message in messages."modelis optional and defaults tomodulify/fast.ncan only be1. Anything else answers400.toolstakes function tools only. Any other type answers400withunsupported_tools, and so does sending tools to a model that cannot use them, such asperplexity/sonar.perplexity/sonarlists the pages it used inannotations, on the message or, in a stream, on thedeltaof a chunk. Each one holds aurl_citationwith itsurlandtitle.- A
max_tokensabove 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 withfinish_reasonset tolength.
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.
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.
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."
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.
{
"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. 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
429until the minute rolls on. - A request body can be up to 16 MB, images included.
- The History tab keeps each request for 30 days.