Quickstart
Get your first inference call running in under 5 minutes. Create an account, add credits, generate a key, and fire your first request.
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.
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.
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"])
GET /gateway/v1/account/credits or in the dashboard. All amounts are in INR (paise internally).
"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.
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.
/gateway/v1/inference/run
Text generation: chat, completion, instruction following.
/gateway/v1/inference/embed
Generate vector embeddings from text inputs.
/gateway/v1/images/render
Generate images from a text prompt.
/gateway/v1/models
List all available models with pricing and capabilities.
/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
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:
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.
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
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())
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]
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.
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
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.
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.
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 }
}
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"
}
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.
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.
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.
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:
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
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.
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.
| Kind | Detected by | What 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.pyexposing a module-levelapp, or anentrypointinvelona.json. - Do not call
app.run(), bind a port, or add a web server torequirements.txt, Velona starts your app for you. requirements.txttakes plainname==versionlines only. No index flags, URLs, orgit+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 underos.environ["DATA_DIR"]. - Your app is given
PORT,DATA_DIR,VELONA_APP_ID, andVELONA_API_KEYplusVELONA_API_BASEfor 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.htmlat 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 ofdist/. -
A stray
requirements.txtin 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.
{
"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"
}
}
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.
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")
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.
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.
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
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