Developer guide
Everything you need to call the Not Safu API: authentication, prompt syntax, every endpoint with curl, Python and JavaScript examples, and the rules for what you may generate. Read the acceptable use policy before you send your first request; it is part of your terms of access.
- Quick start
- Authentication
- Requests and responses
- Errors and limits
- Media URLs
- Prompt syntax
- Render (images, video, audio)
- Chat
- Concepts and workflows
- Knowledge bases
- Skills
- Virtual users
- Content rules
Quick start
Base URL: https://api.notsafu.com. All endpoints live under /v1.
- Keep the API key you received by email in an environment variable:
export NOTSAFU_API_KEY=... - List the models you can use:
GET /v1/concepts, and the pipelines:GET /v1/workflows. - Start a render with
POST /v1/renderand either acallback_urlor"stream": true. - Download the finished media from the URLs in the result and store your own copy.
curl -sS -X POST "https://api.notsafu.com/v1/render" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-d '{"prompt": "/images:1 lingerie editorial, soft window light", "callback_url": "https://your-app.example/webhooks/render"}'
Already integrated with the same API on another host? Keep your code and change only the host: /api/v3/... paths are accepted too and behave exactly like /v1/....
Authentication
Send your key as a Bearer token on every request: Authorization: Bearer YOUR_KEY. The render-events WebSocket prefers the same Bearer header (or Sec-WebSocket-Protocol: bearer.YOUR_KEY); ?token=YOUR_KEY still works for compatibility. There is no sign-up or key management on this site: your key and any changes to your access arrive by email. If you think a key leaked, reply to that email right away.
Call the API from your server only. The API does not send CORS headers, so browsers will block direct calls, which keeps your key out of web pages.
Requests and responses
- Bodies are JSON. Send
Content-Type: application/vnd.api+json(plainapplication/jsonalso works) andAccept: application/vnd.api+json. - Responses follow JSON:API: the object is in
data, its fields indata.attributes, and its id indata.id. Lists addmeta(total, page, per_page, total_pages) andlinks(pagination URLs). - Every response carries an
x-request-idheader. Quote it when you report a problem.
Errors and limits
Errors use the same envelope: {"errors": [{"status": 403, "title": "Missing Authentication Token"}]}.
| Status | Meaning |
|---|---|
| 400 | Invalid request: missing field, malformed JSON, bad option value. |
| 401 / 403 | Missing or invalid key, or the resource belongs to another account. |
| 404 | Unknown endpoint or id. |
| 413 | Request body too large (10 MB max for JSON bodies). Send media as URLs, not inline. |
| 429 | Too many requests from your IP address. Back off and retry; honor Retry-After when present. |
| 502 / 503 / 504 | The rendering backend failed or timed out. Retry with exponential backoff (for example 2 s, 4 s, 8 s, max 5 tries). |
Rate limit: about 120 API requests per minute per IP address, and 600 media downloads per minute. Renders are queued; one request can produce several images.
Media URLs
Images, videos and audio in responses, webhooks and stream events are served from https://api.notsafu.com/v1/assets/.... These URLs need no key, so treat them as private links: download the file and serve your own copy rather than hot-linking. You can pass these URLs back as inputs (init_image, placeholders, /v1/render/from-url/...).
Prompt syntax
A prompt is plain text plus optional slash options. Options can go anywhere but are easiest to read at the start.
| Write | Effect |
|---|---|
/images:2 | Number of images in the batch. |
/size:832x1216 | Width x height in pixels. |
/seed:12345 | Fixed seed for repeatable results. |
/guidance:5 | Prompt adherence (CFG). |
/steps:28, /sampler:euler_ancestral, /strength:0.6 | Sampler settings; strength applies to image-to-image. |
<token> or <token:0.8> | Use a concept (model, LoRA, embedding) by its token from /v1/concepts, optionally weighted. |
[[blurry, deformed]] | Negative prompt: what to avoid. Put it at the end. |
/workflow /run:slug (or /wf /run:slug) | Run a workflow from /v1/workflows. |
/length:81 /fps:16 | Video length in frames, and frame rate. |
/initimage:URL1, /initvideo:URL1, /initaudio:URL1, /blend:URL1 | Use an input from placeholders. |
Example: /images:2 /size:832x1216 /seed:7 <token:0.8> candid photo of an adult couple on a beach at golden hour [[cartoon, blurry]]
Describe adults as adults. Prompts that ask for minors, school settings with sexual content, or childlike bodies are not allowed and will cost you your access; see Content rules.
Render (images, video, audio)
POST/v1/render
Create a render
Queue an image, video or audio generation. Rendering is asynchronous: the call returns right away with a render_hash, and results arrive at your callback_url (webhook), on a stream (see Streaming below), or on the render-events WebSocket. A typical image takes about 15 seconds.
| Parameter | Description |
|---|---|
prompt required | The prompt. Every option can be written inline (see Prompt syntax), for example "/images:2 /size:832x1216 a portrait, studio light". |
callback_url required | Your HTTPS webhook. It receives a POST when the render completes. Required unless you stream ("stream": true). |
error_url | Webhook that receives a POST with error_details if the render fails. |
init_image | URL of a starting image for image-to-image, edits and image-to-video workflows. |
mask_image | URL of an inpainting mask. |
placeholders | Map of named input URLs, keys must start with URL, e.g. {"URL1": "https://example.com/a.jpg"}. Refer to them in the prompt: /initimage:URL1, /initvideo:URL1, /initaudio:URL1, /blend:URL1. |
progressive_return | true: one webhook per finished image. Default false: one webhook when the whole batch is done. |
stream | true: keep the HTTP connection open and receive progress as Server-Sent Events (Accept: text/event-stream). |
session_id | Your own correlation id (string, under 256 characters), echoed back in the response and webhook. |
virtual_user_id | Attribute the render to one of your virtual users (VU...). |
curl
curl -sS -X POST "https://api.notsafu.com/v1/render" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"prompt": "/images:2 /size:832x1216 portrait of a woman in a red dress, film photo [[blurry, deformed]]",
"callback_url": "https://your-app.example/webhooks/render",
"session_id": "user-42-job-7"
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"prompt": "/images:2 /size:832x1216 portrait of a woman in a red dress, film photo [[blurry, deformed]]",
"callback_url": "https://your-app.example/webhooks/render",
"session_id": "user-42-job-7",
}
r = requests.post(f"{BASE}/render", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"prompt": "/images:2 /size:832x1216 portrait of a woman in a red dress, film photo [[blurry, deformed]]",
"callback_url": "https://your-app.example/webhooks/render",
"session_id": "user-42-job-7"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Example response
{
"data": {
"id": "P8W8xG",
"type": "render",
"attributes": {
"render_hash": "P8W8xG",
"has_been_rendered": false,
"estimated_render_time": 12.5,
"estimated_wait_time": 3.2,
"session_id": "user-42-job-7",
"task": "txt2img",
"is_nsfw": true,
"images": []
}
},
"jsonapi": { "version": "1.0" }
}POST/v1/render
Run a workflow (edit, video, audio)
Workflows are named pipelines (see List workflows). Select one with /workflow /run:<slug> (short form /wf /run:<slug>) at the start of the prompt. Inputs go in init_image or placeholders.
| Parameter | Description |
|---|---|
prompt required | Starts with /workflow /run:<slug>, followed by workflow options and your text. |
init_image | Input image for edit and image-to-video workflows. |
callback_url required | Your webhook (or use "stream": true). |
curl
curl -sS -X POST "https://api.notsafu.com/v1/render" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"init_image": "https://your-cdn.example/input.png",
"prompt": "/workflow /run:animate-wan22 /size:704x1024 /length:81 /fps:16 she smiles at the camera",
"callback_url": "https://your-app.example/webhooks/render"
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"init_image": "https://your-cdn.example/input.png",
"prompt": "/workflow /run:animate-wan22 /size:704x1024 /length:81 /fps:16 she smiles at the camera",
"callback_url": "https://your-app.example/webhooks/render",
}
r = requests.post(f"{BASE}/render", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"init_image": "https://your-cdn.example/input.png",
"prompt": "/workflow /run:animate-wan22 /size:704x1024 /length:81 /fps:16 she smiles at the camera",
"callback_url": "https://your-app.example/webhooks/render"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
GET/v1/render/{render_hash}
Get a render
Status and results of a render, in the same format as the completion webhook. Do not poll this endpoint in a loop; use the webhook, a stream or the WebSocket and call this only to recover state.
| Parameter | Description |
|---|---|
render_hash required | Path parameter: the render_hash returned when you created the render. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/render/P8W8xG" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
r = requests.get(f"{BASE}/render/P8W8xG", headers=HEADERS, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/render/P8W8xG", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Example response
{
"data": {
"id": "P8W8xG",
"type": "render",
"attributes": {
"render_hash": "P8W8xG",
"has_been_rendered": true,
"is_nsfw": true,
"images": [
{
"id": 12345,
"url": "https://api.notsafu.com/v1/assets/s1/renders2/P8W8xG/1-portrait.png",
"render_options": { "width": 832, "height": 1216, "guidance": 4.5, "steps": 28 }
}
]
}
}
}GET/v1/render/from-url/{encoded_image_url}
Look up a render from an image URL
Given the URL of a finished image (as returned by this API), return the render it came from, including generated_prompt so you can recreate it. URL-encode the image URL before putting it in the path. Images from other accounts return a "bad community" error.
| Parameter | Description |
|---|---|
encoded_image_url required | Path parameter: the form-encoded image URL. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/render/from-url/https%3A%2F%2Fapi.notsafu.com%2Fv1%2Fassets%2Fs1%2Frenders2%2FP8W8xG%2F1-portrait.png" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
r = requests.get(f"{BASE}/render/from-url/https%3A%2F%2Fapi.notsafu.com%2Fv1%2Fassets%2Fs1%2Frenders2%2FP8W8xG%2F1-portrait.png", headers=HEADERS, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/render/from-url/https%3A%2F%2Fapi.notsafu.com%2Fv1%2Fassets%2Fs1%2Frenders2%2FP8W8xG%2F1-portrait.png", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/render with "stream": true
Streaming
Set "stream": true and Accept: text/event-stream to receive progress on the same connection as Server-Sent Events. Each data: line is a JSON object. No callback_url is needed. Keep the connection open until the final event; set a generous read timeout (video can take minutes).
curl
curl -N -X POST "https://api.notsafu.com/v1/render" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: text/event-stream" \
-d '{"prompt": "/images:1 neon-lit boudoir portrait", "stream": true}'
Python
import json, os, requests
r = requests.post(
"https://api.notsafu.com/v1/render",
headers={
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Content-Type": "application/vnd.api+json",
"Accept": "text/event-stream",
},
json={"prompt": "/images:1 neon-lit boudoir portrait", "stream": True},
stream=True,
timeout=300,
)
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if line and line.startswith("data:"):
event = json.loads(line[5:].strip())
print(event)
JavaScript
const res = await fetch("https://api.notsafu.com/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
"Content-Type": "application/vnd.api+json",
Accept: "text/event-stream",
},
body: JSON.stringify({ prompt: "/images:1 neon-lit boudoir portrait", stream: true }),
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += value;
let i;
while ((i = buf.indexOf("\n\n")) !== -1) {
const evt = buf.slice(0, i); buf = buf.slice(i + 2);
for (const line of evt.split("\n")) if (line.startsWith("data:")) console.log(JSON.parse(line.slice(5)));
}
}
WSS/v1/render-events/{render_hash}?token=YOUR_KEY
Render events WebSocket
Live progress for one render. Prefer Authorization: Bearer YOUR_KEY on the WebSocket handshake (or Sec-WebSocket-Protocol: bearer.YOUR_KEY). The ?token=YOUR_KEY query form still works for compatibility but puts the key in the URL; avoid it when you can. The render must belong to your account, otherwise the connection is refused with 403. Messages are JSON: {"event": "...", "data": {...}}. Events: connected (current state), started, image_done (one image finished), done (all images, with URLs), error. You can send {"command": "get_state"} or {"command": "ping"}. Connect from your server, not from a browser: a key in a browser is a leaked key.
CLI
# curl cannot hold a WebSocket open; use websocat: websocat "wss://api.notsafu.com/v1/render-events/P8W8xG?token=$NOTSAFU_API_KEY"
Python
import asyncio, json, os, sys
import websockets # pip install websockets
async def main(render_hash):
url = f"wss://api.notsafu.com/v1/render-events/{render_hash}?token={os.environ['NOTSAFU_API_KEY']}"
async with websockets.connect(url) as ws:
async for message in ws:
msg = json.loads(message)
print(msg["event"], msg.get("data"))
if msg["event"] in ("done", "error"):
break
asyncio.run(main(sys.argv[1]))
JavaScript
// Node 22+ has a global WebSocket; older Node: npm install ws
const hash = process.argv[2];
const ws = new WebSocket(`wss://api.notsafu.com/v1/render-events/${hash}?token=${process.env.NOTSAFU_API_KEY}`);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
console.log(msg.event, msg.data);
if (msg.event === "done" || msg.event === "error") ws.close();
};
// Keepalive / resync: ws.send(JSON.stringify({ command: "ping" })) or { command: "get_state" }
Webhooks (callback_url and error_url)
When a render finishes we POST JSON to your callback_url. With progressive_return: true you get one POST per image (delivery: "single"), otherwise one POST for the batch (delivery: "all"). Failures go to error_url with error_details. Answer with any 2xx status within a few seconds and do slow work afterwards. Use an unguessable path and match session_id / id to a job you created; ignore anything you did not ask for.
Example payload
{
"id": "P8W8xG",
"has_been_rendered": true,
"session_id": "user-42-job-7",
"is_nsfw": true,
"delivery": "all",
"images": [
{
"id": 12345,
"url": "https://api.notsafu.com/v1/assets/s1/renders2/P8W8xG/1-portrait.png",
"render_options": { "width": 832, "height": 1216, "guidance": 4.5, "steps": 28 }
}
]
}Python
# Minimal webhook receiver (Flask). Respond 2xx quickly, then process.
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/render")
def render_done():
p = request.get_json(force=True)
if p.get("has_been_rendered"):
for img in p.get("images", []):
print(p.get("session_id"), img["url"]) # download and store your own copy
else:
print("render failed:", p.get("error_details"))
return "", 204
JavaScript
// Minimal webhook receiver (Express).
import express from "express";
const app = express();
app.post("/webhooks/render", express.json({ limit: "5mb" }), (req, res) => {
const p = req.body;
if (p.has_been_rendered) for (const img of p.images ?? []) console.log(p.session_id, img.url);
else console.log("render failed:", p.error_details);
res.sendStatus(204);
});
app.listen(3000);Chat
POST/v1/chat
Chat with a persona
Send a message to a chatbot persona backed by a large language model. Use "sync": true to get the answer inline (3 to 60 seconds; set a client timeout), or give a callback_url to receive it by webhook.
| Parameter | Description |
|---|---|
persona required | Persona slug, e.g. "polly" for the default assistant, or one you created. |
prompt required | Your message. |
sync | true: wait and return the reply in this response. |
callback_url | Webhook for the reply when sync is not set. |
image_url | Image for vision-capable models (JPEG, PNG or WebP over HTTPS). Ignored by text-only models. |
reply_to | response_id of a previous reply, to continue that conversation with its history. |
session_id | Your correlation id, echoed back. |
virtual_user_id | Chat on behalf of one of your virtual users. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/chat" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"persona": "polly",
"prompt": "Write three flirty opening lines for a fantasy roleplay.",
"sync": true
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"persona": "polly",
"prompt": "Write three flirty opening lines for a fantasy roleplay.",
"sync": True,
}
r = requests.post(f"{BASE}/chat", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"persona": "polly",
"prompt": "Write three flirty opening lines for a fantasy roleplay.",
"sync": true
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
GET/v1/bot/{slug}
Get a persona
Name, greeting and personality summary of a chatbot persona.
| Parameter | Description |
|---|---|
slug required | Path parameter: persona slug. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/bot/polly" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
r = requests.get(f"{BASE}/bot/polly", headers=HEADERS, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/bot/polly", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Concepts and workflows
GET/v1/concepts
List concepts (models)
Models, LoRAs and embeddings you can use in prompts. Each concept has a token: write it in angle brackets in your prompt, optionally with a weight, e.g. <token> or <token:0.8>. is_nsfw tells you whether a concept is tuned for adult content.
| Parameter | Description |
|---|---|
search | Filter by name, model family or description. |
page | Page number, default 1. |
per_page | Items per page, default 500, max 1000. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/concepts?search=realistic&per_page=50" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
params = {
"search": "realistic",
"per_page": "50",
}
r = requests.get(f"{BASE}/concepts", headers=HEADERS, params=params, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/concepts?search=realistic&per_page=50", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Example response
{
"data": [
{
"id": "1234",
"type": "concept",
"attributes": {
"name": "Example Realistic XL",
"token": "example-realistic-xl",
"model_family": "SDXL",
"type": "full_model",
"is_nsfw": true,
"tags": ["photo", "realistic"],
"example_url": "https://api.notsafu.com/v1/assets/s1/..."
}
}
],
"meta": { "total": 812, "page": 1, "per_page": 50, "total_pages": 17 },
"links": { "next": "https://api.notsafu.com/v1/concepts?page=2&per_page=50&search=realistic" }
}GET/v1/workflows
List workflows
Rendering pipelines for images, video and audio. Use the slug with /workflow /run:<slug>. The supports_* flags tell you which inputs and outputs a workflow handles (txt2img, img2img, img2vid, txt2vid, and so on); avg_elapsed is the typical run time in seconds.
| Parameter | Description |
|---|---|
search | Filter by slug. |
page | Page number, default 1. |
per_page | Items per page, default 500, max 1000. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/workflows?search=wan&per_page=50" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
params = {
"search": "wan",
"per_page": "50",
}
r = requests.get(f"{BASE}/workflows", headers=HEADERS, params=params, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/workflows?search=wan&per_page=50", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Knowledge bases
POST/v1/knowledge
Create a knowledge base
Create a named knowledge base ("domain"; just a name, not an internet domain). The key that creates it owns it; only that key can add content.
| Parameter | Description |
|---|---|
name required | Lowercase letters, numbers and hyphens. |
metadata | Object stored with the domain, e.g. {"description": "..."}. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/knowledge" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"name": "my-characters",
"metadata": {
"description": "Character bios for my roleplay app"
}
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"name": "my-characters",
"metadata": {
"description": "Character bios for my roleplay app",
},
}
r = requests.post(f"{BASE}/knowledge", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/knowledge", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"name": "my-characters",
"metadata": {
"description": "Character bios for my roleplay app"
}
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/knowledge/{domain}/search
Search a knowledge base
Semantic search; results come with a distance score (lower is closer).
| Parameter | Description |
|---|---|
query required | What to look for. |
n_results | Default 10, max 20. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/knowledge/my-characters/search" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"query": "who is Mira",
"n_results": 5
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"query": "who is Mira",
"n_results": 5,
}
r = requests.post(f"{BASE}/knowledge/my-characters/search", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/knowledge/my-characters/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"query": "who is Mira",
"n_results": 5
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/knowledge/{domain}/chat
Chat with a knowledge base
Ask a question; the answer is grounded in the knowledge base and lists its sources.
| Parameter | Description |
|---|---|
query required | Your question. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/knowledge/my-characters/chat" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"query": "Summarize Mira in two sentences."
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"query": "Summarize Mira in two sentences.",
}
r = requests.post(f"{BASE}/knowledge/my-characters/chat", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/knowledge/my-characters/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"query": "Summarize Mira in two sentences."
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/knowledge/{domain}/crawl
Crawl a URL into a knowledge base (owner only)
Fetch a site and add its pages to the knowledge base. Only crawl content you have the right to use.
| Parameter | Description |
|---|---|
url required | Start URL. |
restrict_prefix | Only crawl URLs starting with this prefix. Defaults to url. |
max_depth | Link depth from the start URL, default 2. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/knowledge/my-characters/crawl" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"url": "https://your-site.example/wiki",
"restrict_prefix": "https://your-site.example/wiki",
"max_depth": 2
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"url": "https://your-site.example/wiki",
"restrict_prefix": "https://your-site.example/wiki",
"max_depth": 2,
}
r = requests.post(f"{BASE}/knowledge/my-characters/crawl", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/knowledge/my-characters/crawl", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"url": "https://your-site.example/wiki",
"restrict_prefix": "https://your-site.example/wiki",
"max_depth": 2
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/knowledge/{domain}/fact
Add a fact (owner only)
Add one piece of text to the knowledge base.
| Parameter | Description |
|---|---|
text required | The fact. |
metadata | Object stored with the fact. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/knowledge/my-characters/fact" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"text": "Mira is a 29-year-old starship pilot from Titan.",
"metadata": {
"character": "mira"
}
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"text": "Mira is a 29-year-old starship pilot from Titan.",
"metadata": {
"character": "mira",
},
}
r = requests.post(f"{BASE}/knowledge/my-characters/fact", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/knowledge/my-characters/fact", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"text": "Mira is a 29-year-old starship pilot from Titan.",
"metadata": {
"character": "mira"
}
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Skills
GET/v1/skills
List skills
Skills are instruction documents that turn plain-language requests into render commands. This lists your own skills plus public ones.
| Parameter | Description |
|---|---|
virtual_user_id | Query parameter: list as this virtual user. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/skills" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
r = requests.get(f"{BASE}/skills", headers=HEADERS, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
GET/v1/skills/{slug}
Get a skill
Skill details. content is included only if you may view the source (owner, or an open-source skill).
| Parameter | Description |
|---|---|
slug required | Path parameter. |
virtual_user_id | Query parameter: view as this virtual user. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/skills/photo-enhance" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
r = requests.get(f"{BASE}/skills/photo-enhance", headers=HEADERS, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills/photo-enhance", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/skills
Create a skill
Create a skill owned by your key (or by virtual_user_id). content is Markdown for the language model; reference workflows as [workflow-slug] and commands as /wf /run:<slug>.
| Parameter | Description |
|---|---|
name required | Display name. |
description required | Short description used to pick the skill. |
content required | Markdown instructions. |
slug | Generated from name if omitted. |
allows_input_media | Accepts image/audio/video input. Default false. |
is_public | Visible to everyone. Default false. |
is_open_source | Source visible to others. Default true. |
virtual_user_id | Owner virtual user. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/skills" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"name": "Photo Enhancer",
"slug": "photo-enhance",
"description": "Upscale and clean up a photo",
"content": "# Photo Enhancer\n\n## Workflow References\n- [upscale] - High-quality upscaling\n\n## Instructions\n1. Use /wf /run:upscale",
"allows_input_media": true,
"is_public": false
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"name": "Photo Enhancer",
"slug": "photo-enhance",
"description": "Upscale and clean up a photo",
"content": "# Photo Enhancer\n\n## Workflow References\n- [upscale] - High-quality upscaling\n\n## Instructions\n1. Use /wf /run:upscale",
"allows_input_media": True,
"is_public": False,
}
r = requests.post(f"{BASE}/skills", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"name": "Photo Enhancer",
"slug": "photo-enhance",
"description": "Upscale and clean up a photo",
"content": "# Photo Enhancer\n\n## Workflow References\n- [upscale] - High-quality upscaling\n\n## Instructions\n1. Use /wf /run:upscale",
"allows_input_media": true,
"is_public": false
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
PATCH/v1/skills/{slug}
Update a skill
PUT or PATCH. Send only the fields to change; version increments on each update. Owner (or write access) only.
| Parameter | Description |
|---|---|
(any create field) | Fields to change. |
curl
curl -sS -X PATCH "https://api.notsafu.com/v1/skills/photo-enhance" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"description": "Upscale, denoise and fix faces"
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"description": "Upscale, denoise and fix faces",
}
r = requests.patch(f"{BASE}/skills/photo-enhance", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills/photo-enhance", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"description": "Upscale, denoise and fix faces"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
DELETE/v1/skills/{slug}
Delete a skill
Permanent. Owner only.
| Parameter | Description |
|---|---|
slug required | Path parameter. |
curl
curl -sS -X DELETE "https://api.notsafu.com/v1/skills/photo-enhance" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
r = requests.delete(f"{BASE}/skills/photo-enhance", headers=HEADERS, timeout=120)
r.raise_for_status()
print(r.status_code)
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills/photo-enhance", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(res.status);
POST/v1/skills/select
Select a skill
Stage 1 only: pick the best skill for a request (returns selected_skill, confidence, reasoning) without generating a command.
| Parameter | Description |
|---|---|
prompt required | The plain-language request. |
virtual_user_id | Choose among this virtual user's skills. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/skills/select" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"prompt": "make this portrait look professional"
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"prompt": "make this portrait look professional",
}
r = requests.post(f"{BASE}/skills/select", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills/select", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"prompt": "make this portrait look professional"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/skills/{slug}/invoke
Invoke a skill
Stage 2: generate a render command with a given skill. Returns command, explanation and safe; if safe is false the command will not run. Sending input_media to a skill that does not accept media returns 400.
| Parameter | Description |
|---|---|
prompt required | The plain-language request. |
input_media | {"type": "image|audio|video", "reference": "<url>"} |
virtual_user_id | Invoke as this virtual user. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/skills/photo-enhance/invoke" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"prompt": "enhance this portrait with soft lighting",
"input_media": {
"type": "image",
"reference": "https://your-cdn.example/portrait.jpg"
}
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"prompt": "enhance this portrait with soft lighting",
"input_media": {
"type": "image",
"reference": "https://your-cdn.example/portrait.jpg",
},
}
r = requests.post(f"{BASE}/skills/photo-enhance/invoke", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills/photo-enhance/invoke", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"prompt": "enhance this portrait with soft lighting",
"input_media": {
"type": "image",
"reference": "https://your-cdn.example/portrait.jpg"
}
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/skills/auto
Select and invoke in one call
Both stages at once. Pass the returned command as the prompt of POST /v1/render. error is set to no_skills_available, no_match or media_not_supported when nothing could be generated.
| Parameter | Description |
|---|---|
prompt required | The plain-language request. |
input_media | {"type": "image|audio|video", "reference": "<url>"} |
virtual_user_id | Run as this virtual user. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/skills/auto" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"prompt": "turn this photo into a 5 second video where she waves",
"input_media": {
"type": "image",
"reference": "https://your-cdn.example/photo.jpg"
}
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"prompt": "turn this photo into a 5 second video where she waves",
"input_media": {
"type": "image",
"reference": "https://your-cdn.example/photo.jpg",
},
}
r = requests.post(f"{BASE}/skills/auto", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/skills/auto", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"prompt": "turn this photo into a 5 second video where she waves",
"input_media": {
"type": "image",
"reference": "https://your-cdn.example/photo.jpg"
}
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Virtual users
POST/v1/virtualuser/user
Create a virtual user
Map one of your end users to a virtual user so their renders and chats are tracked separately. Send any flat object of string keys and string values; you get back an id starting with VU (treat it as opaque).
| Parameter | Description |
|---|---|
(any string keys) required | Metadata to store, all values strings. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/virtualuser/user" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"app_user_id": "123",
"plan": "gold"
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"app_user_id": "123",
"plan": "gold",
}
r = requests.post(f"{BASE}/virtualuser/user", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/virtualuser/user", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"app_user_id": "123",
"plan": "gold"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/virtualuser/search
Search virtual users
Returns every virtual user whose metadata matches all given key/value pairs, as {"VU...": {metadata}}.
| Parameter | Description |
|---|---|
(any string keys) required | Metadata to match. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/virtualuser/search" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"app_user_id": "123"
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"app_user_id": "123",
}
r = requests.post(f"{BASE}/virtualuser/search", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/virtualuser/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"app_user_id": "123"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
GET/v1/virtualuser/user/{virtual_user_id}
Get a virtual user
Metadata of one virtual user.
| Parameter | Description |
|---|---|
virtual_user_id required | Path parameter. |
curl
curl -sS -X GET "https://api.notsafu.com/v1/virtualuser/user/VUxyz-123" \ -H "Authorization: Bearer $NOTSAFU_API_KEY" \ -H "Accept: application/vnd.api+json"
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
r = requests.get(f"{BASE}/virtualuser/user/VUxyz-123", headers=HEADERS, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/virtualuser/user/VUxyz-123", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
POST/v1/virtualuser/user/{virtual_user_id}
Update a virtual user
Set or overwrite metadata keys. Returns the full metadata after the update.
| Parameter | Description |
|---|---|
(any string keys) required | Keys to set. |
curl
curl -sS -X POST "https://api.notsafu.com/v1/virtualuser/user/VUxyz-123" \
-H "Authorization: Bearer $NOTSAFU_API_KEY" \
-H "Accept: application/vnd.api+json" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"plan": "platinum"
}'
Python (pip install requests)
import os
import requests
BASE = "https://api.notsafu.com/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['NOTSAFU_API_KEY']}",
"Accept": "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
}
payload = {
"plan": "platinum",
}
r = requests.post(f"{BASE}/virtualuser/user/VUxyz-123", headers=HEADERS, json=payload, timeout=120)
r.raise_for_status()
print(r.json())
JavaScript (Node 18+ or any runtime with fetch, server side only)
const res = await fetch("https://api.notsafu.com/v1/virtualuser/user/VUxyz-123", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTSAFU_API_KEY}`,
Accept: "application/vnd.api+json",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
"plan": "platinum"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
Content rules
By using a key you agree to the acceptable use policy. In short:
- 18+ only, for you and for your end users. You are responsible for age-gating your own product.
- Everything you generate must be legal where you and your users are, and consensual.
- No real minors and no realistic-looking minors, ever, in any context. No sexual content involving anyone who is or appears to be under 18, whether photographic, drawn, animated or fictional.
- No sexual or intimate imagery of a real, identifiable person without that person's documented consent. No deepfakes of real people.
- No content depicting non-consensual sexual acts with real people, and nothing that is illegal to make or share.
Report abuse or request a takedown at legal@notsafu.com.