API and API keys
Moparmeg REST API v1 reference — authentication, scopes, endpoints, rate limits
The v1 API is twenty-six endpoints in twenty-one route files. Eighteen accept an API key and are opened by its scopes, three manage keys and accept nothing but the account owner's browser session, and five are public. This page describes what the server literally does.
On this page

Overview
Base URL: https://moparmeg-ksa.com. Every path below starts with /api/v1; requests and responses are JSON.
- API key (Bearer): eighteen endpoints, each opened by its scope — OpenAI-compatible chat, models and code generation; projects with their pages, versions and publishing; the design system and logo; domains; template stage builds; and
GET /api/v1/me, which accepts any valid key. - Browser session (cookie): three endpoints — list keys, create a key, revoke a key.
- Public, no authentication: five endpoints — the template list, one template, the section library, the downloads list and a tool download.
That is all of v1: twenty-six endpoints. The platform's other /api/* routes — webhooks included — serve the dashboard and the editor, authenticate with the browser session, and do not accept an API key.
Authentication
| Mechanism | How it is sent | Accepted by | On failure |
|---|---|---|---|
| API key | Header Authorization: Bearer mop_… | Every row whose auth column says "key" in the endpoint table | 401 {"error":"Invalid API key","code":"API_KEY_INVALID"}; 403 API_KEY_SCOPE_REQUIRED for a valid key without the scope |
| Browser session | The httpOnly access_token cookie set at sign-in | GET and POST /api/v1/api-keys, DELETE /api/v1/api-keys/{id} | 401 {"error":"غير مصرّح"} (Unauthorized) |
| None | — | GET /api/v1/templates, GET /api/v1/templates/{id}, GET /api/v1/sections, GET /api/v1/downloads, GET /api/v1/downloads/{id} | — |
API key
A key is mop_ followed by 32 characters from A-Z a-z 0-9 _ -. The server rejects a malformed header before any lookup, then looks the key up by its fingerprint. The response is the same (401 Invalid API key with the code API_KEY_INVALID) for every reason: unknown, revoked or expired key, deleted account, or malformed header — the response does not reveal which. Create keys on the API keys page.
A key always acts as its owner, with no admin powers even when the owner is an admin, and sees only the projects its owner owns: anyone else's project is 404 «المشروع غير موجود» (Project not found), even when it is published or the key's owner is a collaborator on it.
Browser session
The /api/v1/api-keys routes do not read the Authorization header at all: an API key sent to them gets 401. This is deliberate — a key that could mint keys would turn one leak into permanent, self-renewing access. These routes are built for the dashboard's keys page, i.e. requests from the same browser while signed in.
Scopes
Scopes are chosen when a key is minted (the scopes field) and cannot be edited afterwards. Leaving them out at minting means the default: every scope except publish and domains:write. A key without the required scope is refused with 403:
403
{ "error": "هذا المفتاح لا يحمل صلاحية «…».", "code": "API_KEY_SCOPE_REQUIRED", "scope": "publish" }
| Scope | Name on the keys page | Endpoints | In the default |
|---|---|---|---|
ai:chat | «محادثة الذكاء الاصطناعي والنماذج» (AI chat and models) | POST /api/v1/chat/completions, GET /api/v1/models, POST /api/v1/ai | Yes |
projects:read | «قراءة المشاريع وصفحاتها ونسخها» (Read projects, their pages and versions) | GET /api/v1/projects, GET /api/v1/projects/{id}, GET /api/v1/projects/{id}/pages, GET /api/v1/projects/{id}/versions | Yes |
projects:write | «إنشاء المشاريع ورفع ملفاتها وإضافة الصفحات وحفظ النسخ» (Create projects, upload their files, add pages and save versions) | POST /api/v1/projects, PUT /api/v1/projects/{id}, POST /api/v1/projects/{id}/pages, POST /api/v1/projects/{id}/versions | Yes |
publish | «نشر المشاريع وإلغاء نشرها» (Publish and unpublish projects) | POST /api/v1/projects/{id}/publish | No |
domains:read | «فحص توفّر النطاقات» (Check domain availability) | GET /api/v1/domains/availability | Yes |
domains:write | «طلب شراء النطاقات ونقلها» (Request domain purchases and transfers) | POST /api/v1/domains/orders | No |
design | «توليد نظام التصميم والشعار» (Generate the design system and logo) | POST /api/v1/design/system, POST /api/v1/design/logo | Yes |
templates | «بناء مراحل القوالب» (Build template stages) | POST /api/v1/templates/build | Yes |
Legacy keys (minted before scopes, so their scopes are empty) keep working on POST /api/v1/ai, GET and POST /api/v1/projects and GET /api/v1/me, and get 403 API_KEY_SCOPE_REQUIRED everywhere else, with a message asking you to mint a key with scopes.
Endpoint table
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/v1/me | Key (any valid key) | Account, plan, quota and the key's scopes |
POST | /api/v1/chat/completions | Key ai:chat | OpenAI-compatible chat, with streaming and tools |
GET | /api/v1/models | Key ai:chat | Model list in OpenAI format |
POST | /api/v1/ai | Key ai:chat (and legacy) | Generate code files from a prompt |
GET | /api/v1/projects | Key projects:read (and legacy) | The 50 most recently updated of your projects |
POST | /api/v1/projects | Key projects:write (and legacy) | Create a project with the dashboard's own checks |
GET | /api/v1/projects/{id} | Key projects:read | The full project, files included, plus filesDigest |
PUT | /api/v1/projects/{id} | Key projects:write | Save the project's files from the desktop (Mubarmij Agent app): files and the expectedFilesDigest of the last read; a stale digest is 409, a project over the plan's size is 413, a stack outside the plan is 403 STACK_NOT_IN_PLAN; a version is kept before the write |
GET | /api/v1/projects/{id}/pages | Key projects:read | Pages detected in the project's files |
POST | /api/v1/projects/{id}/pages | Key projects:write | Add a page |
GET | /api/v1/projects/{id}/versions | Key projects:read | The version list, or one version with its files |
POST | /api/v1/projects/{id}/versions | Key projects:write | Save a version of the current files |
POST | /api/v1/projects/{id}/publish | Key publish | Publish or unpublish the project |
POST | /api/v1/design/system | Key design | A design system saved into the project (SSE stream) |
POST | /api/v1/design/logo | Key design | An SVG logo |
GET | /api/v1/domains/availability | Key domains:read | Whether a domain can be registered |
POST | /api/v1/domains/orders | Key domains:write | Request a domain purchase or transfer |
POST | /api/v1/templates/build | Key templates | Build one stage of a template |
GET | /api/v1/templates | Public | Industry templates and their categories |
GET | /api/v1/templates/{id} | Public | One template with its stages |
GET | /api/v1/sections | Public | The section library and its layouts |
GET | /api/v1/downloads | Public | Tools actually available to download |
GET | /api/v1/downloads/{id} | Public | Download a tool |
GET | /api/v1/api-keys | Browser session | Your active keys, masked |
POST | /api/v1/api-keys | Browser session | Mint a new key (returned in full once) |
DELETE | /api/v1/api-keys/{id} | Browser session | Revoke a key |
GET /api/v1/me
Accepts any valid key, legacy or scoped, and answers even on the «المجانية» (Free) plan, so your program can show the plan and why AI is refused. It does not count against the plan's hourly cap and has its own cap of 120 calls per hour (429 «كثرة الطلبات — حاول لاحقًا», Too many requests — try later). It does not return the email address.
200
{
"data": {
"id": "…",
"name": "…",
"plan": { "id": "…", "tier": "…", "label": "…" },
"ai": {
"allowed": true,
"reason": null,
"quota": { "used": 0, "limit": 0, "remaining": 0, "resetAt": …, "window": "24h-rolling" }
},
"api": { "callsPerHour": 0 },
"key": { "id": "…", "scopes": ["ai:chat", "projects:read"], "legacy": false }
}
}
When the plan does not include AI, allowed is false, reason carries the reason text, and quota is null.
GET /api/v1/projects
Returns up to 50 of the projects you own, most recently updated first. There is no pagination, and total is the number of items returned (at most 50), not your overall project count. Project files are not returned here; request a single project with GET /api/v1/projects/{id}.
200
{
"data": [
{
"id": "…",
"title": "…",
"slug": "…",
"status": "…",
"views": 0,
"forks": 0,
"createdAt": "2026-09-01T10:00:00.000Z",
"updatedAt": "2026-09-10T12:00:00.000Z"
}
],
"total": 1
}
Status codes: 200, 401, 403 (API_REQUIRES_PAID_PLAN or API_KEY_SCOPE_REQUIRED), 429 (hourly cap).
GET /api/v1/projects/{id}
Returns the project record as stored, files included, in {"data": {…}}. Only for projects the key's owner owns: a missing project and anyone else's project — even a published one, or one where you are a collaborator — both return 404 {"error":"المشروع غير موجود"} (Project not found).
POST /api/v1/projects
Creates a project in your name with the same function the dashboard uses, so it passes the same checks: field validation, the abuse check, the plan contract and the project-count limit.
| Field | Type | Notes |
|---|---|---|
title | string | Required, up to 100 characters |
description | string | Optional, up to 500 characters |
framework | "html" | "nextjs" | "react" | Optional (default html). nextjs and react create a ready project scaffold, and are allowed only on a plan that includes Next.js |
template | "blank" | "landing" | "portfolio" | "dashboard" | "form" | Optional for an HTML project: the starter template files (default blank) |
industryId | string | Optional; an industry template id from GET /api/v1/templates. With HTML it generates the full template page; with nextjs or react it is passed to the scaffold |
The slug is built from the title: letters in any script (Arabic included) and digits are kept, everything else becomes a hyphen, it is cut to 60 characters, and a random suffix is appended.
POST /api/v1/projects
{ "title": "My Store", "description": "Clothing store", "template": "landing" }
201
{ "data": { "id": "…", "title": "My Store", "slug": "my-store-…", "files": [ … ], … } }
The response carries the full project record as stored. Errors after the key, scope and hourly cap:
- 400 with the first validation message, such as «عنوان المشروع طويل جداً» (The project title is too long) — a non-JSON body or a missing title included. 400 «قالب المجال المحدد غير موجود» (The selected industry template does not exist) for an unknown
industryId. - 403 with the reason when the abuse check refuses the title or description.
- 403
STACK_NOT_IN_PLAN:nextjsorreacton a plan without Next.js, including «المجانية» (Free), which includes static HTML only. - 403
PROJECT_QUOTAwithlimitandused: you reached your plan's project-count limit.
Project pages
GET /api/v1/projects/{id}/pages (projects:read) returns {"pages": […]}: the pages detected in the project's files.
POST /api/v1/projects/{id}/pages (projects:write) with the body {"path": "…", "title": "…"} generates the files of a new page and adds them to the project's files: a Next.js page when the project has a next.config file, an HTML page otherwise. It returns {"added": [file names], "totalFiles": …}. A missing field or an empty title: 400 {"error":"path و title مطلوبان"} (path and title are required). A project you do not own: 404.
Project versions
GET /api/v1/projects/{id}/versions (projects:read) returns the array of versions, newest first, each with id, label, createdAt, fileCount and totalSize but no files. With ?versionId=… it returns one version with its files (id, label, createdAt, files), or 404 «النسخة غير موجودة» (Version not found).
POST /api/v1/projects/{id}/versions (projects:write) with an optional body {"label": "…"} saves a snapshot of the project's current files and returns 201 with the version record. The cap is 20 saves per minute (429 «كثرة الطلبات», Too many requests). A project keeps at most 50 versions: at the limit the oldest version is deleted to make room.
Restoring an earlier version with an API key: Not available yet — restoring needs an open editor, so it lives on the session route only.
POST /api/v1/projects/{id}/publish
Needs the publish scope, which is not in the default. It is the same function the editor's publish button uses, with the same checks. See Publishing.
| Field | Type | Notes |
|---|---|---|
expectedFiles | array of {name, content, language?} | Required to publish: it must equal the project's current files exactly, so only what you reviewed goes live. Read it from GET /api/v1/projects/{id} |
unpublish | boolean | true unpublishes: status DRAFT and the project is no longer public |
Success returns the project record plus address: the address the site got, or null. It also fires the project.published event to the project's webhooks.
- 400
PUBLISH_REVISION_REQUIRED:expectedFilesis missing or malformed. - 409
PUBLISH_REVISION_CONFLICT: the files changed from what you sent, before or during publishing. 409PUBLISH_ADDRESS_TAKEN: the address was just taken; retry. - 403: refused by the content scan, or
PUBLISH_NOT_STATICwhen the files include something your plan does not publish, with arefusalslist. - 404 for a project you do not own, and 429 after 10 publish actions in a minute.
POST /api/v1/ai
Sends your prompt to the AI model and asks for code files as JSON. The request may run for up to 120 seconds.
| Field | Type | Notes |
|---|---|---|
prompt | string | Required and not blank; missing or not a string gives 400 {"error":"prompt required"} |
framework | "html" | "nextjs" | "react" | Optional. nextjs appends "(Next.js + Tailwind + Prisma)" to the prompt, react appends "(React + Vite + Tailwind)", anything else appends nothing |
model | string | Optional; passed to the model layer |
200
{
"data": {
"files": [ … ],
"raw": "…",
"model": "…",
"tokens": …
}
}
files: the post-processed files when the model's reply is JSON containingfiles; otherwisenull.raw: the model's reply text as is — fall back to it whenfilesis null.modelandtokens: the answering model and its usage, as reported by the model layer.
The order of checks matters for your quota: key (401) → scope (403) → plan without AI (402 AI_PAID_ONLY) → hourly cap (403 or 429) → request body: not JSON (400 {"error":"جسم الطلب ليس JSON","code":"invalid_json"}, Request body is not JSON) or no prompt (400) → daily AI quota (429 RATE_LIMITED) → generation (200 or 500). A malformed request or one missing prompt is therefore refused before the daily quota is spent. A generation failure returns 500 {"error":"…"} with the error message, and the quota is given back. For chat and streaming use POST /api/v1/chat/completions.
POST /api/v1/chat/completions
An OpenAI Chat Completions-compatible endpoint, with the ai:chat scope; a legacy key does not open it. A request may run for up to 840 seconds.
| Field | Notes |
|---|---|
messages | Required. Roles system, developer, user, assistant and tool. Content is a string or text and image_url parts; images as data:…;base64,… only — an http link is refused with 400 image_url_must_be_data_url |
model | Optional; omitted or "auto" means the administrator's preference. Names come from GET /api/v1/models |
stream | true returns an SSE stream: chat.completion.chunk parts, then data: [DONE] |
max_tokens or max_completion_tokens | An integer from 1 to 32768 |
tools, tool_choice, parallel_tool_calls | Tools are supported: tool_choice is one of none, auto, required or {"type":"function","function":{"name":"…"}}. tool messages carry tool_call_id, and assistant messages may carry tool_calls |
A non-streamed chat.completion response carries choices[0].message, with tool_calls and finish_reason: "tool_calls" when the model calls a tool; streaming sends tool_calls deltas. usage values are an estimate, not a provider count, and the field estimated: true says so.
The order of checks: key and scope (401/403) → plan without AI (402 AI_PAID_ONLY) → hourly cap (403/429) → request shape (400) → daily quota (429 RATE_LIMITED) → the model. Shape and provider errors use the OpenAI format: {"error": {"message": "…", "type": "…", "code": "…"}}.
- 400
invalid_jsonfor a body that is not JSON,legacy_functions_not_supportedforfunctionsorfunction_call(the legacy format),n_not_supportedwhennis not 1, andinvalid_max_tokens. - 400
tools_not_supported_for_model: the chosen model does not support tools; the quota is given back and the tools are never dropped silently. - 502
upstream_errorand other provider codes: the AI provider failed. If it failed before the model started answering, the quota is given back.
GET /api/v1/models
With the ai:chat scope; a plan without AI is refused with 402 before the hourly cap. Returns an OpenAI list from the catalog of models configured on the server:
200
{ "object": "list", "data": [ { "id": "…", "object": "model", "created": 1757940000, "owned_by": "…" } ] }
Design system and logo
Both take the design scope, a plan without AI is refused with 402 before the hourly cap, and both count against the daily AI quota. They are the same functions the editor uses.
POST /api/v1/design/system takes {"projectId": "…", "brandName": "…", "industry"?, "mood"?, "primaryColor"?}, where mood is one of professional, playful, luxury, minimal, bold and warm (default professional). You must own the project (otherwise 404). The response is an SSE stream of start, then thinking and token, then files (the files after they are saved into the project, committed: true), then data: [DONE] — or an error event. If the project's files change during generation nothing is saved and an error event arrives.
POST /api/v1/design/logo takes {"brandName": "…", "style"?, "projectId"?}; without brandName: 400 «اسم العلامة مطلوب» (Brand name is required). It returns JSON with files and committed: with the projectId of a project you own, logo.svg is saved into its files (committed: true), or 409 MUBARMIJ_PROJECT_REVISION_CONFLICT if the files changed. An invalid logo from the model: 502.
Domains
GET /api/v1/domains/availability?domain=example.sa (domains:read) returns {"data": {"domain": "…", "status": "available" | "taken" | "unknown", "reason": "…"}}. unknown is an honest answer when the check could not be made, never "available". Without domain: 400 «النطاق مطلوب» (Domain is required). An extra cap of 20 lookups per minute applies.
POST /api/v1/domains/orders (domains:write, not in the default) submits a domain purchase or transfer order — a task priced after team review, not an immediate purchase. The body: domain, kind (PURCHASE by default, or TRANSFER_IN), years from 1 to 10 (default 1), contact with required name and email, authCode for transfers, and an optional projectId of a project you own. It returns 201 {"data": {…}}. A plan without custom domains: 402 PLAN_CAPABILITY_REQUIRED. A field error: 400 with the reason. An extra cap of 5 orders per hour applies.
GET /api/v1/templates
Public: no authentication and no key cap. Returns the industry templates the editor builds from; their count is in total.
200
{
"data": [
{
"id": "…",
"name": "…",
"nameEn": "…",
"icon": "…",
"category": "…",
"categoryLabel": { "ar": "…", "en": "…" },
"colorScheme": …,
"stages": 0,
"description": "…",
"descriptionEn": "…"
}
],
"total": 0,
"categories": [ { "id": "…", "ar": "…", "en": "…" } ]
}
stages is the number of build stages in the template, not the stages themselves. For one template with all its stages: GET /api/v1/templates/{id} (public) returns {"data": {…}} with categoryLabel, or 404 «القالب غير موجود» (Template not found). See Templates.
POST /api/v1/templates/build
With the templates scope. Builds one stage of a template and returns its files without saving them into any project.
| Field | Notes |
|---|---|
industryId | Required; a template id from GET /api/v1/templates |
stage | Required, 1 to 5; otherwise 400 «stage يجب أن يكون 1-5» (stage must be 1-5) |
companyName, companyDesc, logoUrl, colorOverride | Optional; colorOverride is {"primary"?, "secondary"?} |
projectId | Optional, a project you own (otherwise 404): the products section renders that project's store catalog |
200
{
"files": [
{ "name": "index.html", "content": "…", "language": "html" },
{ "name": "style.css", "content": "…", "language": "css" },
{ "name": "script.js", "content": "…", "language": "javascript" }
],
"progress": …,
"sectionsAdded": …,
"nextStageHint": …,
"storefront": null
}
GET /api/v1/sections
Public. The section library the editor inserts: {"schema": …, "data": [{"id", "label", "hint", "markup"}], "packs": …, "total": …}, where markup is what the editor itself inserts for the section.
Downloads
GET /api/v1/downloads (public) returns {"data": […], "total": …} with only the tools actually available, each with id, platform, kind, fileName, version, sha256, size, source and href. A tool that is not available does not appear in the list at all.
GET /api/v1/downloads/{id} (public) streams the tool or redirects to it with 302, with the X-Checksum-SHA256 header so you can verify the file after download. An unknown id and an unavailable tool are the same: 404 with the code DOWNLOAD_UNAVAILABLE. See Desktop app: download and install.
Key-management endpoints (browser session)
GET /api/v1/api-keys
Your active (non-revoked) keys, newest first. The fingerprint never leaves the server. legacy is true for a legacy key, whose scopes are then an empty array.
200
{
"keys": [
{
"id": "…",
"name": "desktop app",
"maskedKey": "mop_••••••••abcd",
"createdAt": "…",
"lastUsedAt": null,
"expiresAt": null,
"scopes": ["ai:chat", "projects:read", "projects:write"],
"legacy": false
}
]
}
POST /api/v1/api-keys
| Field | Type | Notes |
|---|---|---|
name | string | Required, up to 64 characters after trimming |
expiresInDays | integer | Optional, 1 to 365. Omitted = a key that never expires |
scopes | array of strings | Optional. Omitted = the default (every scope except publish and domains:write). An empty array or an unknown value is refused with 400 |
201
{
"key": "mop_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"id": "…",
"name": "desktop app",
"maskedKey": "mop_••••••••XXXX",
"createdAt": "…",
"expiresAt": null,
"scopes": ["ai:chat", "projects:read"],
"prefix": "mop_",
"warning": "انسخه الآن — لن يُعرض مرّةً أخرى."
}
key is the raw key and never comes back in any later read; warning means "Copy it now — it will not be shown again." Errors: 400 «جسم الطلب ليس JSON» (Request body is not JSON), 400 «الاسم مطلوب» (Name is required), 400 «الاسم يتجاوز 64 محرفاً» (Name exceeds 64 characters), 400 when expiresInDays is outside 1–365 or not an integer, 400 when scopes is empty or holds an unknown scope, 409 at 20 active keys, 401 without a session. This route does not check the plan: any account can create a key.
DELETE /api/v1/api-keys/{id}
Revokes the key (the record is kept). 200 {"ok":true}. A missing key, another account's key and an already-revoked key all return 404 {"error":"المفتاح غير موجود"} (Key not found) — the response does not distinguish them.
Webhooks (outside v1, browser session)
Webhooks are registered per project from its settings page, and their /api/webhooks routes accept only the project owner's browser session, not an API key. They need a plan that includes hosting; otherwise 402 PLAN_CAPABILITY_REQUIRED.
GET /api/webhooks?projectId=…: the registrations and the event list.POST /api/webhookswith{"projectId", "url", "events": […]}returns 201 with the signing secret, shown this one time only. At most 10 registrations per project (409).- Events:
project.published(also fired by publishing with an API key),order.createdandform.submitted. - Each delivery is a POST carrying the header
x-mubarmij-signature: t=…,v1=…, wherev1is an HMAC-SHA256 with the secret over<t>.<body>. Only a public https address is accepted (400WEBHOOK_URL_REFUSED). - If the platform's secret-encryption key is not configured, creation is refused with 503
WEBHOOK_SECRETS_UNCONFIGURED, and no secret is stored unencrypted.
Rate limits
Two main counters, both per account rather than per key, with numbers taken from your plan:
- Hourly API cap: every key-authenticated call counts, except
GET /api/v1/me. The hourly cap equals the plan's daily AI quota. - Daily AI quota: for
POST /api/v1/ai,POST /api/v1/chat/completions,POST /api/v1/design/systemandPOST /api/v1/design/logo; a 24-hour window starting at the first request, shared with AI requests from the editor in the browser.
On top of them, some endpoints have their own caps: GET /api/v1/me 120 per hour, domain availability 20 per minute, domain orders 5 per hour, version saves 20 per minute, publishing 10 per minute. These endpoint caps return 429 with a text message and no rate-limit headers.
| Plan | API calls per hour | AI requests per day |
|---|---|---|
| «المجانية» (Free) | 0 (key calls refused with 403 or 402, except /api/v1/me) | 0 |
| «الأساسية» (Basic) | 50 | 50 |
| «المميزة» (Premium) | 200 | 200 |
| «الاحترافية» (Professional) | 1000 | 1000 |
Plan details in Billing and plans.
Rate-limit headers
The headers are sent only with the hourly cap's 429 response, not with successful responses:
| Header | Meaning |
|---|---|
Retry-After | Seconds until the window renews (at least 1) |
X-RateLimit-Limit | Your plan's hourly cap |
X-RateLimit-Remaining | Calls left in the window |
X-RateLimit-Reset | When the window renews, in Unix seconds |
429
Retry-After: 1200
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1757940000
{ "error": "تجاوزتَ سقف باقتك (50 نداءً في الساعة) — أعد المحاولة بعد 1200 ثانية" }
The error text reads "You exceeded your plan's cap (N calls per hour) — retry after S seconds". Running out of the daily quota instead returns 429 without these headers, with a body carrying code and upgradeUrl:
429
{ "error": "وصلت الحد اليومي للطلبات. ترقّى لباقةٍ أعلى لطلبات أكثر.", "code": "RATE_LIMITED", "upgradeUrl": "/billing", "remaining": 0, "limit": 50 }
Errors
| Status | Body or code | When |
|---|---|---|
| 400 | prompt required / field validation messages / PUBLISH_REVISION_REQUIRED / OpenAI codes such as n_not_supported and tools_not_supported_for_model | A required field is missing or invalid |
| 401 | Invalid API key with the code API_KEY_INVALID | Key endpoints: key missing, malformed, unknown, revoked or expired |
| 401 | غير مصرّح (Unauthorized) | Key-management and webhook endpoints: no valid browser session |
| 402 | AI_PAID_ONLY | A plan without AI: on code generation, chat, models and design, before the hourly cap |
| 402 | PLAN_CAPABILITY_REQUIRED | A domain order on a plan without custom domains, or webhooks on a plan without hosting |
| 403 | API_REQUIRES_PAID_PLAN | A key call from an account on the «المجانية» (Free) plan |
| 403 | API_KEY_SCOPE_REQUIRED with scope | The key lacks the endpoint's scope, or a legacy key outside its three endpoints |
| 403 | PUBLISH_NOT_STATIC or the content-scan message | Publishing refused |
| 403 | STACK_NOT_IN_PLAN / PROJECT_QUOTA / the abuse-check message | Project creation: a framework your plan does not include, the project-count limit reached, or refused content |
| 404 | المشروع غير موجود (Project not found) / المفتاح غير موجود (Key not found) / DOWNLOAD_UNAVAILABLE | A project that is missing or not yours, a key that is missing, not yours or revoked, a tool that is not available |
| 409 | Maximum-keys message / PUBLISH_REVISION_CONFLICT / PUBLISH_ADDRESS_TAKEN / MUBARMIJ_PROJECT_REVISION_CONFLICT | 20 active keys, the project's files changed, or the address was taken |
| 429 | Hourly-cap message (no code), RATE_LIMITED, or an endpoint-cap message | Hourly cap, daily quota or an endpoint's own cap |
| 500 | Error message | Generation failed on POST /api/v1/ai (the quota is given back) |
| 502 | upstream_error and provider codes | The AI provider failed during chat, or the model returned an invalid logo |
curl example
Keep your key in an environment variable rather than in saved commands:
export MOPARMEG_API_KEY=mop_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
curl https://moparmeg-ksa.com/api/v1/me -H "Authorization: Bearer $MOPARMEG_API_KEY"
curl https://moparmeg-ksa.com/api/v1/projects -H "Authorization: Bearer $MOPARMEG_API_KEY"
curl -X POST https://moparmeg-ksa.com/api/v1/chat/completions -H "Authorization: Bearer $MOPARMEG_API_KEY" -H "Content-Type: application/json" -d '{"messages":[{"role":"user","content":"Suggest a headline for a specialty coffee shop landing page"}]}'
curl -i https://moparmeg-ksa.com/api/v1/templates
The -i flag shows headers, including Retry-After on a 429. To start from scratch see Quickstart; to understand what the AI builds see Building with AI.
FAQ
Does my key work on routes like /api/ai/generate or /api/terminal?
No. Those are dashboard and editor routes and authenticate with the browser session. The key works only on the v1 endpoints above, according to its scopes: reading a project with its files (projects:read) and publishing it (publish) included. Deleting a project or restoring an earlier version with a key: Not available yet.
Is the project list paginated?
Not available yet. The list returns the 50 most recently updated of your projects; a project beyond them can be read by its id with GET /api/v1/projects/{id} if you know the id.
Should I use POST /api/v1/ai or POST /api/v1/chat/completions?
POST /api/v1/ai returns code files as JSON in one go and keeps working with a legacy key. POST /api/v1/chat/completions is compatible with OpenAI clients, supports streaming, tools and images, and needs a key with the ai:chat scope.
Why are there no X-RateLimit-* headers on successful responses?
The server sends them only with the hourly cap's 429. Track usage in your program, or read Retry-After when refused.
Can a key create another key?
No. Creating, listing and revoking keys takes the browser session only — see API keys.