An NSFW Image Generation API for Developers
By The Fellowi Team · · 7 min read

Most image-generation APIs refuse NSFW content outright, as a matter of policy, before you even get to a technical limit. That is a reasonable choice for a general-purpose platform, and it also means there is close to nothing for a developer who genuinely needs programmatic access to a generator that allows adult content. Fellowi’s public API exists for that case. It allows explicit generation for registered adult accounts, under hard limits that never move: no content involving minors, no content depicting non-consent. Those are not settings you can toggle around - they are refused outright, no exceptions.
Outside of that, it is a plain REST API: an API key, a POST that queues a job, a poll, a download. If you have used any async generation API before, this will feel familiar.
Get a key
Create an account, then create an API key at the API Console. The key looks like fk_live_<random string> and is shown to you in full exactly once, at creation. After that, only a hash and a short preview are kept on our side - if you lose it, you revoke it and create a new one, the same as any other API key you have ever worked with. Save it somewhere real before you close the tab.
Every request authenticates with a standard bearer header:
Authorization: Bearer fk_live_your_key_hereQueue a generation
POST /v1/api/images with a prompt and your options. The call returns immediately with HTTP 202 - it does not block while the image renders:
POST /v1/api/images
Content-Type: application/json
Authorization: Bearer fk_live_your_key_here
Idempotency-Key: a-key-you-generate-per-attempt
{
"prompt": "a neon-lit rooftop at night, wide shot, cinematic lighting",
"quality": "standard",
"format": "png",
"aspectRatio": "16:9"
}The response carries a job id, an etaMs estimate, and your remaining coin balance after the charge. The optional Idempotency-Key header makes a retried request safe - send the same key on a retry and you will not be charged twice for the same attempt.
The job itself sits under image, with your wallet balance alongside it:
{
"image": { "id": "0f9c...", "status": "queued", "etaMs": 42000, "coinsCharged": 40 },
"coins": 360
}Poll, then download
Poll GET /v1/api/images/:id until status reaches "succeeded" or "failed". On success, fetch the actual bytes from GET /v1/api/images/:id/content. If it fails, the coins you spent are refunded automatically, and failureCode is always one of exactly four values: TIMEOUT, MODERATION_REJECTED, VENDOR_ERROR, or UNKNOWN. There is no fifth code to handle and no partial charge to reconcile - we go deeper into why in how the API is priced and what happens when a generation fails.
curl, start to finish
API_KEY="fk_live_your_key_here"
# 1. queue a job
JOB=$(curl -s https://fellowi.com/api/v1/api/images \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"a neon-lit rooftop at night","quality":"standard","format":"png","aspectRatio":"16:9"}')
ID=$(echo "$JOB" | jq -r .image.id)
# 2. poll until it's done
while true; do
STATUS=$(curl -s https://fellowi.com/api/v1/api/images/$ID \
-H "Authorization: Bearer $API_KEY" | jq -r .image.status)
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && { echo "generation failed"; exit 1; }
sleep 3
done
# 3. download the image
curl -s https://fellowi.com/api/v1/api/images/$ID/content \
-H "Authorization: Bearer $API_KEY" -o output.pngNode
const API_KEY = process.env.FELLOWI_API_KEY;
const base = "https://fellowi.com/api/v1/api";
const headers = { Authorization: `Bearer ${API_KEY}` };
const create = await fetch(`${base}/images`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "a neon-lit rooftop at night", quality: "standard", format: "png", aspectRatio: "16:9" }),
});
const { image } = await create.json();
const id = image.id;
let job;
do {
await new Promise((r) => setTimeout(r, 3000));
job = await fetch(`${base}/images/${id}`, { headers }).then((r) => r.json());
} while (job.image.status !== "succeeded" && job.image.status !== "failed");
if (job.image.status === "failed") throw new Error(job.image.failureCode);
const bytes = await fetch(`${base}/images/${id}/content`, { headers }).then((r) => r.arrayBuffer());
await Bun.write("output.png", bytes);Python
import os, time, requests
API_KEY = os.environ["FELLOWI_API_KEY"]
base = "https://fellowi.com/api/v1/api"
headers = {"Authorization": f"Bearer {API_KEY}"}
job = requests.post(f"{base}/images", headers=headers, json={
"prompt": "a neon-lit rooftop at night",
"quality": "standard",
"format": "png",
"aspectRatio": "16:9",
}).json()
while True:
time.sleep(3)
status = requests.get(f"{base}/images/{job['image']['id']}", headers=headers).json()
if status["image"]["status"] in ("succeeded", "failed"):
break
if status["image"]["status"] == "failed":
raise RuntimeError(status["image"]["failureCode"])
content = requests.get(f"{base}/images/{job['image']['id']}/content", headers=headers)
open("output.png", "wb").write(content.content)Model, pricing, and limits
Every response reports the model as fellowi-nsfw-image-1. That is the only name you will see - it is the same generator behind the web app at Fellowi Images, and it is billed against the same Fellowi Coins wallet at the same price: 40 coins for Standard (1.5K), 60 for High quality (2K). There is no separate API price list to reconcile against the web app’s.
Rate limits are an abuse guard, not the real ceiling: 20 requests per minute for generation calls, 120 per minute for reads, by default. GET /v1/api/mereturns your current coin balance, your key’s rate limit, and how many images you have left today under the daily cap - poll it before you queue a batch, not after you hit a wall.
Full reference docs, live pricing, and key management all live at the API Console.