Jenova Agent API Reference
Base URL: https://api.jenova.ai/v1
Authentication: Bearer token in Authorization header
Table of Contents
- Overview
- Quick Start
- Core Concepts
- Authentication
- API Endpoints
- Streaming (SSE)
- MCP Server Integration
- Billing
- Rate Limits
- Error Handling
- Pagination
- Localization
- Privacy and Data
- Support
Overview
Run custom agents in your product or workflow in minutes. Set up your agents for any task, choose any model and tools, and let Jenova handle the harness, database, and scale.
The Full Agent Stack:
- Managed Agent Harness: One call in, finished work out. Your agent researches, reasons, calls tools, and produces the files — exactly as you instruct.
- Unbounded Context & Memory: RAG-powered context management and proprietary session state tracking enable unlimited memory within conversations.
- Library of Tools & MCP: Connect to our library of 30+ tools and MCP servers, such as search, image generation, email, and more — or plug in your own MCP servers.
- Seamlessly Switch to Any Model: Deploy models from OpenAI, Anthropic, Google, xAI, or leading open-source providers, and switch between them with one parameter.
- Managed Knowledge & RAG: Jenova handles the end-to-end RAG pipeline — chunking, indexing, storage, and retrieval.
- Proven at Scale: Deploy on battle-tested infrastructure running hundreds of live agents in production. Zero clusters to provision, scale, or maintain.
Quick Start
1. Get Your API Key
Generate an API key from the developer dashboard at www.jenova.ai/platform. Keys use the format jnv_sk_* and are passed as Bearer tokens.
2. Pick or Create an Agent
Choose a pre-built agent from the platform, or create a custom agent in the dashboard with instructions, model settings, knowledge base files, tools, and MCP servers.
3. Send Your First Message
Create a session and send a message in one call:
curl -N -X POST https://api.jenova.ai/v1/messages \
-H "Authorization: Bearer jnv_sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"agent": "my-support-agent",
"user": "user_12345",
"content": "What can you help me with?"
}'
The response streams back as Server-Sent Events:
event: stream_started
data: {"session_id":"ses_xyz789","run_id":"run_abc123","agent":"my-support-agent"}
event: stream_delta
data: {"session_id":"ses_xyz789","run_id":"run_abc123","chunk_content":"I can help you with ","seq":1}
event: stream_ended
data: {"session_id":"ses_xyz789","run_id":"run_abc123","success":true,"stop_reason":"end_run","usage":{"cost":"0.0032"}}
Capture session_id from stream_started for follow-up requests. For synchronous JSON responses, see Send Message.
Core Concepts
Agent
An AI agent you interact with through the API. Each agent has a unique slug (e.g., my-support-agent) used as the agent value in API calls.
- Pre-built: Select from existing agents on the platform.
- Custom: Configure your own in the dashboard with instructions, model, knowledge base, tools, and MCP servers.
Session
An independent conversation thread between an end user and an agent.
- Identifier: Prefixed ID (e.g.,
ses_abc123). - Scope: Multiple sessions can exist for the same agent and end user, each with independent conversation state.
- Lifecycle: Sessions persist indefinitely until deleted via API. For one-shot tasks that do not need a session, use
POST /messageswithephemeral: true. - Platform isolation: API sessions are separate from conversations in the Jenova web app. End users, session history, and billing are independent between the API and web app.
Message
A single entry in a session's conversation history, returned by the Messages endpoints. Each message includes a structured from object with type (user or agent) and name, plus a message type:
external- a conversation message intended to be displayed as chat content.internal- an optional message representing agent work steps during a run, such as tool calls or retrieval.
Run
A single agent execution created when you send a message. A run has a run_id, may stream events while it is active, and may produce one or more completed messages. Each session can have only one active run at a time.
End User
The user field scopes sessions to an end user in your application. Use a stable opaque ID, such as your internal user ID or UUID. Avoid emails or other PII unless your application requires them. Sessions created with the same user value are grouped together, enabling per-user session listing.
If user is omitted, the session is scoped to your developer account and cannot be filtered by end user later. Pass user in production.
For requests to an existing session (any endpoint with {session_id} in its path), user is an optional ownership guard. If you provide it, it must match the user value used when the session was created; otherwise the API returns 404 session_not_owned. Send it as a query parameter on GET and DELETE requests, and in the JSON body on POST and PATCH requests. A user query parameter on POST or PATCH returns 400 bad_request.
Authentication
Authenticate every request with a Bearer token in the Authorization header:
Authorization: Bearer jnv_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API keys are generated from the developer dashboard.
User-Agent is optional. SDKs may set it for diagnostics, but the API does not require it.
Key format: Keys start with the prefix jnv_sk_ followed by a base62-encoded random string.
Limits: Each developer account can have up to 10 active API keys.
API Endpoints
Messages
Message sends are the primary API path. Use POST /messages for the first message; it creates the session and starts the run in one request. Use POST /sessions/{session_id}/messages when continuing a captured session_id.
Send Message
POST /messages
Creates a persistent session and sends the first message in one request. Set ephemeral: true for a streaming one-shot request that returns no session ID and cannot be continued. Its data is deleted shortly after the request completes.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
agent | string | Yes | - | The agent's slug identifier |
content | string | Conditional | - | Message text. Required unless file_urls is provided |
file_urls | string[] | Conditional | - | URLs of files to attach. Required unless content is provided |
user | string | No | - | Your external end user identifier (max 255 characters). If omitted, defaults to your developer account |
session_name | string | No | - | Display name for the new session (max 200 characters) |
ephemeral | boolean | No | false | Streaming-only one-shot request. Returns no session ID, cannot be continued, and its data is deleted shortly after the request completes |
stream | boolean | No | true | true for SSE streaming, false for JSON. MCP authorization requires streaming |
include_progress | boolean | No | true | Set to false to omit stream_progress events. Streaming only |
model | string | No | - | One-time model override for this request only. Use a stable model ID such as claude-sonnet-5. Does not change the session's default model |
additional_context | object[] | No | - | Named context for this request only. See Additional Context |
Example - Streaming (default)
curl -N -X POST https://api.jenova.ai/v1/messages \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"agent": "my-support-agent",
"user": "user_12345",
"content": "Hello, I need help with my account"
}'
The response is an SSE stream (see Streaming for event format). Persistent requests include the new session_id; ephemeral: true requests omit session_id and must use streaming.
Example - JSON (non-streaming)
curl -X POST https://api.jenova.ai/v1/messages \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"agent": "my-support-agent",
"user": "user_12345",
"content": "Hello, I need help with my account",
"stream": false
}'
Response
Streaming responses emit the SSE events documented in Streaming. Non-streaming responses return the message object with stop_reason and usage for the request, as shown in Continue Session. stop_reason is end_run, or user_cancelled if the run was cancelled after the agent produced a message.
If the run is cancelled before the agent produces a message, the API returns 200 OK with status: "cancelled", session_id, run_id, stop_reason: "user_cancelled", and usage.
If a non-streaming run is still processing after 90 seconds, the API returns 202 Accepted with status: "processing", session_id, run_id, and a human-readable message. The run continues after the response or client disconnect; check the session's messages, or use streaming for longer workflows.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | missing_required_field | agent is required |
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | bad_request | Invalid ephemeral mode, invalid additional_context, or user/session_name exceeds its maximum length |
| 400 | content_or_uploaded_files_required | Neither content nor file_urls provided |
| 400 | content_too_long | Message content exceeds 32,000 tokens |
| 400 | exceed_max_upload_files | More than 10 distinct file URLs in a single request |
| 400 | unsupported_file_format | A file URL has an unsupported file extension |
| 400 | invalid_file_url | A file URL is malformed or not HTTPS |
| 400 | invalid_model_selection | model is not a valid model ID, or the agent's default model is not available |
| 402 | insufficient_credits | Not enough credits to create the session or send the message |
| 404 | agent_not_found | Agent does not exist or is not accessible to your account |
Continue Session
POST /sessions/{session_id}/messages
Sends a message to an existing persistent session and receives the agent's response. Responses stream via SSE by default; set stream: false for JSON.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
content | string | Conditional | - | Message text. Required unless file_urls is provided |
file_urls | string[] | Conditional | - | URLs of files to attach. Required unless content is provided |
stream | boolean | No | true | true for SSE streaming, false for JSON. MCP authorization requires streaming |
include_progress | boolean | No | true | Set to false to omit stream_progress events. Streaming only |
model | string | No | - | One-time model override for this request only. Use a stable model ID such as claude-sonnet-5. Does not change the session's default model |
additional_context | object[] | No | - | Named context for this request only. See Additional Context |
Example - Streaming (default)
curl -N -X POST https://api.jenova.ai/v1/sessions/ses_abc123/messages \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"content": "How do I reset my password?"
}'
Example - JSON (non-streaming)
curl -X POST https://api.jenova.ai/v1/sessions/ses_abc123/messages \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"content": "How do I reset my password?",
"stream": false
}'
Response 200 OK
{
"id": "msg_xyz789",
"session_id": "ses_abc123",
"sequence": 4,
"from": {
"type": "agent",
"name": "my-support-agent"
},
"type": "external",
"time": "2026-05-19T10:31:05Z",
"content": "To reset your password, go to Settings > Security > Change Password...",
"model": "claude-sonnet-5",
"files": [],
"stop_reason": "end_run",
"usage": {
"cost": "0.0015"
}
}
If the session already has an active run or queued messages, the API returns JSON 202 Accepted with status: "queued", session_id, run_id, and message_id. The user message is processed by the active run. Requests with additional_context return 409 busy instead.
Non-streaming responses follow the same rules as Send Message, including the status: "cancelled" response when the run is cancelled before the agent produces a message.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | bad_request | Invalid additional_context |
| 400 | content_or_uploaded_files_required | Neither content nor file_urls provided |
| 400 | content_too_long | Message content exceeds 32,000 tokens |
| 400 | exceed_max_upload_files | More than 10 distinct file URLs in a single request |
| 400 | unsupported_file_format | A file URL has an unsupported file extension |
| 400 | invalid_file_url | A file URL is malformed or not HTTPS |
| 400 | invalid_model_selection | model is not a valid model ID, or the session's model is no longer available |
| 402 | insufficient_credits | Not enough credits to send a message |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | busy | The session is temporarily unavailable because another update is in progress, or additional_context was sent while the session has an active run or queued messages |
| 429 | too_many_pending | The session already has 200 queued messages |
| 503 | service_unavailable | Service temporarily unavailable |
Idempotency
POST /messages, POST /sessions/{session_id}/messages, and POST /invoke accept an optional Idempotency-Key header (max 255 characters). Use a unique key for each logical user send so network retries or double-submits do not create duplicate runs. Keys are kept for 24 hours.
curl -N -X POST https://api.jenova.ai/v1/messages \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: send-user_12345-2026-05-19T10:30:00Z" \
-d '{
"agent": "my-support-agent",
"user": "user_12345",
"content": "Hello"
}'
Non-streaming: Retrying returns the saved response with the Idempotent-Replayed: true header, including errors returned after the request started processing. A saved 202 Accepted response is replayed unchanged; use Get Run Status or List Messages to check the result. Requests rejected before processing starts, such as validation or credit errors, are not saved, so a retry is evaluated again.
Streaming: The stream is not replayed. For POST /messages and POST /sessions/{session_id}/messages, retrying while running or after completion returns an idempotency error with the original run_id and, for persistent requests, session_id. For persistent requests, use Get Run Status to check an active run, or List Messages to fetch persisted results. Streamed POST /invoke results cannot be retrieved after the stream closes. POST /invoke retries return the original id instead of run_id and session_id.
Completed streaming retry example:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": {
"code": "idempotency_key_reused",
"message": "This Idempotency-Key has already completed. Streaming responses cannot be replayed; use the returned session_id and run_id to retrieve the result."
},
"idempotency": {
"status": "completed",
"session_id": "ses_abc123",
"run_id": "run_abc123"
}
}
Idempotency errors
| Status | Code | Condition |
|---|---|---|
| 400 | bad_request | Idempotency-Key exceeds its maximum length |
| 409 | idempotency_key_reused | The same key was used with a different request |
| 409 | idempotency_key_in_use | The original request is still running |
| 409 | idempotency_key_reused | The original streaming request already completed and cannot be replayed |
List Messages
GET /sessions/{session_id}/messages
Returns a paginated list of the session's messages. By default only external conversation messages are returned. The first page contains the most recent messages; within each page, messages are in chronological order (oldest first). sequence is the message's ordering number within the session.
Query parameters
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
limit | integer | 20 | 100 | Messages per page |
cursor | string | - | - | Pagination cursor |
include | string | external | - | Message types to return: external, internal, or all |
Example
curl "https://api.jenova.ai/v1/sessions/ses_abc123/messages?limit=50" \
-H "Authorization: Bearer jnv_sk_xxx"
Response 200 OK
{
"items": [
{
"id": "msg_002",
"session_id": "ses_abc123",
"sequence": 4,
"from": {
"type": "agent",
"name": "my-support-agent"
},
"type": "external",
"time": "2026-05-19T10:31:05Z",
"content": "To reset your password, go to Settings > Security...",
"model": "claude-sonnet-5",
"stop_reason": "end_run"
}
],
"next_cursor": "",
"has_more": false
}
Message object
| Field | Type | Description |
|---|---|---|
id | string | Message ID (prefixed with msg_) |
session_id | string | Parent session ID |
sequence | integer | Ordering number within the session. Numbers can be reused after messages are deleted |
from | object | Sender object with type (user or agent) and name |
type | string | external for conversation messages, or internal for agent work steps |
time | string | ISO 8601 timestamp |
content | string | Text content. Present on external messages |
model | string | Stable model ID that generated the response. Only present on agent messages |
files | array | Attached or generated files included with the message. See File object below. Omitted from List Messages and Get Message when the message has no files |
stop_reason | string | Present on external agent messages with content. Value is end_run |
agent | string | Executing agent slug, when available |
agent_name | string | Executing agent display name, when available |
progress_events | array | Activity recorded for an internal message, when available. Each entry has the same fields as a stream_progress event |
File object
| Field | Type | Description |
|---|---|---|
file_id | string | Jenova file ID. Empty when unknown |
name | string | File name |
url | string | File URL. Empty when unknown |
format | string | Lowercase file format, such as pdf, png, or csv |
size | integer | File size in bytes. 0 when unknown |
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | bad_request | Invalid query parameter |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
Get Message
GET /sessions/{session_id}/messages/{message_id}
Retrieves a single message by ID.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
include | string | external | Message types that can be returned: external, internal, or all |
Response 200 OK
Returns a single message object with the same structure as the list response.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | bad_request | Invalid query parameter |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 404 | not_found | Message does not exist in this session |
File Attachments
Pass publicly accessible HTTPS URLs in the file_urls field.
| Limit | Value |
|---|---|
| Max files per message | 10 |
| Max file size | 20 MB per file |
Supported formats
- Images: JPG, JPEG, PNG, WebP
- Documents: PDF, DOCX, XLSX, PPTX, TXT, CSV, RTF, MD, HTML, XML, JSON, LOG
- Code: JS, TS, TSX, JSX, PY, Java, Go, C, CPP, H, HPP, CS, RB, PHP, RS, Swift, KT, Scala, SQL, CSS, YAML, YML
File URLs are fetched when the agent reads them during the run, not when the request is accepted. A URL that cannot be fetched, or a file over 20 MB, is not read; the request itself is not rejected.
When listing messages, attached files appear in the message's files array.
Additional Context
Pass named context for a single message in additional_context, such as details about the end user's account. It is not saved to message history.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Lowercase letters, digits, and underscores, starting with a letter (max 64 characters). Unique within the request, ignoring underscores and plural forms |
content | string | Yes | Context text. Must not be empty (max 12,000 tokens) |
| Limit | Value |
|---|---|
| Max items per message | 4 |
| Max tokens per item | 12,000 |
Use a descriptive name such as customer_profile or order_details.
An item with a missing or non-string name or content returns 400 invalid_payload. Other invalid items return 400 bad_request.
curl -N -X POST https://api.jenova.ai/v1/sessions/ses_abc123/messages \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"content": "How do I reset my password?",
"additional_context": [
{
"name": "customer_profile",
"content": "Plan: Pro. Last login: 2026-05-18."
}
]
}'
Sessions
Sessions are persistent conversations between an end user and an agent. Most integrations can create them implicitly with POST /messages.
Create Session
POST /sessions
Creates an empty persistent session bound to a specific agent. Use this when you need a session ID before the first message; otherwise prefer POST /messages.
Note:
ephemeralis not accepted onPOST /sessions; usePOST /messageswithephemeral: truefor one-shot requests.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agent | string | Yes | The agent's slug identifier |
user | string | No | Your external end user identifier (max 255 characters). If omitted, defaults to your developer account |
session_name | string | No | Display name for the session (max 200 characters) |
Example
curl -X POST https://api.jenova.ai/v1/sessions \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"agent": "my-support-agent",
"user": "user_12345",
"session_name": "Billing inquiry"
}'
Response 201 Created
{
"id": "ses_abc123",
"session_name": "Billing inquiry",
"agent": "my-support-agent",
"user": "user_12345",
"model": "claude-sonnet-5",
"created_at": "2026-05-19T10:30:00Z",
"updated_at": "2026-05-19T10:30:00Z"
}
Session object
| Field | Type | Description |
|---|---|---|
id | string | Session ID (prefixed with ses_) |
session_name | string | Display name |
agent | string | Agent slug |
agent_details | object | Agent details with display_name. Only present when requested with include=agent |
user | string | End user identifier. null if the session was created without user |
model | string | Model used when a message does not specify one. Set to the agent's default model when the session is created. Empty when the agent lets Jenova choose the model |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | missing_required_field | agent is required |
| 400 | bad_request | ephemeral was provided, or user/session_name exceeds its maximum length |
| 402 | insufficient_credits | Not enough credits to create a session |
| 404 | agent_not_found | Agent does not exist or is not accessible to your account |
List Sessions
GET /sessions
Returns a paginated list of your sessions, ordered by most recently updated.
Query parameters
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
limit | integer | 20 | 100 | Items per page |
cursor | string | - | - | Pagination cursor |
user | string | - | - | Filter by end user identifier (max 255 characters) |
agent | string | - | - | Filter by agent slug |
include | string | - | - | Set to agent to add agent_details to each session |
Example
curl "https://api.jenova.ai/v1/sessions?user=user_12345&limit=10" \
-H "Authorization: Bearer jnv_sk_xxx"
Response 200 OK
{
"items": [
{
"id": "ses_abc123",
"session_name": "Billing inquiry",
"agent": "my-support-agent",
"user": "user_12345",
"model": "claude-sonnet-5",
"created_at": "2026-05-19T10:30:00Z",
"updated_at": "2026-05-19T11:15:00Z"
}
],
"next_cursor": "2026-05-19T11:15:00Z|ses_abc123",
"has_more": true
}
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | bad_request | Invalid query parameter |
Get Session
GET /sessions/{session_id}
Retrieves a single session by ID.
Query parameters
| Parameter | Type | Description |
|---|---|---|
include | string | Set to agent to add agent_details to the session |
Response 200 OK
Returns a session object.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | bad_request | Invalid query parameter |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
Rename Session
PATCH /sessions/{session_id}
Updates the display name of a session.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
session_name | string | Yes | New display name (max 200 characters) |
Response 200 OK
Returns the updated session object.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | missing_required_field | session_name is required |
| 400 | bad_request | session_name exceeds its maximum length |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | busy | The session is temporarily unavailable because another update is in progress |
Delete Session
DELETE /sessions/{session_id}
Permanently deletes a session and all of its messages. The session must not have an active run.
Response 204 No Content
Errors
| Status | Code | Condition |
|---|---|---|
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | busy | Session has an active run - cancel it first |
Operations
These endpoints are recovery and editing controls for persistent sessions. Most integrations only need Cancel Active Run; use the other operations when you intentionally want to alter or recover session state. All operations support the optional user ownership guard described in End User.
Cancel Active Run
POST /sessions/{session_id}/cancel
Cancels the active agent run. This does not delete messages that were already completed before cancellation.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
run_id | string | No | Optional stale-run guard. If supplied and it does not match the active run, the API returns 409 stale_run |
Response 204 No Content
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | cancel_not_allowed | No active run to cancel, or cancellation not permitted |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | stale_run | Provided run_id does not match the active run |
Undo Active Run
POST /sessions/{session_id}/undo
Cancels the active run, waits for it to stop, then removes any messages it already added. No additional output is persisted after the undo is issued.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
run_id | string | No | Optional stale-run guard. If supplied and it does not match the active run, the API returns 409 stale_run |
Response 200 OK
{
"session_id": "ses_abc123",
"run_id": "run_abc123",
"deleted": ["msg_002", "msg_001"]
}
If the run is cancelled before any message is completed, deleted is an empty array.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | cancel_not_allowed | No active run to undo, or cancellation not permitted |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | stale_run | Provided run_id does not match the active run |
| 409 | busy | The session is temporarily unavailable because another update is in progress |
Delete Recent Messages
POST /sessions/{session_id}/messages/delete
Removes the count most recent messages from an idle session, including internal messages. Use include=all on List Messages to see them. The session must not have an active run.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
count | integer | Yes | Number of recent messages to delete from the end (must be greater than 0). Values above 100 are treated as 100 |
Response 200 OK
{
"deleted": ["msg_002", "msg_001"]
}
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | bad_request | count missing, zero, or negative; or no messages to delete |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | busy | Session has an active run |
Fork Session
POST /sessions/{session_id}/fork
Creates a new session by copying the source session up to a specific message. The source session must not have an active run.
Request body (optional)
| Field | Type | Required | Description |
|---|---|---|---|
message_id | string | No | Fork point message ID. If omitted, forks from the last message |
Response 201 Created
Returns the newly created session object with the same structure as session creation.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | bad_request | message_id is invalid |
| 402 | insufficient_credits | Not enough credits to fork a session |
| 404 | not_found | message_id does not exist in this session |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | busy | Source session has an active run |
Get Run Status
GET /sessions/{session_id}/runs/{run_id}
Returns the current state of an active run. Use this after a dropped SSE connection or an idempotency response that returned a run_id. After a run finishes, fetch results with GET /sessions/{session_id}/messages.
Response 200 OK
{
"session_id": "ses_abc123",
"run_id": "run_abc123",
"status": "streaming",
"started_at": "2026-05-19T10:30:00Z",
"updated_at": "2026-05-19T10:30:06Z",
"content": "Partial visible response text so far"
}
status is running while the agent is working, streaming once response text is being produced, or ended briefly after the run finishes. The response may also include progress_events with recent activity, in the same format as stream_progress. When status is ended or the request returns 404 not_found, fetch the session's messages.
Errors
| Status | Code | Condition |
|---|---|---|
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 404 | not_found | The run is not active for this session |
Credits
Get Balance
GET /credits/balance
Returns your current credit balance.
Response 200 OK
{
"balance": "123.45"
}
Agents
Create and edit custom agents in the dashboard. API support for creating and editing agents is coming soon.
Scheduled/background workflows are not currently supported through the API. Support is coming soon.
List Agents
GET /agents
Returns the agents available to your account. Use the agent value when creating sessions or sending messages.
Response 200 OK
{
"agents": [
{
"agent": "jenova",
"display_name": "Jenova",
"description": "General-purpose Jenova agent"
},
{
"agent": "my-support-agent",
"display_name": "Support Agent",
"description": "Answers customer questions"
}
]
}
| Field | Type | Description |
|---|---|---|
agent | string | Stable agent slug to pass as the agent value |
display_name | string | Human-readable display name |
description | string | Agent description |
Models
List Models
GET /models
Returns all models available for use in the model field when sending messages or calling POST /invoke.
Response 200 OK
{
"models": [
{
"id": "claude-opus-4-8",
"name": "Claude Opus 4.8",
"thinking_variant": "claude-opus-4-8-thinking"
},
{
"id": "claude-opus-4-8-thinking",
"name": "Claude Opus 4.8 (Thinking)"
},
{
"id": "claude-sonnet-5",
"name": "Claude Sonnet 5",
"thinking_variant": "claude-sonnet-5-thinking"
}
]
}
| Field | Type | Description |
|---|---|---|
id | string | Stable model identifier. Pass this as the model value in Send Message or Invoke Model |
name | string | Human-readable display name |
thinking_variant | string | Model ID of the thinking/reasoning variant. Present only on base models that support reasoning |
Models with a thinking_variant support extended reasoning. Use the variant ID directly in the model field to enable it.
If no model is specified when sending a message, the session's model is used. It is set to the agent's default model when the session is created.
Invoke Model
POST /invoke
Sends a prompt directly to a model, without an agent, session, or tools. Responses stream via SSE by default; set stream: false for JSON.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
model | string | Yes | - | Model ID from List Models |
content | string | Yes | - | Prompt, including any instructions |
stream | boolean | No | true | true for SSE streaming, false for JSON |
user | string | No | - | Your external end user identifier (max 255 characters). If omitted, defaults to your developer account |
Example
curl -X POST https://api.jenova.ai/v1/invoke \
-H "Authorization: Bearer jnv_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"content": "Describe a forest in one sentence.",
"stream": false
}'
Response 200 OK
{
"id": "inv_abc123",
"model": "claude-sonnet-5",
"content": "Sunlight filters through tall pines onto a quiet, mossy floor.",
"usage": {
"cost": "0.0004"
}
}
Streaming responses send stream_started, then stream_delta events as the model generates text, and end with stream_ended, which carries usage. Concatenate chunk_content values in seq order to build the streamed response. If the request fails, stream_error is sent before stream_ended. Events carry id instead of session_id and run_id.
usage.cost is null when the cost is unavailable. If a non-streaming request fails after the model call starts, the error response includes top-level id and usage fields.
Streaming requests are limited to 120 minutes. Non-streaming requests wait up to 30 minutes for the result instead of returning 202 Accepted, and return an error if the result is not ready by then.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | missing_required_field | content is required |
| 400 | content_too_long | Content exceeds 32,000 tokens |
| 400 | bad_request | user exceeds its maximum length |
| 400 | invalid_model_selection | model is missing or not a valid model ID |
| 400 | content_policy_violation | The model provider rejected the request for content policy reasons |
| 400 | llm_context_limit_reached | Input exceeds the model's context window |
| 402 | insufficient_credits | Not enough credits to invoke the model |
| 500 | internal_error | Unexpected server error |
| 502 | llm_api_error | Model provider or upstream model API error |
| 502 | llm_empty_response | Model returned an empty response |
| 502 | llm_reasoning_limit_reached | Model reached its reasoning limit without a response |
| 502 | llm_generation_limit_reached | Model reached its generation limit without a response |
| 503 | service_unavailable | Service temporarily unavailable |
Docs
GET /docs?lang=en
GET /doc?lang=en
Returns this reference as Markdown. Use lang to select the language.
Streaming (SSE)
When stream is omitted or true (the default), message responses are delivered as Server-Sent Events. Use message_completed events to identify messages that are ready to fetch or render.
Timeout: SSE connections stay open until the run ends. Runs are limited to 120 minutes. Non-streaming message requests wait up to 90 seconds, then return 202 Accepted while the run continues. For non-streaming POST /invoke, see Invoke Model.
Connection Headers
Message streams set these headers:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
X-Run-Id: run_abc123
X-Run-Id is available before the first SSE event. POST /invoke streams do not set X-Run-Id; use the id in stream_started.
Reconnect and Recovery
SSE streams are not replayed. If the connection drops on a persistent request, use the captured session_id and run_id to recover state:
curl "https://api.jenova.ai/v1/sessions/ses_abc123/runs/run_abc123" \
-H "Authorization: Bearer jnv_sk_xxx"
If the run is still active, this returns the current status, partial text, and recent progress. If it returns status: "ended" or 404 not_found, the run is no longer active; fetch the session's messages to reconcile completed output:
curl "https://api.jenova.ai/v1/sessions/ses_abc123/messages?limit=20" \
-H "Authorization: Bearer jnv_sk_xxx"
Frame Format
Each SSE frame follows the standard format:
event: <event_type>
data: <json_payload>
Two newlines terminate each frame.
Event Types
Streams include lifecycle, text delta, thinking, progress, warning, message completion, MCP connection, error, final, and ping events. Some event types are only sent when relevant.
For ephemeral requests (ephemeral: true), every SSE event omits session_id. Use run_id only to correlate events inside that one stream.
For final reconciliation on persistent requests, wait for stream_ended, then call List Messages.
Common fields on run-scoped events:
| Field | Description |
|---|---|
session_id | Session ID. Omitted for ephemeral streams. Capture this from stream_started for follow-up requests when using persistent POST /messages |
run_id | Current run ID, when available |
id | Invoke request ID (prefixed with inv_). Only present on POST /invoke streams, which have no session_id or run_id |
stream_started
Sent once when the run starts.
| Field | Description |
|---|---|
agent | Session agent slug, when available |
stream_delta
Sent repeatedly as the agent generates visible response text. Concatenate chunk_content values in seq order to build the streamed response.
event: stream_delta
data: {"session_id":"ses_abc123","run_id":"run_abc123","chunk_content":"To reset your ","seq":1}
| Field | Description |
|---|---|
chunk_content | Text chunk |
seq | Monotonic chunk sequence within the stream |
stream_thinking
Sent repeatedly while the agent emits thinking output. Use this for a separate thinking indicator or trace view; do not concatenate it into the final response text.
| Field | Description |
|---|---|
content | Thinking text chunk |
stream_progress
Reports user-visible activity during generation, such as reading a document, searching the web, or waiting for a user action. These events are intended for transient UI display; ignore unknown fields. Use completed messages for the authoritative message history.
Set include_progress: false on a message request to omit only stream_progress. Lifecycle events, ping, message_completed, errors, and terminal events are still sent when relevant.
| Field | Description |
|---|---|
state | Activity state: running, success, failed, or skipped. Handle unknown values gracefully |
label | Human-readable activity label |
Some progress events may include url, file_name, or server_name as optional display hints.
message_completed
Sent each time a message is complete and ready to fetch or render.
This is a boundary marker, not the full message object. Fetch the message if you need content or metadata.
event: message_completed
data: {"session_id":"ses_abc123","run_id":"run_abc123","message_id":"msg_abc123","sequence":4,"from":{"type":"agent","name":"my-support-agent"},"type":"external"}
| Field | Description |
|---|---|
message_id | Completed message ID |
sequence | Ordering number within the session |
from | Sender object with type (user or agent) and name |
type | Message type: external or internal |
mcp_connection
Sent when the agent needs the end user to connect or authorize one or more MCP servers before it can continue. This event is only available in streaming mode.
| Field | Description |
|---|---|
connection_server_list | MCP servers that need a connection action. Each server includes mcp_server_id, mcp_server_name, and optional auth_url |
user_action_deadline_unix | Unix timestamp when the connection user action expires |
mcp_connection_resolved
Sent when the MCP connection user action has been resolved or has expired.
No additional fields beyond the common run-scoped fields.
warning
Sent for non-fatal warnings during a run.
| Field | Description |
|---|---|
message | Human-readable non-fatal warning |
code | Optional warning code |
stream_error
Sent when a run fails. A final stream_ended event may follow with success: false and stop_reason: "error".
| Field | Description |
|---|---|
code | Error code |
message | Human-readable error message |
stream_ended
Sent once when the run finishes. This is the final event for the stream.
event: stream_ended
data: {"session_id":"ses_abc123","run_id":"run_abc123","success":true,"stop_reason":"end_run","usage":{"cost":"0.0032"}}
Cancelled run example:
event: stream_ended
data: {"session_id":"ses_abc123","run_id":"run_abc123","success":false,"stop_reason":"user_cancelled","usage":{"cost":"0.0012"}}
| Field | Description |
|---|---|
success | Whether the run completed successfully |
stop_reason | Terminal run reason: end_run, user_cancelled, user_action_timeout, or error |
usage | Usage object for this request. cost is always present and is null when the cost is unavailable |
ping
Keepalive frames sent every 15 seconds to prevent proxy/CDN timeouts. Ignore these in your client.
MCP Server Integration
Connect your agents to external tools via the Model Context Protocol (MCP):
- Jenova-managed MCP servers: Jenova-hosted servers for search, content retrieval, document generation, and other built-in capabilities
- Remote MCP servers: Other remote MCP servers configured for your agent
MCP servers must be configured in the dashboard when creating or editing your agent. To use your own MCP server, add it to a custom agent, then call that agent through the API. The API executes tools enabled in the agent's configuration; no additional setup is required in API requests.
When an agent uses MCP tools during a response, progress events are sent in the stream as they occur:
event: stream_progress
data: {"session_id":"ses_...","run_id":"run_...","state":"running","label":"Searching Google"}
If the agent needs the end user to connect or authorize an MCP server during the run, streaming responses may include mcp_connection and mcp_connection_resolved events. Present the connection server list to your end user and open the provided auth_url when present. Keep the SSE stream open while the end user connects or authorizes the server.
After authorization, Jenova stores the token, the authorization window shows a completion page, and the same run continues automatically. The end user does not need to resend the message.
If the end user does not connect, authorize, or skip or mute before user_action_deadline_unix, the run ends with stop_reason: "user_action_timeout". You may also cancel the active run with POST /sessions/{session_id}/cancel.
Store the auth_url client-side. If the client disconnects during authorization, the URL remains valid until user_action_deadline_unix. For persistent requests, check the run with Get Run Status, or fetch messages after the run finishes.
Non-streaming requests (stream: false) do not support MCP connection user actions; use streaming for agents that may need this interaction.
Skip MCP Connection
POST /sessions/{session_id}/mcp/connection/skip
Dismisses a pending MCP connection user action and lets the run continue without that connection.
To mute future connection prompts for one server for the same end user and agent, include mcp_server_id and mute. mute accepts a duration such as 30m, 24h, or 7d, or forever.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
run_id | string | Yes | Active run ID from the mcp_connection event |
mcp_server_id | string | No | Required with mute; use the mcp_server_id from the event |
mute | string | No | A duration such as 30m, 24h, or 7d, or forever |
Response 204 No Content
The stream emits mcp_connection_resolved, then the run continues with the same run_id.
Errors
| Status | Code | Condition |
|---|---|---|
| 400 | invalid_payload | Malformed JSON or a field has an invalid type |
| 400 | bad_request | Missing run_id, no active run, or no MCP connection to skip |
| 400 | bad_request | Invalid mute, missing mcp_server_id when mute is set, or mcp_server_id is not valid for this session |
| 404 | session_not_found | Session does not exist |
| 404 | session_not_owned | Session belongs to another developer or does not match the supplied user |
| 409 | stale_run | run_id does not match the active run |
Billing
All costs are deducted from your developer credit balance. View usage and top up credits at www.jenova.ai/platform.
Pricing
| Operation | Cost |
|---|---|
| Create Session | $0.01 flat for every persistent session created by POST /sessions or POST /messages |
| Fork Session | $0.05 flat, with no separate session creation fee |
| Send Message | Variable (see below) |
| Invoke Model | Variable, based on model and tokens |
Message cost depends on:
- Model - different models have different per-token costs
- Context length - longer sessions consume more input tokens per request
- Workflow complexity - longer workflows and heavier tool use (web search, file generation, document analysis) increase total token consumption
The actual cost is returned as stream_ended.usage.cost for streaming requests and usage.cost for non-streaming JSON requests.
Credit Holds
Each POST /messages, POST /sessions/{session_id}/messages, and POST /invoke request places a $0.50 hold on your credit balance before execution begins. This reserves funds for the request. A message queued into an active run (202 Accepted with status: "queued") does not create an additional hold; the active run's usage is checked against your remaining balance.
When the request completes, the hold is settled to the actual cost and the difference is released. If the actual cost exceeds the hold, the difference is also charged, so your balance can go below zero. Cancelled and failed requests are charged only for usage already incurred. If a request fails before reaching the model, the full hold is released.
Because of holds, your available balance may temporarily appear lower during in-flight requests. You need at least $0.50 in available balance for a POST /sessions/{session_id}/messages, ephemeral POST /messages, or POST /invoke request. A persistent POST /messages request also creates a session and needs at least $0.51 to cover the hold plus the session creation fee.
Rate Limits
Every developer account is subject to three rate limit dimensions:
| Dimension | Default | Description |
|---|---|---|
| RPM (Requests Per Minute) | 200 | Fixed 60-second window |
| RPD (Requests Per Day) | 10,000 | Fixed 24-hour window |
| Concurrent | 10 | Maximum simultaneous in-flight requests and active runs |
A run keeps its concurrent slot until it ends, even after a 202 Accepted response or a client disconnect. A POST /invoke request keeps its slot until it finishes, even if the client disconnects.
GET and HEAD requests do not consume concurrent slots. cancel, undo, and mcp/connection/skip do not consume concurrent slots, so these operations remain available when all slots are in use. These requests still count toward RPM and RPD.
A separate per-IP limit of 1,200 requests per 60 seconds also applies to all requests and returns the same 429 rate_limit_exceeded error.
Response Headers
Authenticated API responses include rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Your RPM limit |
X-RateLimit-Remaining | Requests remaining in the current minute window |
X-RateLimit-Reset | Unix timestamp when the current window resets |
Retry-After | Seconds to wait before retrying. Sent on 429 rate_limit_exceeded and 503 write_freeze_active responses |
When the daily limit is exceeded, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset describe the daily window instead.
When a limit is exceeded, the API returns 429 Too Many Requests:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please try again later."
}
}
Error Handling
Immediate HTTP errors and non-streaming run errors follow a consistent envelope:
{
"error": {
"code": "error_code_string",
"message": "Human-readable description"
}
}
Error messages are localized based on lang or Accept-Language (see Localization).
Streaming run errors are delivered as stream_error events. A failed run may still send a final stream_ended event with success: false and stop_reason set. Non-streaming run errors include a top-level usage object; cost is null when the cost is unavailable.
These errors can occur on any endpoint:
| Status | Code | Condition |
|---|---|---|
| 401 | invalid_api_key | API key is missing, invalid, or revoked |
| 402 | developer_not_activated | Your developer account is not activated yet. Purchase credits in the dashboard to activate it |
| 403 | developer_disabled | Your developer account is disabled |
| 403 | forbidden | Requests from this network are blocked |
| 413 | payload_too_large | Request body exceeds 1 MiB |
| 429 | rate_limit_exceeded | A rate limit was exceeded. See Rate Limits |
| 503 | write_freeze_active | Write requests are temporarily disabled for maintenance |
Endpoint-specific errors are documented inline under each endpoint.
Run Errors After Start
Run errors surface after a message request has been accepted. In streaming mode, they appear as stream_error events and may be followed by stream_ended with success: false. In non-streaming mode, they are returned as a JSON error response with the HTTP status below.
| Non-streaming HTTP Status | Code | Condition |
|---|---|---|
| 400 | content_policy_violation | The model provider rejected the request for content policy reasons |
| 400 | llm_context_limit_reached | Input exceeds the model's context window |
| 404 | session_not_found | Session was deleted before the run could execute |
| 409 | busy | Session became busy or temporarily unavailable before the run could start |
| 413 | total_image_size_exceeded | Combined image payload exceeds the model's per-request size limit |
| 500 | internal_error | Unexpected run failure |
| 500 | response_interrupted | The run was interrupted. Send the message again |
| 500 | response_interrupted_message_saved | The run was interrupted after your message was saved. Fetch the session's messages before sending again |
| 502 | llm_api_error | Model provider or upstream model API error |
| 502 | llm_empty_response | Model returned an empty response |
| 502 | llm_reasoning_limit_reached | Model reached its reasoning limit without a response |
| 502 | llm_generation_limit_reached | Model reached its generation limit without a response |
Pagination
List Sessions and List Messages use cursor-based pagination:
{
"items": [],
"next_cursor": "2026-05-19T11:15:00Z|ses_abc123",
"has_more": true
}
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
limit | integer | 20 | 100 | Number of items per page |
cursor | string | - | - | Opaque cursor from a previous next_cursor |
Pass next_cursor as the cursor query parameter to fetch the next page, URL-encoding the value. When has_more is false, there are no more results. A limit outside 1-100 or an invalid cursor returns 400 bad_request.
Localization
All endpoints accept an optional lang query parameter to control the language of error messages and any localized content.
| Source | Priority | Example |
|---|---|---|
lang query parameter | Highest | ?lang=zh |
Accept-Language header | Fallback | Accept-Language: ja |
| Default | Lowest | English (en) |
You can append ?lang=xx to any request URL:
POST /sessions?lang=zh
GET /sessions/ses_abc123/messages?lang=ja
Supported languages: en, zh, ja, ko, es, fr, de, it, pt, ru, id, th, vi
Privacy and Data
Jenova does not use API prompts, outputs, conversation history, uploaded files, agent instructions, or knowledge bases to train Jenova models.
For third-party model providers, Jenova uses commercial API channels, account settings, contractual commitments, or opt-outs intended to prevent customer content from being used to train provider models.
Jenova stores and processes API data using U.S. infrastructure. Third-party providers may process data in other jurisdictions as described in the Privacy Policy and Terms of Use.
For full details, see the Terms of Use, Privacy Policy, and Usage Policy.
Support
- Dashboard: www.jenova.ai/platform
- Email: [email protected]