# Makoda developer API

Use one local job API to send a prompt, model and reasoning effort to an installed coding CLI. No Makoda npm package is required. Your application’s backend calls Makoda; Makoda uses the desktop owner’s provider sign-in. Inference consumes that account’s allowance.

## Versions and compatibility

This reference describes the current **0.1.9 development source**. Public Mac and Ubuntu downloads remain **0.1.8**. Installing from the website does not yet install the Claude, attachment or improved timeout-message changes described here. Read the running gateway’s `/v1/openapi.json`, `/v1/capabilities` and `/v1/models` before enabling a feature.

| Feature | Public 0.1.8 | 0.1.9 development source |
| --- | --- | --- |
| Local Codex jobs, polling and idempotency | Available | Available |
| Namespaced `model`, e.g. `codex/<id>` | Required | Supported |
| Explicit `provider` with unprefixed model | Not supported | Supported |
| Claude adapter and bounded attachments | Not supported | Implemented; see verification limits below |
| Optional timeout error message | Code-only failures | Human-readable recovery guidance |
| Remote tunnels / hosted inference | Not implemented | Not implemented |

Makoda uses HTTP, JSON, bearer tokens and **OpenAPI 3.1**. Its `/v1/jobs` interface is a custom asynchronous job contract, not a drop-in OpenAI Chat Completions, OpenAI Responses or Anthropic Messages API. It does not implement their `messages`, tool-calling, sampling or token-streaming formats. Unknown request fields are rejected. The contract version is `0.1.0`; it is separate from the desktop application version.

## Connect and discover

1. Open Makoda, check the provider’s installation/sign-in and create access for your project.
2. Copy the setup instruction into your coding agent. Keep `MAKODA_BASE_URL` and `MAKODA_TOKEN` in private server-side configuration.
3. Read the discovery routes. None of these reads submits model inference.
4. Submit a job only when the application’s user requests it, then retain its ID and idempotency key.

The default base URL is `http://127.0.0.1:43127`; the owner can change the port in Settings. Use the endpoint shown by the app. Only `/healthz` is unauthenticated.

| Route | Purpose |
| --- | --- |
| `GET /healthz` | Gateway liveness |
| `GET /v1/providers` | Cached provider installation/auth readiness; no fresh CLI check or account quota disclosure |
| `GET /v1/models` | Exact model IDs, efforts, modalities and attachment types |
| `GET /v1/capabilities` | Installed gateway’s limits and execution mode |
| `GET /v1/openapi.json` | Installed gateway’s machine-readable contract |
| `POST /v1/jobs` | Submit one job |
| `GET /v1/jobs/{id}` | Read that integration’s job and result |
| `GET /v1/jobs/{id}/events` | SSE lifecycle events; not generated-text deltas |
| `POST /v1/jobs/{id}/cancel` | Request best-effort cancellation |
| `GET /v1/usage` | Retained job statistics scoped to the authenticated integration |

Operational requests use `Authorization: Bearer <MAKODA_TOKEN>`. Submission also requires `Content-Type: application/json` and exactly one `Idempotency-Key` header containing 1–128 printable non-space ASCII characters. A UUID is a convenient key. Never put the token in a URL, frontend bundle, logs or source control.

Provider readiness is not a guarantee of model entitlement, remaining quota or a successful model response. Discover IDs rather than deriving them from marketing names. Codex models come from its local model catalog. Claude currently exposes only `sonnet` and `opus`, each with `low`, `medium` and `high` effort. Those aliases follow the installed CLI’s mapping; they do not pin a model revision. **Fable 5.1 is not currently accepted by Makoda**, regardless of whether the CLI or account advertises it elsewhere.

## Submit a Codex job

Examples below use fictional job IDs, timestamps and results. They do not show a new live request. Use `gpt-6-sol` with `medium` only when the local discovery response lists that combination.

```http
POST /v1/jobs
Authorization: Bearer <MAKODA_TOKEN>
Content-Type: application/json
Idempotency-Key: <new-key-for-this-logical-request>
```

```json
{
  "provider": "codex",
  "model": "gpt-6-sol",
  "reasoning_effort": "medium",
  "input": [
    {
      "role": "user",
      "content": "Explain what an API gateway does in one sentence."
    }
  ],
  "response_format": { "type": "text" },
  "timeout_ms": 180000
}
```

For public 0.1.8, omit `provider` and use `"model": "codex/gpt-6-sol"` instead. The namespaced form also works on 0.1.9. Do not change between these representations when replaying the same idempotency key: the original request body determines its fingerprint.

An accepted submission returns **HTTP 202**, with a Job object:

```json
{
  "id": "example-codex-job",
  "integration_id": "example-project",
  "model": "codex/gpt-6-sol",
  "reasoning_effort": "medium",
  "status": "queued",
  "created_at": "2026-10-19T09:00:00Z",
  "updated_at": "2026-10-19T09:00:00Z",
  "result_expires_at": null,
  "idempotency_expires_at": "2026-10-26T09:00:00Z",
  "result_availability": "pending",
  "result": null,
  "error": null,
  "execution_mode": "trusted_local_agent",
  "adapter_version": "codex-exec/0.1.0",
  "metrics": { "latency_ms": null, "usage": null }
}
```

Persist `id`. Poll `GET /v1/jobs/example-codex-job` with the same bearer token until `status` is `succeeded`, `failed` or `cancelled`. An illustrative successful **HTTP 200** response:

```json
{
  "id": "example-codex-job",
  "integration_id": "example-project",
  "model": "codex/gpt-6-sol",
  "reasoning_effort": "medium",
  "status": "succeeded",
  "created_at": "2026-10-19T09:00:00Z",
  "updated_at": "2026-10-19T09:00:10Z",
  "result_expires_at": "2026-10-20T09:00:10Z",
  "idempotency_expires_at": "2026-10-26T09:00:00Z",
  "result_availability": "available",
  "result": {
    "provider": "codex",
    "text": "An API gateway receives client requests and routes them to the appropriate backend service.",
    "usage": null,
    "cli_version": "<reported CLI version>"
  },
  "error": null,
  "execution_mode": "trusted_local_agent",
  "adapter_version": "codex-exec/0.1.0",
  "metrics": { "latency_ms": 10000, "usage": null }
}
```

`result.usage` contains provider-reported counters when available. `null` or a missing counter means unknown, not zero. Counters can include provider/agent overhead and are not an estimate of subscription percentage or price. `metrics.latency_ms` includes queue time. The requested model is not proof of the provider’s resolved model revision.

## Submit a Claude job

Use the same endpoint and headers, with a separately chosen idempotency key:

```json
{
  "provider": "claude",
  "model": "opus",
  "reasoning_effort": "low",
  "input": [
    {
      "role": "user",
      "content": "Explain what an API gateway does in one sentence."
    }
  ],
  "response_format": { "type": "text" },
  "timeout_ms": 180000
}
```

An illustrative successful result excerpt, with job bookkeeping omitted:

```json
{
  "id": "example-claude-job",
  "model": "claude/opus",
  "reasoning_effort": "low",
  "status": "succeeded",
  "result": {
    "provider": "claude",
    "text": "An API gateway directs incoming requests to the services that handle them.",
    "usage": null,
    "cli_version": "<reported CLI version>"
  },
  "error": null
}
```

Sending `"model": "fable-5.1"` would currently return **HTTP 400** before inference. Makoda does not substitute Opus or Sonnet:

```json
{
  "error": {
    "code": "model_unavailable",
    "message": "model_unavailable",
    "retryable": false,
    "job_id": null,
    "details": {}
  }
}
```

## Defaults and request validation

| Field | Default / requirement |
| --- | --- |
| `provider` | No default. Optional only when `model` is already namespaced, e.g. `codex/gpt-6-sol`. Supported explicit values: `codex`, `claude`. |
| `model` | Required; must match discovery. |
| `reasoning_effort` | Required; must be supported by that model. |
| `input` | Required; exactly one object with `role: "user"` and nonempty string `content`. |
| `response_format` | Defaults to `{"type":"text"}`. Optional `json_schema` format described below. |
| `timeout_ms` | Defaults to `180000` (3 minutes). Integer range `1000`–`900000` (1 second–15 minutes). |
| `attachments` | Defaults to `[]`. |
| Token limit / temperature / tool calling | No public request fields for these controls. Unknown fields are rejected. |
| Provider or model fallback | None. |
| Conversation memory | None between jobs. |

For a chat UI, keep conversation history in your own application and construct the required context inside the single user message. Multiple `input` messages and separate `system`/`assistant` roles are not accepted. Keep the constructed text within the size limit; silently dropping history can change the answer.

The example client scripts may supply convenience defaults such as medium effort. Those defaults belong to those scripts, not to the HTTP API.

## Images and documents

On 0.1.9, append `attachments` to the same request. Encode actual bytes as standard base64, without a `data:` URL prefix:

```json
{
  "provider": "codex",
  "model": "gpt-6-sol",
  "reasoning_effort": "medium",
  "input": [
    {
      "role": "user",
      "content": "Describe this screenshot and suggest two UI improvements."
    }
  ],
  "attachments": [
    {
      "name": "screenshot.png",
      "media_type": "image/png",
      "data_base64": "<actual base64-encoded PNG bytes>"
    }
  ]
}
```

The placeholder is not executable input. Check the selected model’s `input_modalities` and `attachment_media_types` first. Your backend should validate the file and encode it; browsers should not hold Makoda credentials.

| Asset | Supported behavior |
| --- | --- |
| PNG / JPEG | Actual image input: private temporary image files for Codex; native base64 image blocks for Claude. |
| Plain text / Markdown / JSON / CSV | UTF-8 text is included with labels in prompt context. MIME types: `text/plain`, `text/markdown`, `application/json`, `text/csv`. |
| PDF / DOCX / SVG / audio / video | Unsupported. Convert deliberately in your application or show an unsupported-format error. |
| Remote URLs / arbitrary filesystem paths | Not fetched or opened as attachments. |
| Generated image/video/downloadable binary artifacts | No binary output/export API. Responses remain text or validated JSON. |

| Limit | Value |
| --- | --- |
| Files per request | 4 |
| Decoded size per file | 4 MiB |
| Combined decoded attachments | 5 MiB |
| Entire JSON request including base64 overhead | 8 MiB |
| Prompt plus text attachment contents and framing | 200 KiB |
| Image dimensions | At most 8,192 per dimension and 16,777,216 pixels |
| Attachment name | 1–128 UTF-8 bytes; no slashes/control characters; not a filesystem path |
| Text encoding | UTF-8 without NUL |

Invalid attachments are rejected before dispatch. Uploaded request content is retained locally while the job is nonterminal and removed with the request at completion; avoid treating this as an encrypted upload vault. Temporary Codex image files are cleaned on completion/failure/cancellation.

## Structured JSON output

The outer HTTP response is always JSON. To require the model’s output to follow a schema, supply `response_format`:

```json
{
  "type": "json_schema",
  "schema": {
    "type": "object",
    "properties": { "summary": { "type": "string" } },
    "required": ["summary"],
    "additionalProperties": false
  }
}
```

On success, read `result.json`; `result.text` also contains the model’s JSON text. Makoda validates the output. Invalid output fails with `output_invalid`; unsupported schemas fail with `schema_unsupported`. This is a bounded JSON Schema subset: at most 64 KiB, depth 16 and 2,048 nodes, with no external references. See the installed contract and validator before relying on advanced schema keywords.

## Heavy jobs and timeout handling

**Choose the execution budget before submitting expensive work.** For a heavier task, an application can set `"timeout_ms": 600000` (10 minutes), up to `900000` (15 minutes). Leave space in the client’s overall wait budget for queueing and provider startup as well. A larger timeout allows more time; it does not guarantee an answer, refund tokens or increase available provider allowance.

There are three different timeouts:

| Situation | What it means | Client action |
| --- | --- | --- |
| Submit HTTP request disconnects or times out | Admission may already have happened. | Replay the exact same body with the same idempotency key to recover the job. Do not increase `timeout_ms` on this replay. |
| Polling/client wait deadline expires | Your application stopped waiting; the job may still be running. | Keep the job ID and read it again. Do not create a replacement job automatically. |
| Job reaches terminal `failed` with `error.code: "timed_out"` | Makoda’s local execution deadline elapsed, or the provider reported a timeout. On a local deadline Makoda attempts to stop its CLI; upstream outcome/allowance consumption may be unknown. | Show the error, inspect the existing job, and let the user choose a deliberate new attempt with a larger budget and a new key when appropriate. A larger budget cannot prevent all provider-side timeouts. |

Timeout errors in new builds include an optional human-readable `error.message` explaining the need to increase `timeout_ms` for heavy jobs and the possibility that allowance was already consumed. Older jobs/builds may contain only the stable `error.code`; clients need a fallback explanation. Branch on the code, not the exact message text.

An illustrative terminal timeout response excerpt (polling still returns HTTP 200):

```json
{
  "id": "example-heavy-job",
  "status": "failed",
  "result": null,
  "error": {
    "code": "timed_out",
    "message": "This job timed out. Heavy work may need a higher timeout_ms before a deliberate new attempt (default 180000 ms; maximum 900000 ms). This attempt may have consumed provider allowance; upstream completion and output are unknown. Inspect the existing job first. Recover a lost response with the same idempotency key and unchanged body; a new attempt with a longer timeout needs a new key and may consume more allowance. A higher timeout_ms does not prevent provider-side timeouts or guarantee output."
  }
}
```

Changing the timeout changes the request. Reusing the old key with the changed body returns an idempotency conflict; reusing the old key and original body returns the original failed job and does not resume it. Creating a new key starts new work and may consume more allowance. There is no resume, refund or automatic retry guarantee.

The gateway admits at most 8 queued jobs and executes one job at a time across providers. Queue wait is separate from `timeout_ms`. When the worker next reaches a queued job, it rejects work older than about 120 seconds with `queue_expired`; a job waiting behind a long run can remain `queued` beyond that age before the check happens. Do not expect a terminal error exactly at 120 seconds. Increasing a job’s execution timeout does not extend this queue age limit. Cancelling a job is best effort and likewise does not prove upstream work stopped without cost.

## Job lifecycle, replay and retention

Jobs move from `queued` to `running`, then `succeeded`, `failed` or `cancelled`; `cancelling` is an intermediate state. Polling a failed job is still **HTTP 200** because reading the job succeeded. Always inspect `status`, then `result` or `error`. Successful submission is not successful generation.

Deduplication lasts 7 days. Replaying the same key and body returns the same job, including after a failure; it does not start another inference. Retain the key, request body and returned ID in your application. A deliberate new action should have a new key. Do not change provider, model representation, effort, timeout or attachments during recovery of an uncertain submission.

Completed output is retained for up to 24 hours. When `result_availability` is `expired`, Makoda does not regenerate it. Account allowance is independent of result retention. Lifecycle SSE is available at `/v1/jobs/{id}/events`, with event IDs for reconnection; generated text is final-only, not token-streamed.

## Failures, allowance and fallback

Makoda has **no automatic cross-provider or model fallback** and does not automatically resubmit failed jobs. The tray’s account-wide quota is informational, can be stale, and does not currently gate job admission. A provider may reject a request even after readiness checks pass.

If Claude reports a recognizable rate-limit failure, the job may look like this excerpt:

```json
{
  "id": "example-claude-job",
  "model": "claude/opus",
  "reasoning_effort": "low",
  "status": "failed",
  "result": null,
  "error": { "code": "provider_rate_limited" }
}
```

A rate limit is not proof of zero subscription allowance. Claude maps recognizable structured errors; Codex currently collapses many upstream failures, including possible quota failures, to `provider_failed`. Ambiguous failures can become `timed_out`. There is no reliable universal `quota_exhausted` code yet. Do not display a guessed remaining balance or reset time from these failures.

| Failure | Where it appears / response |
| --- | --- |
| Invalid token | Immediate HTTP 401, `unauthorized` |
| Unsupported model/effort | Immediate HTTP 400, `model_unavailable` / `unsupported_effort` |
| Conflicting provider/model prefix | Immediate HTTP 400, `provider_model_mismatch` |
| Invalid timeout | Immediate HTTP 400, `invalid_timeout` |
| Invalid/unsupported attachment | Immediate HTTP 400, corresponding attachment error |
| Oversized request/context/files | Immediate HTTP 413, corresponding size error |
| Queue full | Immediate HTTP 429, `queue_full`; separate from provider quota |
| Same key with changed request | Immediate HTTP 409, `idempotency_conflict` |
| Recognized Claude throttling | Accepted job later fails with `provider_rate_limited` |
| Generic provider failure | Accepted job later fails with `provider_failed` |
| Execution deadline | Accepted job later fails with `timed_out` |
| Expired queue wait | Accepted job later fails with `queue_expired` |
| Unknown interruption/cancellation outcome | `execution_outcome_unknown`; do not assume no allowance was consumed |

HTTP-level error envelopes include `code`, `message`, `retryable`, `job_id` and `details`. Terminal job errors have a stable `code` and may include a human-readable `message`. Neither format authorizes automatic inference retries. CLI internals and structured-output repair can involve additional upstream attempts; one Makoda job is not a promise of exactly one provider API request.

Detected API-key/cloud/custom provider configurations are rejected by the subscription adapters. However, provider-managed extra usage/credits may still be billed if enabled on the owner’s account. Makoda does not enforce a hard provider spending cap. Project statistics and token counters must not be presented as remaining subscription quota or a dollar bill.

## Local security and verification limits

The gateway binds IPv4 loopback and validates the exact Host header. Browser-origin requests are rejected. A browser app should call its own backend, and that backend should call Makoda using a private project token. A hosted backend cannot reach a user’s computer through its own `127.0.0.1`. Remote signup, pairing and tunnels remain future work.

These are coding-agent CLIs, not a proven tool-free model sandbox. Admit trusted local applications only. Project credentials are revocable and scoped to their own jobs; desktop account allowance is owner-only metadata, not exposed by `/v1/providers` or `/v1/usage`.

Codex text inference has passed authorized live checks. The local Claude adapter, images and structured output have offline fixture coverage, but the latest authorized Claude text checks failed/timed out; live Claude completion, live image understanding and broad model/effort coverage remain unverified. A successful quota read does not prove that generation will work.

Use the running gateway’s OpenAPI as the integration authority. The checked-in contract and this document describe development source and may be newer than an installed public binary.
