# LayerCloud AI - API Documentation

> LayerCloud AI is an AI gateway: one API key for GPT, Claude, Gemini, DeepSeek and 80+ models, an OpenAI-compatible endpoint that works with Claude Code, Cursor, opencode and any SDK - at about 25% of official prices.

## Base URL

| | |
| --- | --- |
| OpenAI-compatible endpoint | https://lcapi.ir/v1 |
| Documentation | https://lcapi.ir/docs |

## Authentication

All requests authenticate with an API key issued from the console (Tokens page).
Pass it as a Bearer token. **Never put a real key into code you share** - snippets below use the placeholder + LAYERCLOUD_API_KEY environment variable.

```bash
export LAYERCLOUD_API_KEY="lc_..."   # from the Tokens page
```

### First request

```bash
curl https://lcapi.ir/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LAYERCLOUD_API_KEY" \
  -d '{
    "model": "openai/gpt-4o",
    "messages": [{"role": "user", "content": "Say hello in one line."}]
  }'
```

## Endpoints

| Endpoint | Purpose |
| --- | --- |
| POST https://lcapi.ir/v1/chat/completions | OpenAI chat completions (streaming via stream: true) |
| POST https://lcapi.ir/v1/systemone | Laya typed-decision endpoint (the native Laya protocol) |
| POST https://lcapi.ir/v1/responses | OpenAI Responses API |
| GET https://lcapi.ir/v1/models | List the models your key can access |
| POST https://lcapi.ir/v1/embeddings | Embeddings |

The endpoint is drop-in compatible with the OpenAI SDKs: point the base URL
at https://lcapi.ir/v1 and use the same key.

### Laya decision service

`laya` is a model id that reaches a LOCAL DECISION ROUTER served on this host,
not a chat model: typed questions in, typed answers out. It answers in two wire
formats.

```bash
curl https://lcapi.ir/v1/systemone \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LAYERCLOUD_API_KEY" \
  -d '{
    "state": "I was charged twice this month",
    "questions": {
      "department": {
        "type": "choice",
        "instructions": "Which department should handle this?",
        "criteria": ["billing", "technical"]
      }
    }
  }'
```

The same decision can be sent through the OpenAI-compatible endpoint. The
message content CARRIES the decision request as JSON rather than a prompt:

```bash
curl https://lcapi.ir/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LAYERCLOUD_API_KEY" \
  -d '{
    "model": "laya",
    "messages": [{"role": "user", "content": "{\"state\": \"I was charged twice\", \"questions\": {\"department\": {\"type\": \"choice\", \"instructions\": \"Which department should handle this?\", \"criteria\": [\"billing\", \"technical\"]}}}"}]
  }'
```

Both replies carry the decision as JSON under `answers`, plus a `caveat` and
`confidence_calibrated`. Prose is refused with `400` rather than answered: a
classifier asked a question nobody typed still returns a confident answer. When
the operator has not enabled the service, requests return `404` with error code
`not_enabled`. Access follows your account's model permissions like any other
model.

Its confidence values are NOT calibrated. Treat them as relative scores.

**What the service understands well is ENGLISH.** MEASURED on this gateway, the same question in two languages:

```
English  "I was charged twice last month"     -> billing    confidence 0.82
Persian  "ماه گذشته دو بار از حساب من پول کم شد" -> technical  confidence 0.00
                                               billing 0.4996  technical 0.5004
```

**Persian came back 0.4996 against 0.5004 - a coin flip, at zero confidence.** The model detects the script and routes to its multilingual checkpoint, so it accepts the request, but it does not reliably understand the language.

**One measured workaround, because it costs nothing and helps:** for non-English states, **send the `criteria` LABELS in English** even when the state is in another language. With the same Persian sentence and English labels the answer moved from a coin flip to **0.6519 / 0.3481 and picked the right one** - so for non-English input the model leans on the labels more than on the state. The state itself may be in any language.

**If your decisions are mostly non-English, treat this as a known limitation rather than a setting to tune.** A fine-tuned checkpoint is what would fix it; telling you that here is cheaper than your discovering it in production.



### SDKs

```bash
# OpenAI Python
pip install openai
```

```python
from openai import OpenAI
client = OpenAI(
    base_url="https://lcapi.ir/v1",
    api_key=os.environ["LAYERCLOUD_API_KEY"],
)
client.chat.completions.create(
    model="openai/gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
)
```

```bash
# OpenAI JavaScript / TypeScript
npm install openai
```

```js
import OpenAI from "openai";
const client = new OpenAI({
    baseURL: "https://lcapi.ir/v1",
    apiKey: process.env.LAYERCLOUD_API_KEY,
});
await client.chat.completions.create({
    model: "openai/gpt-4o",
    messages: [{ role: "user", content: "Hello" }],
});
```

## Streaming

Add "stream": true and the response is server-sent events, one data: line per chunk:

```bash
bash
curl https://lcapi.ir/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LAYERCLOUD_API_KEY" \
  -d '{"model": "openai/gpt-4o", "stream": true, "messages": [{"role": "user", "content": "Hello"}]}'
```

Each line is in the OpenAI chunk format, and the stream ends with a final data: [DONE]. The official SDKs
handle this for you: pass stream=True in Python, or stream: true in JavaScript.

## Model ids

**Use the exact id from GET /v1/models.** It is the list your key is actually entitled to, so it accounts
for your own plan - a model that exists on the gateway but not in your list will answer 404 for you.

A model id may also be used **without** its namespace, and the separators hyphen, underscore and dot are
treated as interchangeable, so qwen3.8-max, qwen_3_8_max and alibaba/qwen3.8-max reach the same model
where that alias exists. Where they do not, you get a clear 404 rather than a different model silently.

## Errors

Errors use the OpenAI envelope with message, type, code and param fields. **Match on the code** - it is the
stable field; the message may be reworded. The gateway answers with its own wording rather than relaying an
upstream body, so a message never contains another provider's internal field names.

| code | HTTP | Meaning | What to do |
| --- | --- | --- | --- |
| invalid_api_key | 401 | No key, or a key that is disabled or expired | Check the Authorization header |
| model_not_found | 404 | That id is not in your catalogue | List models; do not retry |
| model_not_available | 404 | The id exists but no upstream can serve it now | Try another model |
| all_accounts_cooling_down | 429 | Every account for that model is rate-limited | Honour retry_after |
| rate_limited | 429 | An upstream rate limit | Back off and retry |
| out_of_credits | 402 | Your balance does not cover the request | Top up |
| upstream_overloaded | 503 | The upstream is busy | Retry with backoff |
| upstream_rejected | 400 | Every target refused the request as sent | Check the body; retrying will not help |
| upstream_auth_error | 401 | A credentials problem on our side, not yours | Report it; retrying will not help |
| invalid_parameter | 400 | A malformed field | Fix the body |
| context_length_exceeded | 400 | The prompt is too long for the model | Shorten it, or use a larger-context model |
| invalid_decision_request | 400 | Laya was sent prose instead of a decision request | See the Laya section |
| all_upstreams_failed | 502 | Every upstream target for this request failed | Retry with backoff |
| no_available_targets | 503 | No target matches the request's routing policy | Retry; if it persists, report it |
| content_filtered | 400, 413 or 422 | An upstream refused the CONTENT of the request under its own policy. The model is fine; the prompt was declined | Change the prompt. Retrying will not help - every provider applies a comparable policy |
| image_not_supported_by_upstreams | 404 | The request carried an image and no upstream accepts images for that model. The model itself still serves TEXT requests | Send text, or choose a model that accepts images |
| retry_on_exhausted | (the failed attempt's status) | The failure walk stopped because the remaining failure class is not retryable | Match on the accompanying message; retrying the same request will not help |
| missing_messages | 400 | No messages array, or it was empty | Send at least one message |
| invalid_model | 400 | The model field is absent or blank | Set the model field |
| invalid_lineage | 400 | previous_response_id does not belong to this conversation | Start a new response chain |
| tools_not_supported | 400 | The model does not accept the tools you sent | Drop tools, or use a model that supports them |
| unsupported_parameter | 400 | A field is not valid for this model or endpoint | Remove it; see the parameter reference |
| images_not_supported | 400 | An image was sent to a model that does not accept image input | Send text, or use a vision model |
| invalid_upstream_response | 502 | An upstream returned a body we could not interpret | Retry once; if it persists, report it |
| upstream_outcome_unknown | 502 | The upstream connection failed in a way that leaves the outcome unknown | Retry; do not assume the request was not billed |
| continuity_store_unavailable | 503 | The response-continuity store is unreachable, so a chained request cannot be resolved | Retry with backoff |
| request_budget_exhausted | 504 | The failover walk spent its whole time budget without a successful attempt | Retry with backoff |
| generation_failed | 502 | An image or video generation failed on the backend | Retry once; if it persists, report it |
| response_not_found | 404 | That response id is not stored (expired, or never existed) | Start a new response |
| response_id_failed | 500 | A response id could not be generated | Retry; report it if it persists |
| upstream_configuration_error | 500 | A credentials or endpoint problem on our side, for this account | Report it; retrying will not help |
| upstream_request_failed | 500 | The request to the upstream failed before a response was read | Retry with backoff |
| upstream_not_working | 502 | The selected upstream is not currently serving | Retry; the gateway will pick another |
| account_suspended | 403 | Your account is suspended | Contact support; retrying will not help |
| model_not_allowed | 403 | Your key is not permitted to use that model | Use an allowed model |
| insufficient_quota | 429 | Your balance or plan quota is exhausted | Top up, then retry |
| invalid_media | 413 | Inline media exceeds the 2MB limit | Send a URL instead of inlining, or shrink the file |
| media_too_large | 413 | Inline media exceeds the 2MB limit | Send a URL instead of inlining, or shrink the file |
| unsupported_media_type | 415 | The media type is not one this endpoint accepts | Check the supported types |
| unsupported_input_modality | 400 | The model does not accept this input type (text/image/audio) | Use a model that accepts it |
| response_storage_limit | 413 | Response lineage exceeds the storage or target limit | Start a new response chain |
| invalid_previous_response | 400 | previous_response_id is invalid or unavailable | Start a new response chain; do not reuse an expired id |
| invalid_identity | 400 | A continuity identity field is malformed | Check the request's identity fields |
| invalid_request | 400 | The request could not be parsed into a usable form | Check the body against the examples above |
| internal_error | 500 | Billing authorisation failed on our side | Retry; report it if it persists |
| invalid_stream_event | 502 | An upstream emitted malformed SSE JSON mid-stream | Retry; the stream cannot be resumed from a bad frame |
| stream_failed | 502 | The stream ended before the response completed | Retry; do not treat the partial output as final |
| laya_bad_base_url | 502 | The Laya decision service is configured with an unusable base URL | Report it; this is a gateway misconfiguration |
| laya_unreachable | 502 | The decision service did not answer | Retry with backoff |
| laya_failed | 502 | The decision service answered with something unusable | Report it if it persists |
| laya_rejected | 502 | The decision service rejected the request | Check the decision request shape; see the Laya section |
| not_enabled | 404 | The decision service is not enabled on this gateway | Use /v1/chat/completions, or ask the operator to enable it |
| upstream_error | (the failed attempt's status) | An upstream failed and the gateway withheld its body, because backend text can quote model names, context limits or account identifiers | Treat it by the status: 5xx retries, 4xx does not. The raw upstream body is logged server-side, not returned |

**Which to retry:** 429, 502 and 503 are worth retrying with exponential backoff, using retry_after when it
is present. **400, 401, 402 and 404 are not** - the same request fails the same way, and content_filtered
in particular is a decision about your content rather than a transient fault.

## Service status

Live availability for the gateway and for each model is published at https://lcapi.ir/status, and the same data is
available as JSON at https://lcapi.ir/api/public/status for monitoring. **Check it before reporting an outage** - if
a model is degraded there, we already know.

## Claude Code

```bash
export ANTHROPIC_BASE_URL=https://lcapi.ir
export ANTHROPIC_AUTH_TOKEN=$LAYERCLOUD_API_KEY
claude
```

```bash
# macOS / Linux
export ANTHROPIC_BASE_URL=https://lcapi.ir
export ANTHROPIC_AUTH_TOKEN=$LAYERCLOUD_API_KEY

# Windows PowerShell
$env:ANTHROPIC_BASE_URL = "https://lcapi.ir"
$env:ANTHROPIC_AUTH_TOKEN = $env:LAYERCLOUD_API_KEY
```

Model overrides (if your key may use them):

```bash
export ANTHROPIC_MODEL=anthropic/claude-sonnet-5
export ANTHROPIC_SMALL_FAST_MODEL=anthropic/claude-sonnet-5
```

## OpenAI Codex

Set Codex to use a custom base URL:

```bash
export OPENAI_BASE_URL=https://lcapi.ir/v1
export OPENAI_API_KEY=$LAYERCLOUD_API_KEY
codex
```

## opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "layercloud": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "LayerCloud",
      "options": { "baseURL": "https://lcapi.ir/v1" },
      "apiKey": "{env:LAYERCLOUD_API_KEY}",
      "models": {
        "openai/gpt-4o": { "name": "GPT-4o" }
      }
    }
  }
}
```

```bash
opencode run --model layercloud/openai-gpt-4o "Hello"
```

## KiloCode

Add the provider in the KiloCode settings (API Providers):
base URL https://lcapi.ir/v1, API key style Bearer with your LayerCloud key.

## Prime Agent (prime CLI)

```bash
export OPENAI_BASE_URL=https://lcapi.ir/v1
export OPENAI_API_KEY=$LAYERCLOUD_API_KEY
prime
```

## Aider

```bash
export OPENAI_API_BASE=https://lcapi.ir/v1
export OPENAI_API_KEY=$LAYERCLOUD_API_KEY
aider --model openai/gpt-4o
```

## Continue (VS Code / JetBrains)

config.json:

```json
{
  "models": [{
    "title": "LayerCloud GPT-5.6",
    "provider": "openai",
    "model": "openai/gpt-4o",
    "apiBase": "https://lcapi.ir/v1",
    "apiKey": "$LAYERCLOUD_API_KEY"
  }]
}
```

## Editors

### Cursor / Windsurf / VS Code (OpenAI-compatible)

- Base URL: https://lcapi.ir/v1
- API key: your LayerCloud key
- Model: any model from the catalog (see https://lcapi.ir/#pricing)

### OpenClaude

Point OpenClaude at the Anthropic-compatible base: https://lcapi.ir

## Notes

- Prices are per 1M tokens and are billed from your pay-as-you-go balance (see https://lcapi.ir/#pricing for the live table).
- Streaming (SSE) is supported on chat completions and Responses.
- Keys can be scoped with quotas and model allow-lists from the console Tokens page.
- If a model is missing from /v1/models, your key's allow-list may not include it.
