The whole API in one curl
Forget the SDK for a minute. Every example below sends the same thing: a JSON body to POST /v1/chat/completions with a bearer token. If you can send that, you are done. Grab a key from the sign-up page (the trial credit needs no payment details), export it as API_KEY, and run this first.
curl https://api.unrestrictedaiapi.com/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "uncensored",
"messages": [{"role": "user", "content": "Write a two-line noir opening set in a laundromat."}],
"max_tokens": 120
}'The reply is an OpenAI-style object. The text lives at choices[0].message.content, and a usage block tells you what the call cost in tokens. There is one model, uncensored, so the model field never changes. You can confirm that with GET /v1/models.
curl https://api.unrestrictedaiapi.com/v1/models -H "Authorization: Bearer $API_KEY"Two limits worth memorising before you write any code: the context window is 64,000 tokens shared between prompt and completion, and max_tokens defaults to 2048 unless you raise it (up to 16,000). If your output keeps stopping mid-sentence, that default is the usual culprit.
Python with httpx
httpx gives you a synchronous client with sane timeouts and an async twin when you need it. Set the timeout explicitly. Long generations can take a while and the default of five seconds will cut them off.
import os
import httpx
resp = httpx.post(
"https://api.unrestrictedaiapi.com/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={
"model": "uncensored",
"messages": [
{"role": "system", "content": "You are a blunt, vivid fiction writer."},
{"role": "user", "content": "Pitch a heist where the vault is a night market."},
],
"temperature": 0.9,
"max_tokens": 300,
},
timeout=60.0,
)
resp.raise_for_status()
print(resp.json()["choices"][0]["message"]["content"])raise_for_status() converts any 4xx or 5xx into an exception, which is fine for a script. For anything long-running, wrap the call so that two statuses get another chance: 429 (you hit the per-key limit of 300 requests a minute) and 503 (upstream_busy, which clears in a few seconds). Everything else, including 401, 402 and 403, will not fix itself on retry.
import os
import time
import httpx
def chat(messages, retries=3):
for attempt in range(retries + 1):
r = httpx.post(
"https://api.unrestrictedaiapi.com/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={"model": "uncensored", "messages": messages, "max_tokens": 400},
timeout=60.0,
)
if r.status_code in (429, 503) and attempt < retries:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
print(chat([{"role": "user", "content": "Name five cursed objects found in a thrift store."}]))Swap httpx.post for httpx.AsyncClient().post inside an async def and you have the async version with the same payload.
JavaScript with fetch
Node 18 and newer ship fetch, so there is nothing to install. Save the file as .mjs (or set "type": "module") so top-level await works. Browsers have the same API, but do not put the key in front-end code. Proxy the call through your own backend.
const res = await fetch("https://api.unrestrictedaiapi.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "uncensored",
messages: [{ role: "user", content: "Describe a villain's morning routine in four bullets." }],
max_tokens: 250,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`HTTP ${res.status}: ${err?.error?.message ?? "unknown error"}`);
}
const data = await res.json();
console.log(data.choices[0].message.content);Note the order of checks. fetch only rejects on network failure, so a 402 or 403 arrives as a normal response. Test res.ok yourself and read the error JSON, which always has the shape {"error":{"code":...,"message":...}}.
Go with net/http
Go needs a few more lines but no dependencies. Define only the struct fields you read; encoding/json ignores the rest. Reading the body into bytes before checking the status lets you print the server's error message when something goes wrong.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type reply struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
func main() {
payload, _ := json.Marshal(map[string]any{
"model": "uncensored",
"messages": []map[string]string{
{"role": "user", "content": "Give me a ghost story in exactly three sentences."},
},
"max_tokens": 200,
})
req, _ := http.NewRequest("POST", "https://api.unrestrictedaiapi.com/v1/chat/completions", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
panic(fmt.Sprintf("HTTP %d: %s", resp.StatusCode, body))
}
var out reply
if err := json.Unmarshal(body, &out); err != nil {
panic(err)
}
fmt.Println(out.Choices[0].Message.Content)
}For production, build one http.Client{Timeout: 60 * time.Second} and reuse it. The default client has no timeout at all, which is how a stuck request becomes a stuck goroutine.
PHP with cURL and Ruby with Net::HTTP
PHP's cURL extension is already on most hosts. Build the body with json_encode, return the transfer as a string, then read the status with curl_getinfo. Skipping the status check is the classic mistake: a 402 body decodes happily as JSON and your code then fails on a missing choices key.
<?php
$payload = json_encode([
"model" => "uncensored",
"messages" => [
["role" => "user", "content" => "Write a villanelle opening about a broken vending machine."],
],
"max_tokens" => 200,
]);
$ch = curl_init("https://api.unrestrictedaiapi.com/v1/chat/completions");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => $payload,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $status !== 200) {
fwrite(STDERR, "HTTP $status: $body\n");
exit(1);
}
$data = json_decode($body, true);
echo $data["choices"][0]["message"]["content"], "\n";On a shared host with a low max_execution_time, keep max_tokens modest or move the call into a queue worker.
The standard library is enough here too. ENV.fetch raises immediately if the variable is missing, which beats sending an empty bearer token and debugging a 401 for ten minutes.
require "net/http"
require "json"
require "uri"
uri = URI("https://api.unrestrictedaiapi.com/v1/chat/completions")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('API_KEY')}"
req["Content-Type"] = "application/json"
req.body = JSON.generate(
model: "uncensored",
messages: [{ role: "user", content: "Invent a tavern rumor that sounds too specific to be fake." }],
max_tokens: 200
)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 60) { |http| http.request(req) }
abort("HTTP #{res.code}: #{res.body}") unless res.is_a?(Net::HTTPSuccess)
puts JSON.parse(res.body).dig("choices", 0, "message", "content")If you later move to Rails, the same body works from a background job. Keep the call out of the request cycle; generations are slower than a database query.
Both snippets send exactly the body the Python one sends, which is the point: if one language works and another does not, diff the JSON, not the client. Print the raw request body once and compare byte for byte. Nine times out of ten the culprit is a missing Content-Type header or a model name typed with a capital letter.
Streaming without a library
Streaming is the one place where plain HTTP asks a little more of you. With stream: true the server answers with server-sent events: each line starts with data: , carries a JSON chunk, and the stream ends with data: [DONE]. A final usage chunk is added automatically, so you get token counts even when streaming. That chunk has no choices entries, which is why the loop below checks before indexing.
import json
import os
import httpx
body = {
"model": "uncensored",
"messages": [{"role": "user", "content": "Tell a campfire story about a lighthouse keeper."}],
"stream": True,
"max_tokens": 400,
}
headers = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
with httpx.stream("POST", "https://api.unrestrictedaiapi.com/v1/chat/completions",
headers=headers, json=body, timeout=60.0) as r:
r.raise_for_status()
for line in r.iter_lines():
if not line.startswith("data: "):
continue
data = line[6:]
if data == "[DONE]":
break
chunk = json.loads(data)
if chunk.get("choices"):
print(chunk["choices"][0]["delta"].get("content") or "", end="", flush=True)
print()The same pattern ports directly. In JavaScript, read res.body with a reader and split on newlines. In Go, wrap resp.Body in a bufio.Scanner. In Ruby, pass a block to http.request and use read_body. In PHP, set a CURLOPT_WRITEFUNCTION callback. The logic never changes: strip the prefix, stop at the sentinel, parse the JSON, print the delta.
A five-minute smoke test and a budget check
Before you build on top of any of these snippets, run a quick sanity pass. First, call /v1/models with your key; a 200 proves the key and network path. Second, send the smallest chat request you can with max_tokens set to 20. Third, deliberately break something, such as an empty bearer token, and confirm your code surfaces the 401 clearly instead of crashing on a missing field.
Then do the arithmetic once so nothing surprises you. Prices are $0.25 per million input tokens and $1.00 per million output tokens. As an assumption for illustration, say a request sends 600 prompt tokens and receives 400 tokens back. That is 600 x $0.25 / 1,000,000 = $0.00015 in, plus 400 x $1.00 / 1,000,000 = $0.0004 out, about $0.00055 per call. The trial credit of $0.50 would cover roughly 900 calls of that size. Your real prompts will differ, so read the usage block on a few responses and multiply from there.
The trial credit lasts seven days and needs no payment details, which makes it plenty for testing all five languages. If you want a use case to aim at, the NSFW content guide shows a complete example. Remember that one key belongs to one account; if you regenerate it, update every script at once, because the old key stops working immediately.
Errors that look the same in every language
Because the API is plain HTTP, the failure handling is identical whichever language you pick. Memorise this table and you can port any snippet above.
| Status | Meaning | Action |
|---|---|---|
| 400 | Bad request, such as prompt plus max_tokens beyond 64k | Trim the prompt or lower max_tokens |
| 401 | Missing or invalid key | Check API_KEY; a regenerated key kills the old one instantly |
| 402 | no_credit: balance gone or trial expired | Top up the prepaid balance |
| 403 | content_blocked | Do not retry; sexual content involving minors is always blocked |
| 429 | Over 300 requests a minute | Back off and retry |
| 503 | upstream_busy | Retry in a few seconds |
Once the basics work, set stream: true for token-by-token output and read the server-sent events line by line. Craft better prompts with the prompting guide, or wire the same call into a chat platform using the bot walkthrough. Rates are on the pricing page.