uttapen

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.json so 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/json is required.
  • Set --max-time for non-streaming calls (180, say); leave it off for streaming, or set it very high.
  • Add -v to 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; -N has 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.