5 minute setup

Quickstart

Get your first inference call running in under 5 minutes. Create an account, add credits, generate a key, and fire your first request.

1
Create an account and add credits

Sign up, go to Wallet, and add at least ₹10. Credits are deducted per token used. Minimum top-up is ₹10 (1000 paise). Supports UPI, RuPay, and cards via Razorpay.

2
Generate an API key

Go to API Keys and click "Create new key". Copy it immediately, it is shown only once. Keys are in the format key_<tag>_<YYYYMMDD>_<32 hex chars> and stored as SHA-256 hashes, the raw key is never persisted.

3
Make your first call

Send a request to POST /gateway/v1/inference/run with your key in the Authorization header.

import httpx

response = httpx.post(
    "https://velona.in/gateway/v1/inference/run",
    headers={"Authorization": "Bearer <your-key>"},
    json={
        "model": "openai/gpt-4o-mini",
        "turns": [{"role": "user", "content": "Hello, world!"}],
    }
)
print(response.json()["data"]["output"])
Your wallet is charged automatically based on tokens used. Check your real-time balance at GET /gateway/v1/account/credits or in the dashboard. All amounts are in INR (paise internally).
A call without streaming must finish within 90 seconds, so for long answers, reasoning models or agents add "stream": true; see Long-running requests.

API reference

All endpoints accept JSON and return JSON. Authentication is via Bearer token in the Authorization header. Velona exposes two base URLs: the native surface below, and an OpenAI-compatible surface at /v1 for use with the OpenAI SDK and tools built on it.

Base URL, native (full feature set)
https://velona.in/gateway/v1
Base URL, OpenAI-compatible
https://velona.in/v1

The /v1 surface supports chat/completions (including streaming and tool calling) and models. Memory sessions, embeddings, image rendering, text to speech and batches are available only on the native surface above.

On both surfaces a request without stream must finish within 90 seconds. For long generations, reasoning_effort: "high" or tool-calling agents on /v1, pass stream: true (stream=True in the OpenAI SDK); see Long-running requests.

POST /gateway/v1/inference/run Text generation: chat, completion, instruction following.
▾
POST /gateway/v1/inference/embed Generate vector embeddings from text inputs.
▾
POST /gateway/v1/images/render Generate images from a text prompt.
▾
GET /gateway/v1/models List all available models with pricing and capabilities.
▾
GET /gateway/v1/account/credits Read-only wallet balance in USD and INR.
▾

Authentication

All gateway endpoints require a Bearer token. Keys are created and managed via the API Keys dashboard. There is no API for key creation.

How to authenticate

All requests must include an Authorization header:

Authorization: Bearer sk-<your key>

Key format & security

Keys are generated in the format sk-<48 lowercase hex chars>. The raw key is shown exactly once at creation and never stored, only its SHA-256 hash is persisted. Revoke and regenerate from the API Keys page if compromised.

Key attributes

Each key can be configured with optional constraints:

AttributeDescription
nameHuman-readable label (e.g. "Production Backend")
tagsEnvironment tags from allowlist: production, staging, dev, beta, etc.
budget_paiseOptional spending cap. Requests rejected with HTTP 402 once spent >= budget.
expires_atOptional ISO date after which the key is automatically invalid. You receive an email alert 3 days before.

OpenAI SDK compatibility

Point any OpenAI-compatible client at https://velona.in/v1 with your Velona key. Works unmodified with the OpenAI Python and Node SDKs, Codex CLI, CrewAI, Cline, Continue, LangChain and LlamaIndex, streaming and tool/function calling included.

from openai import OpenAI

client = OpenAI(
    api_key="<your-velona-key>",
    base_url="https://velona.in/v1",
)

completion = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "Hello!"}],
)
print(completion.choices[0].message.content)

What /v1 supports

The OpenAI-compatible surface is deliberately minimal, raw paid inference and a model list. Velona-specific capabilities live on the native /gateway/v1 surface.

Capability/gateway/v1/v1
Paid model inferenceYesYes
Streaming (SSE)YesYes
Tool / function callingYesYes
Model listYesYes
Memory sessionsYesNo
EmbeddingsYesNo
Image renderingYesNo
Text to speech (audio output)YesNo
Audio input in chat messagesYesYes
Batch processingYesNo
INR billing, usage logs, rate limitsYesYes (identical)

Endpoints on /v1: POST /v1/chat/completions , GET /v1/models and GET /v1/models/{id}. Every other /v1 path returns a 404 in the OpenAI error format. For the common ones, the message names the native endpoint to use instead: /v1/embeddings → POST /gateway/v1/inference/embed, /v1/images/generations → POST /gateway/v1/images/render, /v1/audio/speech → POST /gateway/v1/audio/speech, and /v1/batches → /gateway/v1/batches. Speech-to-text (/v1/audio/transcriptions, /v1/audio/translations) and /v1/responses are not offered on either surface. Audio input works on both: send it as a content part of a chat message to a model whose input modalities include audio.

Errors follow the OpenAI envelope ({"error":{"message","type","code"}}), so the SDK raises its usual typed exceptions, AuthenticationError, RateLimitError, BadRequestError, NotFoundError.

Models

The gateway routes to 500+ models across leading AI providers. Use the model ID in the format provider/model-name. All pricing is displayed in INR.

Popular models

Model IDProviderTypeContext
openai/gpt-4oOpenAItext128k
openai/gpt-4o-miniOpenAItext128k
anthropic/claude-sonnet-4-6Anthropictext200k
anthropic/claude-opus-4-8Anthropictext200k
google/gemini-2.0-flashGoogletext1M
meta-llama/llama-3.1-70b-instructMetatext128k
mistralai/mistral-7b-instructMistraltext32k
dall-e-3OpenAIimage--
openai/text-embedding-3-smallOpenAIembed--
For the full live model list with INR pricing, call GET /gateway/v1/models or visit the Models page. Pricing data is cached for 2 hours and includes platform markup.

INR pricing formula

All costs are calculated transparently:

cost_paise = max(1, int(cost_usd × buffered_forex_rate × (1 + MARKUP_PERCENT/100) × 100))

# Where:
#   buffered_forex_rate = live USD>INR rate × (1 + FOREX_BUFFER_PERCENT/100)
#   MARKUP_PERCENT      = platform markup (visible on /pricing)
#   Minimum charge      = 1 paise (never zero)

For streaming requests, a provisional debit is made upfront (your prompt plus max_tokens, 1,000 output tokens when unset) and reconciled to the actual token count after the stream completes; see Long-running requests.

Streaming

Set stream: true in your request to receive responses as Server-Sent Events (SSE). Each chunk is a JSON object on a data: line.

import httpx

with httpx.stream(
    "POST",
    "https://velona.in/gateway/v1/inference/run",
    headers={"Authorization": "Bearer <your-key>"},
    json={
        "model": "openai/gpt-4o-mini",
        "turns": [{"role": "user", "content": "Count to 5."}],
        "stream": True,
    }
) as r:
    for line in r.iter_lines():
        if line.startswith("data:"):
            print(line[5:].strip())
SSE stream format
data: {"request_id":"req_abc","event":"delta","data":{"run_id":"run_xyz","delta":"1"}}
data: {"request_id":"req_abc","event":"delta","data":{"run_id":"run_xyz","delta":", 2"}}
data: {"request_id":"req_abc","event":"done","data":{"finish":"stop","usage":{...},"billed_usd":0.000012}}
data: [DONE]
Three event types: delta (incremental token), done (final usage + billing), error (if something goes wrong mid-stream). done.finish is the value a non-streaming call returns in data.finish: the model’s own reason, such as stop, or length when max_tokens cut the reply short; complete when the model gave none, and error after an error event. Lines that start with : are : keep-alive comments sent while the model is thinking; skip them (see Long-running requests).

Streaming billing

For streaming requests the gateway makes a provisional debit upfront, priced for your prompt plus max_tokens (1,000 output tokens when you do not set it) and capped at your current balance. After the stream completes, actual token counts are used to calculate the real cost. The difference is refunded or charged in a reconciliation step. Deterministic idempotency keys (prov-<id>, reconcile-refund-<id>) prevent any double-billing on retries.

Long-running requests

A non-streaming request sends nothing until the whole answer is ready, so it has to finish within 90 seconds. One that has not finished by then ends with 503 UPSTREAM_ERROR and is not charged. A streaming request has no limit on its total length: it sends output as the model produces it, so it can run for as long as the model keeps working. Use stream: true for anything that may take longer than about a minute.

RequestUse stream: true because
Long generationsA large max_tokens (or max_completion_tokens): long documents, code, translations
Reasoning modelsreasoning_effort: "high" on /v1: the model can think for minutes before its first word
Tool-calling agentsLarge tool definitions and long conversations make each turn slower; stream every turn

While the model is thinking

A reasoning model may send its first chunk and then nothing for several minutes. During that silence Velona sends an SSE comment line, : keep-alive, every 15 seconds, so the connection never looks idle to anything between you and us. Comment lines start with : and carry no data: ignore every line that does not start with data:. The OpenAI SDKs already do, and so does every example on this page. A comment changes nothing else: the events, the usage, the closing [DONE] and the bill are the same with or without them.

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

: keep-alive

: keep-alive

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"There are infinitely"},"finish_reason":null}]}

data: [DONE]

The only time limit on a stream is silence from the model: if it sends nothing at all for 300 seconds, the stream ends with an error event whose code is UPSTREAM_ERROR and whose message is The model stopped responding for 300 seconds. Please retry. On the native surface that is an "event": "error" frame followed by done with finish: "error"; on /v1 it is a data: {"error": {..., "code": "upstream_error"}} frame, which the OpenAI SDKs raise as an API error. Both end with data: [DONE].

Set your client's timeout

Use a read timeout (the longest wait for the next bytes), not a deadline for the whole request. Because something arrives at least every 15 seconds, any read timeout comfortably above that works; the examples use 60 seconds for raw HTTP and 600 for the OpenAI SDKs. Do not put a total-time limit on a stream, such as AbortSignal.timeout() in JavaScript or --max-time in cURL: it ends a long generation that is working normally.

from openai import OpenAI

client = OpenAI(
    api_key="<your-velona-key>",
    base_url="https://velona.in/v1",
    # Per read, not per request: the longest wait for the next bytes.
    timeout=600.0,
)

stream = client.chat.completions.create(
    model="openai/gpt-5-nano",
    messages=[{"role": "user", "content": "Prove there are infinitely many primes, three ways."}],
    reasoning_effort="high",
    max_completion_tokens=16000,
    stream=True,
    stream_options={"include_usage": True},   # adds a final usage chunk
)

for chunk in stream:
    if chunk.usage:
        print("\nusage:", chunk.usage)
    for choice in chunk.choices:
        if choice.delta.content:
            print(choice.delta.content, end="", flush=True)
        if choice.finish_reason == "length":
            print("\n[cut off by max_completion_tokens]")

Streaming tool calls

An agent turn that calls tools streams its calls in pieces: each delta.tool_calls entry carries an index, the first piece for an index carries its id and function name, and the arguments JSON arrives as fragments to join in order. Parse the arguments only after the turn ends with finish_reason: "tool_calls".

import json
from openai import OpenAI

client = OpenAI(api_key="<your-velona-key>", base_url="https://velona.in/v1", timeout=600.0)

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

stream = client.chat.completions.create(
    model="openai/gpt-5-nano",
    messages=[{"role": "user", "content": "What is the weather in Mumbai and in Pune?"}],
    tools=tools,
    stream=True,
)

calls = {}      # index -> {"id", "name", "arguments"}
text = ""
finish = None
for chunk in stream:
    for choice in chunk.choices:
        delta = choice.delta
        if delta.content:
            text += delta.content
        for tc in delta.tool_calls or []:
            call = calls.setdefault(tc.index, {"id": None, "name": None, "arguments": ""})
            if tc.id:
                call["id"] = tc.id
            if tc.function and tc.function.name:
                call["name"] = tc.function.name
            if tc.function and tc.function.arguments:
                call["arguments"] += tc.function.arguments
        finish = choice.finish_reason or finish

if finish == "tool_calls":
    for index in sorted(calls):
        call = calls[index]
        args = json.loads(call["arguments"] or "{}")
        print(call["id"], call["name"], args)
        # Run the tool, append {"role": "tool", "tool_call_id": call["id"], ...}
        # to messages, and stream the next turn the same way.
else:
    print(text)

How a long stream is billed

WhenWhat happens to your wallet
The stream opensA provisional hold is taken, priced for your prompt plus max_tokens (1,000 output tokens when unset) and capped at your balance
While it runsIt may produce more than the hold covers, up to what your wallet can pay. If the cost reaches your balance, the stream ends with an INSUFFICIENT_CREDITS error event (insufficient_quota on /v1)
It finishesBilled on the tokens the model reports; the difference from the hold is refunded or charged. The native done event shows usage and billed_usd; on /v1, ask for stream_options: {"include_usage": true}
It fails before the model respondsNot charged: the hold is refunded in full
It fails part-way, or you disconnectBilled for the prompt and the output generated up to that point; the rest of the hold is refunded. Closing the connection stops the stream

Batch

Send many requests in one call and collect the results later. Batch runs asynchronously with a 24-hour completion window and is billed at the model's published batch rate. For most models that is about half the per-token price of the same requests made individually, but not for every model — on a few the batch input rate is higher — so compare the two before batching prompt-heavy work.

When to use it

Batch suits work that does not need an answer right now: classifying a backlog, generating descriptions for a catalog, scoring a dataset. You submit, you get an id, and you poll until it finishes. If you need a response in the same request, use inference instead.

Which models support it

Only models published with a :batch id. Filter the Models page by Batch support to see the current list; in GET /gateway/v1/models those models carry a batch capability. Submit the base id; a :batch id is accepted here too, and only here. A model without a batch variant, or one the batch service has declined, is refused immediately, before anything is charged.

Submit a batch

POST /gateway/v1/batches
Authorization: Bearer YOUR_API_KEY

{
  "endpoint": "/v1/chat/completions",
  "model": "anthropic/claude-haiku-5.5",
  "requests": [
    { "custom_id": "row-1",
      "body": { "messages": [{"role": "user", "content": "Summarise: ..."}],
                "max_tokens": 200 } },
    { "custom_id": "row-2",
      "body": { "messages": [{"role": "user", "content": "Summarise: ..."}],
                "max_tokens": 200 } }
  ]
}

Pass the ordinary model id — the batch variant is resolved for you. endpoint is /v1/chat/completions or /v1/embeddings.

FieldRequiredNotes
endpointYesChat completions or embeddings
modelYesMust have a batch variant
custom_idYesUnique within the batch — results are keyed on it
bodyYesThe same body you would send to that endpoint

Collect the results

GET    /gateway/v1/batches/BATCH_ID   # one batch, with results once finished
GET    /gateway/v1/batches            # your batches, newest first
DELETE /gateway/v1/batches/BATCH_ID   # delete a finished batch and its results

Status moves through validating → in_progress → completed. Results appear once it finishes, each carrying the custom_id you supplied. You can only see your own batches.

Billing

An estimate is held when you submit, and settled against actual usage when the batch finishes — one charge for the whole batch, not one per request. The hold covers each request’s max_tokens (or max_completion_tokens) times n; a chat request that sets neither is held at the model’s maximum output, so set max_tokens to keep the hold small. The batch must fit your balance and your API key’s budget. Settlement happens automatically when the batch finishes, whether or not you poll, and polling never charges again. A batch that fails before running releases the hold in full. A running batch cannot be stopped; delete it once it has finished.

Limits

Up to 10,000 requests per batch. Text only — send requests with images, files or audio individually. 24-hour completion window. Results are available for 30 days.

Image generation

Generate images from a text prompt. Billed on the tokens the render uses, as the model reports them.

Request

POST /gateway/v1/images/render
Authorization: Bearer YOUR_API_KEY

{
  "model": "openai/gpt-5-image-mini",
  "prompt": "A red circle on a white background",
  "config": { "size": "1024x1024", "quality": "high", "count": 1 }
}
FieldDefaultNotes
model—Required. Filter Models by Image
prompt—Required
config.sizemodel defaulte.g. 1024x1024
config.qualityautoauto | low | medium | high | xhigh | max
config.count11 to 10. Cost scales with this

Response

{
  "request_id": "req_...",
  "status": "success",
  "data": {
    "model": "openai/gpt-5-image-mini",
    "images": [ {
      "index": 0,
      "url": "data:image/png;base64,iVBORw0KGgo...",
      "b64_json": "iVBORw0KGgo...",
      "media_type": "image/png",
      "expires_at": null
    } ]
  },
  "meta": { "latency_ms": 24836, "billed_usd": 0.000008 }
}

url is always something you can load directly — a hosted link where one is available, otherwise a data: URI you can drop straight into an <img src>. The raw base64 is also given as b64_json if you would rather decode the bytes yourself. expires_at is null for inline images, which do not expire.

Pricing

Billed on the tokens the render uses, as the model reports them: your prompt at the model's input rate, and its output — the image tokens, plus any text or reasoning it produces alongside — at the model's image-output rate. An estimate for count images is reserved from your wallet before the render and settled to the reported usage afterwards; the difference is refunded or charged, and a render that fails is refunded in full. A render is returned in one piece rather than streamed, so it must finish within 90 seconds, like any non-streaming request; one that does not ends with 503 UPSTREAM_ERROR and is refunded. A model with no published image-output rate is refused with a 404 rather than served — so you are never charged an unknown amount, and never given something free that later starts costing.

Embeddings

Turn text into vectors for search, clustering and retrieval. Billed per input token.

Request

POST /gateway/v1/inference/embed
Authorization: Bearer YOUR_API_KEY

{
  "model": "text-embedding-3-small",
  "inputs": ["first document", "second document"]
}

inputs takes a single string or an array. Filter the Models page by Embeddings for the current list — dimensions differ by model, from 1536 up to 3072.

Response

{
  "request_id": "req_...",
  "status": "success",
  "data": {
    "model": "text-embedding-3-small",
    "vectors": [ { "index": 0, "embedding": [0.012, -0.034] } ],
    "dimensions": 1536,
    "usage": { "total_tokens": 7 }
  }
}

Notes

Vectors come back in the order you sent them, so index matches your input position. Embedding models produce no completion tokens, so you are charged on input only.

Audio

Turn text into speech. Billed per 1,000 characters of input and instructions, with the audio returned inline.

Text to speech

POST /gateway/v1/audio/speech
Authorization: Bearer YOUR_API_KEY

{
  "model": "gpt-4o-mini-tts",
  "input": "Hello from Velona.",
  "voice": "alloy"
}
FieldDefaultNotes
model—Required. Filter Models by Audio / Voice
input—Required. Up to 10,000 characters
voicethe model’s ownEach model has its own voices. Its card in GET /gateway/v1/models lists default_voice (sent when you omit this field) and, where the set is fixed, voices
response_formatmodel defaulte.g. mp3, wav, opus
speed1.0Model dependent
instructions—Optional voice direction. Up to 1,000 characters, billed as input

Response

{
  "request_id": "req_...",
  "status": "success",
  "data": {
    "model": "gpt-4o-mini-tts",
    "audio": {
      "url": "data:audio/mpeg;base64,//uQx...",
      "b64_json": "//uQx...",
      "media_type": "audio/mpeg",
      "bytes": 40320
    },
    "characters": 18,
    "billed_characters": 18
  },
  "meta": { "latency_ms": 2180, "billed_usd": 0.0000108 }
}

The audio comes back inline: url is a data: URI you can put straight into an <audio src>, and b64_json is the same bytes if you would rather decode them yourself. Returning it this way rather than as a raw audio stream is what lets the response also carry the request id, the billed amount and the character count.

Pricing

Billed per 1,000 characters of input: the characters of input plus any instructions, which the model reads too (billed_characters in the response). The charge is priced from your request and taken before synthesis — so the cost is knowable from your own request — and refunded in full if synthesis fails. Rates vary widely by model: see the Models page filtered by Audio / Voice. A model with no published rate is refused with a 404 rather than synthesised.

Availability

Speech models are newer than the text catalog and some are intermittently unavailable at source. A model that cannot be reached, or does not finish within 90 seconds, returns a 503 with code UPSTREAM_ERROR and is not charged: the amount reserved for it is refunded. Speech is returned in one piece rather than streamed, so this limit applies to every speech call (unlike text generation, where stream: true lifts it): split long text across several calls. If one fails, try another from the Audio / Voice list. A model that cannot currently synthesise at all is not listed, and is refused with a 404 MODEL_NOT_AVAILABLE before anything is reserved.

Voices

Voice names are specific to each model: a voice one model accepts is usually unknown to another. Leave voice out to use the model’s default, or pick one from the model’s voices list. A voice the model does not support returns 400 INVALID_REQUEST with a message naming the voice and the model, such as voice 'alloy' is not supported by model 'aura-2', followed by the supported voices where they are known. It is not charged.

Models that accept audio input

Some chat models take audio as an input modality alongside text. Those are callable today through the ordinary inference endpoint — the audio goes in the message content and is billed as part of the request.

Memory sessions

Memory sessions let you persist conversation context across multiple API calls and browser sessions. Attach a mem_ session ID to any inference call and the gateway automatically injects the stored context and appends the new exchange.

How it works

Without memory every API call is stateless, your application must send full conversation history each time. With a memory session, the gateway maintains the history server-side. You only send the latest user message, and the context is injected automatically as a system message before your turns.

Without memory

You manage full history in your app. Every call includes all prior turns. No server-side state. Ephemeral, wiped on page refresh or process restart.

With memory session

Gateway stores & compresses context. Send only the new message. Context survives page refreshes, device switches, and process restarts. Shared across the Chat UI and API.

Step 1: Create a session

Create a session from the Memory page or via the API. Choose a size tier that fits your use case:

TierMax charsBest for
Small50,000Short task chains, simple Q&A bots
Medium (default)150,000Multi-turn assistants, coding helpers
Large500,000Long research sessions, document analysis
Custom1K – 500KPrecise control over context budget
import httpx

# Create a memory session (uses your JWT, not an API key)
response = httpx.post(
    "https://velona.in/memory/sessions",
    headers={"Authorization": "Bearer <your-jwt>"},
    json={
        "name": "My Project Assistant",
        "max_chars": 150000   # Medium tier
    }
)
session = response.json()
memory_id = session["memory_id"]  # e.g. "mem_4a7b2c3d1e9f0a8b"
print(f"Session created: {memory_id}")

Step 2: Attach to an inference call

Pass the memory_id in the velona extension field of any /gateway/v1/inference/run call:

import httpx

MEMORY_ID = "mem_4a7b2c3d1e9f0a8b"

# First turn
r1 = httpx.post(
    "https://velona.in/gateway/v1/inference/run",
    headers={"Authorization": "Bearer <your-key>"},
    json={
        "model": "openai/gpt-4o-mini",
        "turns": [{"role": "user", "content": "My name is Rohan. Remember that."}],
        "velona": {"memory_session": MEMORY_ID},
    }
)
print(r1.json()["data"]["output"])

# Second turn, context from first turn is injected automatically
r2 = httpx.post(
    "https://velona.in/gateway/v1/inference/run",
    headers={"Authorization": "Bearer <your-key>"},
    json={
        "model": "openai/gpt-4o-mini",
        "turns": [{"role": "user", "content": "What's my name?"}],
        "velona": {"memory_session": MEMORY_ID},
    }
)
print(r2.json()["data"]["output"])  # "Your name is Rohan."

Session management endpoints

GET /memory/sessions List all your memory sessions with usage stats
GET /memory/sessions/{memory_id} Session detail + last 3 exchanges preview
POST /memory/sessions/{memory_id}/clear Wipe stored context but keep the session ID active
DELETE /memory/sessions/{memory_id} Soft-delete session (sets inactive, ID no longer usable)

Context compression

When a session's stored context exceeds its max_chars limit, the oldest exchanges are pruned automatically. Compression runs in the background after each append, it never adds latency to your API call. The compressed context is pre-computed and stored, and get_context_for_model() is synchronous and sub-millisecond on the hot path.

Clear vs Delete: Clear wipes all stored messages but keeps the mem_ ID usable, future calls start fresh. Delete soft-deletes the session, the ID can no longer be used in inference calls.

Using memory in the Chat UI

The Velona Chat page has a built-in memory selector. Pick a session from the dropdown to attach it. The chat UI shows a "Context active" indicator and the session's usage percentage. You can also deep-link directly: /chat?memory=mem_abc123

Deployment

Ship a site or an application from the dashboard, from a zip, or straight from a GitHub repository.

The two kinds of application

Velona hosts two things, and which one you get is decided by what is at the top level of your upload.

KindDetected byWhat it is
Static site index.html Your files, served exactly as uploaded. Always on, free, no cold start. No server-side code runs.
Python application requirements.txt Your app, started for you and reachable over HTTPS. Sleeps when idle and wakes on the next request.

If both are present the project is treated as a Python application. If neither is present the deploy is refused.

Packaging

  • Files must be at the top level of the archive, not inside a wrapper folder. Zip from inside the project directory.
  • A Python app needs app.py exposing a module-level app, or an entrypoint in velona.json.
  • Do not call app.run(), bind a port, or add a web server to requirements.txt, Velona starts your app for you.
  • requirements.txt takes plain name==version lines only. No index flags, URLs, or git+ references.
  • Archives are limited to 50 MB and 5,000 files. A build has three minutes to finish.

The runtime contract

  • The filesystem is read only except /data, which persists across deploys, and /tmp, which does not. Write everything under os.environ["DATA_DIR"].
  • Your app is given PORT, DATA_DIR, VELONA_APP_ID, and VELONA_API_KEY plus VELONA_API_BASE for calling the Velona AI gateway from inside your app.
  • Outbound requests must be HTTPS. Plain http://, direct database ports, and SMTP are not available, use your provider's HTTPS API. These fail as a timeout rather than an error, so check this first if a request hangs.
  • Default resources are 256 MB of memory and a quarter of a CPU, adjustable per application.

Two failures that look like success

Both of these produce a green build and a site that does not work, so they are worth knowing before you hit them.

  • Uploading a front-end project instead of its build output. A React or Vue project usually has an index.html at the top level, so it is accepted as a static site and published - but the browser is handed unbuilt source and renders nothing. Build locally and upload the contents of dist/.
  • A stray requirements.txt in a static site. It will be treated as a Python application and refused for a missing entry point. Remove it, or add the entry point you meant.

Deploying from GitHub

Connect a repository from an application's Deployments tab: choose the account, the repository and the branch, then deploy on demand or turn on automatic deploys so every push to that branch ships. Velona needs read-only access to contents and metadata, and nothing else.

Automatic deploys are off until you switch them on, because each one costs a build.

Rollback and safety

  • A failed build never takes your running application offline, the previous version keeps serving.
  • Any earlier deployment can be restored from the deployment history.
  • Uploaded code is reviewed automatically before it is built. This protects the platform; it is not a security audit of your application, and passing it is not a statement that your code is safe.
  • Everything you deploy is public. Do not include credentials, keys, or data you would not publish.

velona.json

Optional, at the top level, only when the defaults do not fit.

{
  "entrypoint": "main.py:application",
  "python": "3.12"
}

entrypoint defaults to app.py:app. python must be 3.12.

Error handling

All errors use a consistent envelope regardless of endpoint. Use the error.code field for programmatic handling.

Error envelope
{
  "request_id": "req_8f3a1c92b4e7",
  "status": "error",
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Balance ₹0.02. Top up from ₹10 at https://velona.in/wallet",
    "docs": "https://velona.in/docs/errors#INSUFFICIENT_CREDITS"
  },
  "meta": {
    "timestamp": "2026-05-14T10:00:00Z"
  }
}
Error codes
401INVALID_KEYAPI key missing, malformed, expired, or deactivated
403ACCOUNT_SUSPENDEDThe key is valid but the account that owns it is suspended or closed (account_suspended on /v1). Rotating the key does not help; contact support
402INSUFFICIENT_CREDITSWallet balance too low, top up at Wallet
400INVALID_REQUESTMissing or malformed required fields in the request body
404MODEL_NOT_AVAILABLEThe model ID is unknown or not offered: not on the Models page, a router or alias ID, or a :batch ID sent outside /gateway/v1/batches. Nothing is charged
422MODEL_NOT_FOUNDA listed model is not available right now. Try another model or retry later. Nothing is charged
429RATE_LIMIT_EXCEEDEDToo many requests, see Retry-After header
503UPSTREAM_ERRORThe model failed, timed out or could not be reached. A failed call is not charged. A non-streaming call that has not finished within 90 seconds ends with this error. Retry after the Retry-After header; for long generations use stream=true (see Long-running requests). In a stream it arrives as an error event, for example when the model sends nothing for 300 seconds
503SERVICE_UNAVAILABLEVelona cannot serve this request right now; your wallet is not affected, retry shortly
500INTERNAL_ERRORUnexpected server-side failure, contact support
import httpx

response = httpx.post(
    "https://velona.in/gateway/v1/inference/run",
    headers={"Authorization": "Bearer <your-key>"},
    json={"model": "openai/gpt-4o-mini", "turns": [{"role": "user", "content": "Hi"}]}
)

body = response.json()
if body["status"] == "error":
    code = body["error"]["code"]
    if code == "INSUFFICIENT_CREDITS":
        print("Top up your wallet at /wallet")
    elif code == "INVALID_KEY":
        print("Check your API key")
    elif code == "RATE_LIMIT_EXCEEDED":
        print("Slow down, retry after", response.headers.get("Retry-After"), "seconds")
    elif code in ("UPSTREAM_ERROR", "SERVICE_UNAVAILABLE"):
        print("Provider issue, retry with exponential backoff")
    else:
        print(f"Error: {code} -- {body['error']['message']}")
else:
    print(body["data"]["output"])

Rate limits

Velona enforces multiple layers of rate limiting to protect platform stability and prevent abuse. The values below are the defaults. All limits return HTTP 429 with a Retry-After header.

Limit typeDefaultWindowApplies to
API requests100 / minper API key/gateway/v1/* and /v1/*. A request without a valid key is counted against its IP address
API key creation10 keysper hour, per accountCreating keys or regenerating the Developer Tools key

Retry strategy

The gateway never re-sends a request that a model may already have started on, so you are never billed twice for one call. When a model fails or times out it returns HTTP 503 with code UPSTREAM_ERROR (or SERVICE_UNAVAILABLE) and a Retry-After header. For 429 and 503 responses, wait the number of seconds in Retry-After, then retry:

import httpx, time

def call_with_retry(payload, max_retries=3):
    for attempt in range(max_retries):
        response = httpx.post(
            "https://velona.in/gateway/v1/inference/run",
            headers={"Authorization": "Bearer <your-key>"},
            json=payload,
        )
        if response.status_code in (429, 503):
            wait = int(response.headers.get("Retry-After", 10))
            time.sleep(wait)
            continue
        return response.json()
    raise Exception("Max retries exceeded")
Rate limits are enforced per IP in development (single worker). In production with Redis enabled, limits are shared across all workers. The circuit breaker opens after 5 consecutive upstream failures and resets after 60 seconds.

Developer tools

Free utilities (hashing, encoding, formatting, DNS/SSL/network inspection, crypto, QR codes), wallet-free, every tool reachable from your own application.

Calling the tools API

POST /tools/v1/<tool-name>, every tool in the fleet is exposed here (see table below). Authenticate with the free Developer Tools API Key auto-issued to every account, visible any time on the API Keys page, that key is gated by exactly one check (your account exists and is active), never touches your wallet, and never expires. A regular billable API key also works here. Accepts a single object or a bulk {"items": [...]} body (capped at TOOLS_BULK_MAX_ITEMS, default 1000), bulk responses come back as {"results": [...]}. Rate limits count each item, per account, whether you call with a key or from the signed-in tool pages: 1,000 items a minute across all tools, and 60 an hour for the tools that connect to a host you name or to third-party servers (ssl-cert, http-headers, security-headers, og-scraper, robots, whois, domain-available, speedtest). A dns-propagation item counts once per resolver it asks (30), and a speedtest run counts 10. Over the limit the whole request gets a 429 with a Retry-After header and no item is run. Network tools reach public addresses only, and ssl-cert connects on port 443.

The free Developer Tools API Key (prefix dk_) only ever works against /tools/v1/*, it is rejected by /gateway/v1/* (billed AI inference), so it can never be used to run up wallet charges even if leaked. It is shown once, when you generate it on API Keys: Velona keeps only a hash of it. If you lose it, or think it has leaked, click Regenerate there; the old key stops working at once.
# Single
curl -X POST https://velona.in/tools/v1/hash \
  -H "Authorization: Bearer <your-dev-tools-key>" \
  -H "Content-Type: application/json" \
  -d '{"data": "hello"}'

# Bulk
curl -X POST https://velona.in/tools/v1/hash \
  -H "Authorization: Bearer <your-dev-tools-key>" \
  -H "Content-Type: application/json" \
  -d '{"items": [{"data": "a"}, {"data": "b", "algorithm": "md5"}]}'

#  is the "dk_..." key from /keys, free, no wallet, never expires.
# It is shown once, when generated; Regenerate on /keys issues a new one.
# A regular billable API key ("key_..." / "sk-...") works here too.
Format+AI tools accept an ai_enhance field, but AI enhancement isn't available through /tools/v1/*, the field is ignored and you always get the base (non-AI) result.

All tools

ToolCategoryEndpointRequired fields
Loading tools…
Network-category tools can be disabled platform-wide by the admin independently of the rest, if you get a 404 on one of them, check /status.

SDKs & examples

Velona's gateway is fully OpenAI-compatible. Any library built for the OpenAI API works by changing two lines. No new SDK needed.

LangChain

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="openai/gpt-4o-mini",
    openai_api_key="<your-velona-key>",
    openai_api_base="https://velona.in/v1",
)

response = llm.invoke("Explain transformers in one paragraph.")
print(response.content)

LlamaIndex

from llama_index.llms.openai import OpenAI

llm = OpenAI(
    model="openai/gpt-4o-mini",
    api_key="<your-velona-key>",
    api_base="https://velona.in/v1",
)

response = llm.complete("What is RAG?")
print(response.text)

Streaming with OpenAI SDK

from openai import OpenAI

client = OpenAI(
    api_key="<your-velona-key>",
    base_url="https://velona.in/v1",
)

stream = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    messages=[{"role": "user", "content": "Tell me a short story."}],
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta.content if chunk.choices else None
    if delta:
        print(delta, end="", flush=True)

Environment setup

Store your key in an environment variable, never hardcode it:

# .env
VELONA_API_KEY=key_<tag>_<date>_<32-hex>
VELONA_BASE_URL=https://velona.in/v1

Quick reference

ResourceLink
Full model list + pricing/models
Live INR pricing calculator/pricing
Model benchmarks & leaderboard/ai-analysis
Side-by-side model evaluator/evaluator
Memory session manager/memory
Platform status/status
Need help? Check the Status page for live health checks, or review your recent calls in Usage & Logs. Wallet alerts and anomaly notifications are delivered to your in-app notification bell and email.