Moenjo API
The API follows the OpenAI chat completions format, so the OpenAI SDKs and most existing tools work once you change the base URL and the key.
https://moenjo.com/api/v1Quickstart
Create a key in your dashboard, then send a message. The model id is moenjo-1.0.
curl https://moenjo.com/api/v1/chat/completions \
-H "Authorization: Bearer $MOENJO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "moenjo-1.0",
"messages": [
{"role": "user", "content": "Pakistan ke sab se bare shehar kaun se hain?"}
]
}'Authentication
Send your key in the Authorization header as a bearer token. Keys start with mj_live_ and are shown in full only once, when you create them.
Authorization: Bearer mj_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEach key is billed either to your subscription or to your pay-as-you-go credit. You choose when you create the key and can switch it later. Keep keys on your server: anyone who has one can spend your limits or credit.
Chat completions
POST https://moenjo.com/api/v1/chat/completions
| Parameter | Type | What it does |
|---|---|---|
model | string, required | The model id, for example moenjo-1.0. |
messages | array, required | The conversation so far. Each item has a role (system, user, assistant or tool) and content. |
stream | boolean | Send the reply as server-sent events while it is being written. Default false. |
temperature | number, 0 to 2 | Higher values give more varied replies. Defaults to the model's own setting. |
top_p | number, 0 to 1 | Nucleus sampling. Change this or temperature, not both. |
max_tokens | integer | The most tokens the reply may use. Capped at the model's maximum output. |
stop | string or array | Up to 8 sequences that end the reply when generated. |
tools | array | Functions the model may call. See Tool calls. |
tool_choice | string or object | auto, none, required, or a specific function. |
response_format | object | Set {"type": "json_object"} to get valid JSON back. |
web_search | boolean | Moenjo extension. Lets the model search the web and open pages on our servers when a message needs current information. |
reasoning_effort | string | How hard the model thinks before answering: off, minimal, low, medium, high, xhigh or max. See Reasoning. |
thinking_budget_tokens | integer | How many tokens the model may spend thinking, up to your plan's number for the level. See Reasoning. |
seed | integer | Makes sampling repeatable where the model supports it. |
The response has the same shape as an OpenAI chat completion:
{
"id": "chatcmpl-9f2KQw1x7LmZ3vBn5TyR8cPd",
"object": "chat.completion",
"created": 1790889257,
"model": "moenjo-1.0",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Karachi, Lahore aur Faisalabad..." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 24, "completion_tokens": 58, "total_tokens": 82 }
}Streaming
Set stream to true to receive the reply in pieces. Each event is a chat.completion.chunk; the stream ends with data: [DONE]. Add "stream_options": {"include_usage": true} to get token counts in a final chunk.
stream = client.chat.completions.create(
model="moenjo-1.0",
messages=[{"role": "user", "content": "Allama Iqbal ke baare mein batao"}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="", flush=True)Images
Models with vision accept images inside a user message. Give a public URL or a base64 data URL. PNG, JPEG, WebP and GIF are supported.
reply = client.chat.completions.create(
model="moenjo-1.0",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Is tasveer mein kya hai?"},
{"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}},
],
}],
)Tool calls
Describe your functions in tools. When the model wants one, the reply has finish_reason: "tool_calls" and a tool_calls list. Run the function yourself, then send the result back as a tool message to get the final answer.
tools = [{
"type": "function",
"function": {
"name": "track_parcel",
"description": "Look up where a parcel is right now",
"parameters": {
"type": "object",
"properties": {"tracking_id": {"type": "string"}},
"required": ["tracking_id"],
},
},
}]
messages = [{"role": "user", "content": "Mera parcel LHE-48213 kahan hai?"}]
first = client.chat.completions.create(model="moenjo-1.0", messages=messages, tools=tools)
call = first.choices[0].message.tool_calls[0]
messages.append(first.choices[0].message)
messages.append({"role": "tool", "tool_call_id": call.id, "content": '{"status": "Out for delivery"}'})
final = client.chat.completions.create(model="moenjo-1.0", messages=messages, tools=tools)
print(final.choices[0].message.content)Web search
Add "web_search": true and the model can search the web and open pages when a message needs current information, such as a price, a news item or who holds a post today. The searches run on our servers, so you do not need to handle any tool calls. The pages found come back in a sources list next to choices. On a subscription key, searches count against your plan's daily search limit. On a pay-as-you-go key, each search that runs is charged at the per-search price shown on your Usage page, on top of the tokens.
curl https://moenjo.com/api/v1/chat/completions \
-H "Authorization: Bearer $MOENJO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "moenjo-1.0",
"web_search": true,
"messages": [{"role": "user", "content": "Aaj Karachi mein mausam kaisa hai?"}]
}'Searches and page reads happen inside the same request, so replies with web search take longer and use more tokens.
Reasoning
When the model thinks before it answers, its reasoning is returned separately from the answer: as message.reasoning_content in a normal response, and as delta.reasoning_content chunks when streaming. Reasoning tokens count as output tokens.
reasoning_effort sets how hard it thinks: off answers straight away, then minimal, low, medium, high, xhigh and max think for longer. Each plan allows some of these levels and has a default; GET /models lists what your key may send for each model under reasoning_efforts, with each level's limits: the max_tokens and thinking_budget_tokens used when you send none, which are also the most you may ask for. A level your plan does not include is refused with effort_not_included. Higher levels take longer and use more output tokens.
thinking_budget_tokens caps the thinking itself; reasoning tokens count inside max_tokens. A value above the level's number is refused with thinking_budget_too_large, and a max_tokens above the level's is brought down to it. Neither has any meaning at off.
reply = client.chat.completions.create(
model="moenjo-1.0",
messages=[{"role": "user", "content": "Is 221 a prime number?"}],
reasoning_effort="high",
extra_body={"thinking_budget_tokens": 2000},
)
print(reply.choices[0].message.reasoning_content)
print(reply.choices[0].message.content)List models
GET https://moenjo.com/api/v1/models returns the models your key can call, with each one's context window, capabilities and pay-as-you-go price.
curl https://moenjo.com/api/v1/models \
-H "Authorization: Bearer $MOENJO_API_KEY"Limits and billing
Subscription keys share your plan's daily message limit: each request counts as one message, and the count resets at midnight, Pakistan time. Pay-as-you-go keys are charged per token from your credit and stop working when the balance reaches zero.
Each plan also sets a context size. A request whose messages are longer than that is refused with context_length_exceeded, and the reply is capped so the request stays inside it.
Every response tells you where you stand:
| Header | Meaning |
|---|---|
X-Moenjo-Plan | The plan this request was billed to. |
X-Moenjo-Billing | subscription or payg. |
X-RateLimit-Limit-Messages-Day | How many messages your plan allows in a day. |
X-RateLimit-Remaining-Messages-Day | Messages left today, after this one. |
X-RateLimit-Reset | Seconds until the daily count resets. |
X-Moenjo-Context-Limit | The context size, in tokens, your plan allows on this model. |
Retry-After | On a 429, how many seconds to wait before trying again. |
See your usage by day, model and key on the Usage page.
Errors
Errors use standard HTTP status codes and a JSON body with a readable message and a stable code you can branch on.
{
"error": {
"message": "You've used your daily token limit on the Free plan. It frees up as older usage rolls off.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"param": null
}
}| Status | Code | What to do |
|---|---|---|
| 400 | invalid_request | Fix the request body. The message names the field. |
| 400 | context_length_exceeded | Send fewer or shorter messages. |
| 401 | invalid_api_key | Check the key, or create a new one. |
| 402 | insufficient_credits | Add pay-as-you-go credit, or switch the key to your subscription. |
| 403 | model_not_included | Your plan doesn't include this model. |
| 404 | model_not_found | Check the model id against the models list. |
| 429 | rate_limit_exceeded | Wait for the time in Retry-After, or move to a bigger plan. |
| 503 | model_unavailable | The model is busy or restarting. Retry with a short delay. |