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 with mai_.

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.

  • 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.

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 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