Developer guide

18+ only. This site and the API are for adults. They describe and produce sexually explicit material. If you are under 18, or adult content is illegal where you are, leave now.

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

Base URL: https://api.notsafu.com. All endpoints live under /v1.

  1. Keep the API key you received by email in an environment variable: export NOTSAFU_API_KEY=...
  2. List the models you can use: GET /v1/concepts, and the pipelines: GET /v1/workflows.
  3. Start a render with POST /v1/render and either a callback_url or "stream": true.
  4. 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

Errors and limits

Errors use the same envelope: {"errors": [{"status": 403, "title": "Missing Authentication Token"}]}.

StatusMeaning
400Invalid request: missing field, malformed JSON, bad option value.
401 / 403Missing or invalid key, or the resource belongs to another account.
404Unknown endpoint or id.
413Request body too large (10 MB max for JSON bodies). Send media as URLs, not inline.
429Too many requests from your IP address. Back off and retry; honor Retry-After when present.
502 / 503 / 504The 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.

WriteEffect
/images:2Number of images in the batch.
/size:832x1216Width x height in pixels.
/seed:12345Fixed seed for repeatable results.
/guidance:5Prompt adherence (CFG).
/steps:28, /sampler:euler_ancestral, /strength:0.6Sampler 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:16Video length in frames, and frame rate.
/initimage:URL1, /initvideo:URL1, /initaudio:URL1, /blend:URL1Use 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.

ParameterDescription
prompt requiredThe prompt. Every option can be written inline (see Prompt syntax), for example "/images:2 /size:832x1216 a portrait, studio light".
callback_url requiredYour HTTPS webhook. It receives a POST when the render completes. Required unless you stream ("stream": true).
error_urlWebhook that receives a POST with error_details if the render fails.
init_imageURL of a starting image for image-to-image, edits and image-to-video workflows.
mask_imageURL of an inpainting mask.
placeholdersMap 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_returntrue: one webhook per finished image. Default false: one webhook when the whole batch is done.
streamtrue: keep the HTTP connection open and receive progress as Server-Sent Events (Accept: text/event-stream).
session_idYour own correlation id (string, under 256 characters), echoed back in the response and webhook.
virtual_user_idAttribute 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.

ParameterDescription
prompt requiredStarts with /workflow /run:<slug>, followed by workflow options and your text.
init_imageInput image for edit and image-to-video workflows.
callback_url requiredYour 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.

ParameterDescription
render_hash requiredPath 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.

ParameterDescription
encoded_image_url requiredPath 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.

ParameterDescription
persona requiredPersona slug, e.g. "polly" for the default assistant, or one you created.
prompt requiredYour message.
synctrue: wait and return the reply in this response.
callback_urlWebhook for the reply when sync is not set.
image_urlImage for vision-capable models (JPEG, PNG or WebP over HTTPS). Ignored by text-only models.
reply_toresponse_id of a previous reply, to continue that conversation with its history.
session_idYour correlation id, echoed back.
virtual_user_idChat 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.

ParameterDescription
slug requiredPath 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.

ParameterDescription
searchFilter by name, model family or description.
pagePage number, default 1.
per_pageItems 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.

ParameterDescription
searchFilter by slug.
pagePage number, default 1.
per_pageItems 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.

ParameterDescription
name requiredLowercase letters, numbers and hyphens.
metadataObject 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());

Semantic search; results come with a distance score (lower is closer).

ParameterDescription
query requiredWhat to look for.
n_resultsDefault 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.

ParameterDescription
query requiredYour 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.

ParameterDescription
url requiredStart URL.
restrict_prefixOnly crawl URLs starting with this prefix. Defaults to url.
max_depthLink 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.

ParameterDescription
text requiredThe fact.
metadataObject 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.

ParameterDescription
virtual_user_idQuery 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).

ParameterDescription
slug requiredPath parameter.
virtual_user_idQuery 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>.

ParameterDescription
name requiredDisplay name.
description requiredShort description used to pick the skill.
content requiredMarkdown instructions.
slugGenerated from name if omitted.
allows_input_mediaAccepts image/audio/video input. Default false.
is_publicVisible to everyone. Default false.
is_open_sourceSource visible to others. Default true.
virtual_user_idOwner 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.

ParameterDescription
(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.

ParameterDescription
slug requiredPath 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.

ParameterDescription
prompt requiredThe plain-language request.
virtual_user_idChoose 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.

ParameterDescription
prompt requiredThe plain-language request.
input_media{"type": "image|audio|video", "reference": "<url>"}
virtual_user_idInvoke 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.

ParameterDescription
prompt requiredThe plain-language request.
input_media{"type": "image|audio|video", "reference": "<url>"}
virtual_user_idRun 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).

ParameterDescription
(any string keys) requiredMetadata 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());

Returns every virtual user whose metadata matches all given key/value pairs, as {"VU...": {metadata}}.

ParameterDescription
(any string keys) requiredMetadata 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.

ParameterDescription
virtual_user_id requiredPath 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.

ParameterDescription
(any string keys) requiredKeys 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:

Report abuse or request a takedown at legal@notsafu.com.