curl and raw HTTP: use the uttapen API without an SDK
Call every uttapen endpoint with curl: chat, streaming with -N, cost and rate-limit headers, uttapen.meta, embeddings, models, usage and a deploy check.
Updated: September 7, 2026
Any language that can do HTTP and JSON works with uttapen. This page shows every endpoint as a curl command — for Bash, for CI scripts, or for when you want to see exactly what goes over the wire.
Put the key in your environment once:
export UTTAPEN_API_KEY=sk-up-...
export UTTAPEN=https://api.uttapen.ir/v1
Chat (non-streaming)
curl -s -i "$UTTAPEN/chat/completions" \
-H "Authorization: Bearer $UTTAPEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-5-mini",
"messages": [{"role": "user", "content": "Name three advantages of Postgres in three lines."}],
"max_tokens": 200
}'
-i prints the headers too:
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1788734238
X-Uttapen-Balance-Toman: 97564.413809
X-Uttapen-Cost-Toman: 6.659688
X-Uttapen-Model: openai/gpt-5-mini
X-Uttapen-Request-Id: 01a078dd-853e-7480-aa83-262d3239e6a2
Just the answer text, with jq:
curl -s "$UTTAPEN/chat/completions" -H "Authorization: Bearer $UTTAPEN_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"openai/gpt-5-nano","messages":[{"role":"user","content":"Hello"}]}' \
| jq -r '.choices[0].message.content'
X-API-Key: $UTTAPEN_API_KEY is accepted as an alternative to Authorization.
Streaming
curl -s -N "$UTTAPEN/chat/completions" \
-H "Authorization: Bearer $UTTAPEN_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Uttapen-Include-Meta: 1" \
-d '{"model":"openai/gpt-5-mini","messages":[{"role":"user","content":"Write a short poem."}],"stream":true}'
-N turns off curl's output buffering so chunks are printed the moment they arrive. The output is a series of data: lines, and the last few look like this:
data: {"id":"gen-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":24,"total_tokens":34}}
data: {"id":"gen-…","object":"uttapen.meta","cost_toman":"6.659688","balance_toman":"97557.754121","hold_toman":"100.000000"}
data: [DONE]
Live text only, with jq:
curl -s -N "$UTTAPEN/chat/completions" -H "Authorization: Bearer $UTTAPEN_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"openai/gpt-5-mini","messages":[{"role":"user","content":"Hello"}],"stream":true}' \
| sed -u 's/^data: //' | grep -v '^\[DONE\]' | grep -v '^:' | grep . \
| jq -rj '.choices[0].delta.content // empty'
Embeddings
curl -s "$UTTAPEN/embeddings" \
-H "Authorization: Bearer $UTTAPEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"openai/text-embedding-3-small","input":["lease agreement","property rental contract"]}' \
| jq '.data | length, (.[0].embedding | length)'
Tool calling
curl -s "$UTTAPEN/chat/completions" \
-H "Authorization: Bearer $UTTAPEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-5-mini",
"messages": [{"role": "user", "content": "What is the weather in Tehran?"}],
"tools": [{"type": "function", "function": {"name": "get_weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}]
}' | jq '.choices[0].message.tool_calls'
Append the result of running the function to messages as {"role":"tool","tool_call_id":"…","content":"…"} and send the whole thing again (tool calling).
Model list (no key needed)
curl -s "$UTTAPEN/models" | jq -r '.data[] | "\(.id)\t\(.uttapen_pricing.prompt_toman_per_1m)\t\(.uttapen_pricing.completion_toman_per_1m)"' | column -t
curl -s "$UTTAPEN/models/openai/gpt-5-mini" | jq '{id, context_length, input_modalities, supported_parameters, uttapen_pricing}'
Vision models:
curl -s "$UTTAPEN/models" | jq -r '.data[] | select(.input_modalities | index("image")) | .id'
Account status and usage
curl -s "$UTTAPEN/uttapen/me" -H "Authorization: Bearer $UTTAPEN_API_KEY" | jq '.wallet, .key'
curl -s "$UTTAPEN/uttapen/usage?group_by=day&from=2026-09-01" -H "Authorization: Bearer $UTTAPEN_API_KEY" \
| jq -r '.data[] | "\(.bucket)\t\(.requests)\t\(.charge_toman)"'
Errors
curl -s -i "$UTTAPEN/chat/completions" -H "Authorization: Bearer sk-up-wrong" -H "Content-Type: application/json" \
-d '{"model":"openai/gpt-5-mini","messages":[{"role":"user","content":"hi"}]}'
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
{"error":{"message":"The API key is invalid, revoked, or expired.","type":"invalid_api_key","code":"invalid_api_key","param":null}}
In a script, capture the status separately:
code=$(curl -s -o /tmp/resp.json -w '%{http_code}' "$UTTAPEN/chat/completions" -H "Authorization: Bearer $UTTAPEN_API_KEY" \
-H "Content-Type: application/json" -d @request.json)
if [ "$code" != "200" ]; then jq -r '.error.code + ": " + .error.message' /tmp/resp.json; fi
Tips
- Send the body as
@file.jsonso you don't have to fight Bash quoting; non-ASCII text inside-d '...'is fine, but a literal'in the text ends the string early. Content-Type: application/jsonis required.- Set
--max-timefor non-streaming calls (180, say); leave it off for streaming, or set it very high. - Add
-vto see request and response headers together — the key is printed in that output, so don't keep it in logs. - If you are behind a corporate proxy and the whole stream arrives at once at the end, the proxy is buffering it;
-Nhas nothing to do with that.
Batching in Bash
For a few hundred questions from a file, build the body with jq -n --arg so quotes and newlines are escaped correctly, read the cost of each answer from the header, and space the requests out to stay under the key's per-minute limit.
#!/usr/bin/env bash
set -euo pipefail
total=0
while IFS= read -r line; do
body=$(jq -n --arg q "$line" '{model:"openai/gpt-5-nano", max_tokens:120, messages:[{role:"user", content:$q}]}')
hdr=$(mktemp)
answer=$(curl -s -D "$hdr" "$UTTAPEN/chat/completions" \
-H "Authorization: Bearer $UTTAPEN_API_KEY" -H "Content-Type: application/json" \
-d "$body" | jq -r '.choices[0].message.content // .error.message')
cost=$(grep -i '^x-uttapen-cost-toman:' "$hdr" | tr -d '\r' | awk '{print $2}')
printf '%s\t%s\t%s\n' "$line" "$cost" "$answer"
rm -f "$hdr"
sleep 1 # 60 requests per minute
done < questions.txt
-D file writes the response headers to a file and -s hides the progress bar. On an error, .error.message is printed instead of the answer and the script keeps going; to stop on 402, capture the status separately with -w '%{http_code}' and check it.
HTTPie, Postman and Insomnia
With HTTPie the same request is shorter and the JSON is built for you:
http POST "$UTTAPEN/chat/completions" "Authorization:Bearer $UTTAPEN_API_KEY" \
model=openai/gpt-5-mini messages:='[{"role":"user","content":"Hello"}]' --stream
In Postman and Insomnia, set the Authorization tab to "Bearer Token" and paste the key there; set the body to "raw / JSON". Postman does not render a stream live — the whole response shows up at the end — so use curl with -N when you want to watch the chunks. Store the key in an Environment rather than inside the request itself, so exporting the collection to a colleague doesn't hand them your key.
A quick check before deploying
Three commands that tell you in seconds whether your server is ready to talk to uttapen:
curl -s -o /dev/null -w '%{http_code} %{time_total}s\n' https://api.uttapen.ir/healthz # expect 200 plus the connect time
curl -s "$UTTAPEN/uttapen/me" -H "Authorization: Bearer $UTTAPEN_API_KEY" | jq '.wallet.available_toman, .key.allowed_models'
curl -s "$UTTAPEN/chat/completions" -H "Authorization: Bearer $UTTAPEN_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"openai/gpt-5-nano","messages":[{"role":"user","content":"ok"}],"max_tokens":5}' | jq -r '.choices[0].message.content'
The first checks network and DNS, the second checks the key, the balance and the model restrictions, and the third is a real request costing a few toman. If your server goes out through a proxy, pass --proxy http://… to curl or set HTTPS_PROXY; the SDKs read the same variable. Put these three lines in your deploy script so a misconfiguration surfaces before your users find it.