Keural API
Send requests in OpenAI Chat Completions format with an API key.
Calls a model registered in Keural in OpenAI Chat Completions format.
The Base URL has the form https://<public API domain for each environment>/llm/v1. The public API domain differs by environment, so check it with your administrator.
| Method | Path | Description |
|---|---|---|
POST | /llm/v1/chat/completions | Create a conversation (streaming supported) |
GET | /llm/v1/models | List of models connected to the key |
GET | /llm/v1/models/{model} | Information about a model connected to the key |
Paths for embeddings, image generation, voice, and the Responses API are not supported. Requests sent to these paths return 404.
Usage flow
| Step | What to do |
|---|---|
| 1. Issue a key | Choose a model under Settings → API and get a key. Copy the model ID and the key. |
| 2. Connect a client | Enter the Base URL, API key, and model ID into the app or code. |
| 3. Use and manage | Check usage on the same screen, and reissue or revoke keys as needed. |
Prerequisites
| Item | Details |
|---|---|
| Keural account | An account you can sign in with |
| API menu | If there is no API menu in settings, ask your administrator to enable the feature. |
| Organization member permission | To get a key in an organization workspace, the organization admin must first allow your API use. See Organization member API permissions. |
| Base URL | The public API address provided by your administrator |
| Client | An app or SDK that supports OpenAI API-compatible connections |
Issuing an API key
- Open Settings → API from the account menu. In the desktop and mobile apps, it is in the Tools section.
- On the API keys tab, choose the key owner (personal workspace or organization workspace) and the Model ID.
- If needed, set the New key expiration. If you leave it blank, the key does not expire until it is revoked.
- Press Issue key, then copy the key with Copy key and store it somewhere safe. The full key is displayed only once.
- A key is connected to the chosen workspace and to one model. Usage is attributed to the person who issued the key.
- A workspace can have only one usable key per model. To get a new key, reissue or revoke the existing key.
- If you lose a key, reissue it. The previous key becomes invalid immediately after reissue.
- If no model is available for issuing, ask your administrator to check the model's public setting, your plan, and the organization API permission.
Organization member API permissions
This section is for organization owners and admins. For members to get keys in an organization workspace, an admin must allow it first. The default for all members is blocked.
Organization management → Organization API consists of the following three sections. If the menu does not appear, the API feature is turned off or the account does not have organization management permission.
| Section | What it does |
|---|---|
| API permissions by user | Turn API use on or off for each member. |
| Issued keys | Check the status of keys issued in the organization and revoke them. You cannot view the full key. |
| API usage | Check the request and token usage of keys issued in the organization. |
| Action | Result |
|---|---|
| Turn permission on | The member can issue keys in the organization workspace. |
| Turn permission off | New issuance and calls with existing keys are blocked. Keys already issued are not deleted automatically, so revoke them under Issued keys if needed. |
When a member leaves or is removed from the organization, the keys that the member received from the organization are revoked automatically.
Connecting to a client
Enter the following three values into an OpenAI API-compatible app (chat client, IDE extension, automation tool, and so on).
| Value | What to enter | Names used by apps |
|---|---|---|
| Base URL | The public API address your administrator provided (…/llm/v1) | Base URL, API Base, Endpoint, Host |
| API Key | The issued key (without leading or trailing spaces) | API Key, Token, Secret Key, Authorization |
| Model | The Model ID on the issue screen. It must exactly match the model connected to the key | Model, Model ID, Model Name |
- In the app settings, add an OpenAI-compatible connection.
- Enter the Base URL and API Key, then save.
- Choose a model from the model list. The list shows only the one model connected to the key.
- Send a short message and check the response.
The request format is as follows.
| Type | Content |
|---|---|
| Chat request | POST <Base URL>/chat/completions |
| Model lookup | GET <Base URL>/models |
| Authentication header | Authorization: Bearer <issued key> |
| Content header | Content-Type: application/json |
First request
export KEURAL_API_KEY='your_issued_key'
export KEURAL_BASE_URL='https://<public API domain for each environment>/llm/v1'
export MODEL_ID='Model ID from the issue screen'
curl "$KEURAL_BASE_URL/chat/completions" \
-H "Authorization: Bearer $KEURAL_API_KEY" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$MODEL_ID\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello.\"}]}"The model in the request body must match the model ID connected to the key. If a path prefix such as provider/model-ID is attached, compare the part after the last /. If they differ, 403 is returned. The model in the response always contains the key's model ID.
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ["KEURAL_API_KEY"],
base_url=os.environ["KEURAL_BASE_URL"],
)
res = client.chat.completions.create(
model=os.environ["MODEL_ID"],
messages=[{"role": "user", "content": "Hello."}],
)
print(res.choices[0].message.content)Supported request fields
Only the 18 top-level fields below are accepted. If any other field is sent, 400 is returned. Optional fields whose value is null are treated as not sent.
| Field | Rule |
|---|---|
model | Required. The model ID of the key |
messages | Required. 1 to 4,096 items |
stream · stream_options | Send stream_options only when stream: true. include_usage is supported. |
tools | Up to 128. Function names are [A-Za-z0-9_-], 1 to 64 characters |
tool_choice | none · auto · required · a specific function. required requires tools to be present. |
parallel_tool_calls | Returns 400 if the model does not support it |
response_format | text · json_object · json_schema |
max_tokens · max_completion_tokens | Send only one of the two. If neither is sent, 4,096 is used (or a lower model limit if one is set). Exceeding the model limit returns 400. |
temperature | 0 to 2. Returns 400 if the model does not support it |
top_p | 0 to 1 |
frequency_penalty · presence_penalty | -2 to 2 |
stop | A string, or an array of up to 4 items |
seed | Integer |
user | Up to 256 characters |
n | 1 only |
Messages
| Role | Accepted fields |
|---|---|
system · developer | content, name |
user | content (a string, or an array of text and image parts), name |
assistant | content, name, refusal, tool_calls |
tool | content, tool_call_id |
- A
toolmessage answers eachidin the precedingtool_callsonly once. - Image parts (
image_url) go only inusermessages for models that accept images. Use anhttporhttpsaddress, or a base64 data URI in PNG, JPEG, GIF, or WebP.detailisauto,low, orhigh.
Body
| Item | Limit |
|---|---|
| Content-Type | application/json |
| Size | 1 MiB |
| JSON structure | One top-level object, nesting depth of 64 or less, no duplicate keys |
| Request time | Up to 600 seconds |
Breaking a body rule returns 400, and param is body.
Streaming
If you include "stream": true in the request, the response is sent as SSE. Chunks are sent as data: ... events, and data: [DONE] is sent at the end. If you include "stream_options": {"include_usage": true}, one additional token usage chunk is sent just before [DONE].
When include_usage is included, every chunk contains usage: null, and the final usage chunk has an empty choices array. A non-streaming response contains usage only when both input and output tokens are confirmed.
The response id has the form chatcmpl-<x-request-id>. Every response header includes x-request-id.
If an error occurs during streaming, a data: event containing the error JSON is sent, and the connection ends without [DONE]. If a chunk exceeds 1 MiB or the response exceeds 16 MiB, the response ends with 503.
curl -N "$KEURAL_BASE_URL/chat/completions" \
-H "Authorization: Bearer $KEURAL_API_KEY" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"$MODEL_ID\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true}}"Usage limits
Requests are limited per user, organization, and model by the number of concurrent requests, requests per minute (RPM), and tokens per minute (TPM). The actual values differ by environment.
Tokens per minute are deducted when a request is received. Input is counted as 1 token per 4 English characters, other characters as 1 token per character, and one image as 2,000 tokens. Output is deducted up to max_tokens (maximum 4,096), so setting the output limit only as high as you need leaves more room under the limit.
429 code | Cause | Retry-After |
|---|---|---|
rate_limit_exceeded | RPM, TPM, or concurrent requests exceeded | 60 seconds for RPM and TPM, 1 second for concurrent requests |
api_busy | The server's processing capacity is exceeded | None. Wait briefly and try again. |
upstream_rate_limit | The model provider's limit is exceeded | None |
Key management
Under Settings → API → API keys, check the status of issued keys (valid, expired, or revoked), and reissue or revoke them.
A key is a 51-character string that starts with krl_llm_. Each key owner can have one key. If a key already exists, reissue it instead of issuing a new one. The expiration date can be set only to a date after today.
| Action | Result | Note |
|---|---|---|
| Reissue | You get a new key. | The previous key becomes invalid immediately. Also update the key in any connected apps. |
| Revoke | The key is blocked immediately. | Use this when the key may have been exposed. A revoked key cannot be restored. |
Checking usage
Check API requests and the tokens they used in the following two places.
| Where | What |
|---|---|
| Settings → API → API usage tab | Usage of keys you issued in the current workspace |
| Organization management → Organization API, then API usage at the bottom | Usage of all keys issued in the organization (organization owners and admins) |
Search conditions
| Condition | Description |
|---|---|
| Start time · End time | Based on Korea time. The start time is included and the end time is not. If left blank, the last 7 days are searched, and you can search up to 90 days at a time. |
| Model ID | Searches only requests for a specific model. If left blank, all models are searched. |
| Request state | Choose from All · Running · Completed · Failed · Cancelled · Unknown. |
Set the conditions and press Search. To reflect requests you just sent, press Refresh.
Values on screen
- Applied time range (KST): The time range actually searched. If the start or end time was left blank or the range exceeded 90 days, the adjusted range is shown.
- Requests and the totals of Input tokens · Output tokens · Total tokens: Only confirmed values are added. Tokens not yet confirmed are shown as Unconfirmed.
- Request list: Each request shows its request time, Model ID, request state, and input, output, and total tokens. 20 items are shown per page. Move between pages with Previous and Next.
Press Request details on a row to show the following values.
| Item | Description |
|---|---|
| Request ID | The value to provide when contacting support |
| Response type | Streaming or non-streaming response |
| Usage confirmation status | Confirmed · Partially confirmed · Unconfirmed |
| End time · Response time (ms) | The time the request ended and the time it took |
| Cache read · Cache creation · Reasoning tokens | Shown only when the model returns these values |
Usage is recorded for streaming requests even if you do not include stream_options.include_usage. If a request is interrupted partway through, it may remain as Partially confirmed or Unconfirmed.
Error format
Errors are returned in OpenAI-compatible format.
{
"error": {
"message": "Human-readable error description",
"type": "authentication_error",
"code": "invalid_api_key",
"param": null
}
}type is determined by the status code, and code distinguishes the cause.
| Status | type | code | Cause |
|---|---|---|---|
400 | invalid_request_error | invalid_request | Violation of a field, value, or body rule. param names the problem field |
400 | invalid_request_error | upstream_rejected_request | The model provider rejected the request |
401 | authentication_error | invalid_api_key | Key format error, no key, expired, revoked, or reissued key, or more than one Authorization header |
403 | permission_error | api_disabled | The Keural API feature is turned off |
403 | permission_error | model_api_disabled | Public API access for the model is turned off |
403 | permission_error | model_not_allowed | model differs from the key's model, or the member has no API permission |
404 | invalid_request_error | not_found | A path that does not exist |
405 | invalid_request_error | method_not_allowed | Wrong method. The allowed methods are in the Allow header |
429 | rate_limit_error | See the Usage limits table above | Limit exceeded |
503 | server_error | upstream_timeout | The response did not finish within 600 seconds |
503 | server_error | invalid_upstream_response | Unexpected response format from the provider, or stream size exceeded |
503 | server_error | dependency_unavailable · unsupported_provider | Temporary internal problem. Include x-request-id when contacting support |
Troubleshooting
| Symptom | Response code | What to check |
|---|---|---|
| The model list is empty or the connection fails | 404 · 405 | Whether the Base URL ends with …/llm/v1 |
| Authentication fails | 401 | Typos or leading and trailing spaces in the key, and whether it has expired, been revoked, or been reissued |
| The connection works but you cannot use the model | 403 | Whether the model ID matches the key's model, and the organization API permission |
| Responses are slow or fail occasionally | 429 | Usage limits |
| No response for a while | 5xx | Try again after a short while |
| Usage is not shown or differs from actual usage | — | The search period and the workspace the key belongs to |
If the problem persists, contact your administrator with the information below. Do not send the full key or the prompt content.
- Time the problem occurred
- Model ID used
- Name and version of the client app
x-request-idin the response headers- Error message shown on screen