Base URL https://api.skillsafe.ai/v1/app-api. Every response is an envelope:
{"data": …} on success, {"error": {"code", "message", "details"}} on
failure. The bearer token identifies the app as well as the caller, so there is
no slug header to set.
The shape of this app
A meme here is made of three things, and only two of them are model runs.
| Stage | What it is | Where it happens |
|---|---|---|
| The words | A text run. Returns captions, two alternative caption sets, and a scene to paint. | This API |
| The picture | An image run, reached with a $model override. Returns one 1024×1024 picture as base64. | This API |
| The typesetting | Drawing the caption onto the picture, sized to fit. | Your side. No run, no charge. |
The brief sent on the image run forbids lettering inside the picture. That is deliberate and you should keep it if you build your own client: an image model cannot reliably spell, cannot be held to a character limit, and cannot be edited afterwards. Typeset the caption yourself and it is correct, it fits, and changing it is free.
The four layouts and their caption slots
format | Slots | What the picture has to be |
|---|---|---|
impact | top (70), bottom (70) — at least one | One subject, calm top and bottom fifths |
banner | caption (200) | One scene, whole frame usable |
split | caption (90, optional), left (60), right (60) | One image as two equal panels with a vertical gutter |
labels | caption (90, optional), label1–label4 (46 each; the first two required) | Two to four separated elements with space around each |
Error codes
| Code | Status | What it means here |
|---|---|---|
unauthorized | 401 | No token, or one the server no longer accepts. A cold guest call answering 401 is normal — mint a session first. |
payment_required | 402 | Balance below min_credits. Call /estimate first and compare against /me. |
validation_error | 400 | A malformed input. On an image run this is also what a rejected extra field such as $files looks like. |
rate_limited | 429 | Back off. Do not tight-loop a poll. |
internal | 5xx | The run itself failed. On a failed job the error field is a plain string about as often as an object — read both shapes. |
1. A tiny client
One helper, reused by every step below. Keep the token out of your source and out of your repository.
# Keep the token in your shell, never in a file you commit.
TOKEN="paste-your-token-here"
API="https://api.skillsafe.ai/v1/app-api"
# Every call is a bearer call. The token identifies the app as well as the caller,
# so there is no slug header to set.
call() { curl -s -X "$1" "$API$2" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" ${3:+--data "$3"}; }
import json, time, urllib.request
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # read it from your own secret store
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
# A default urllib User-Agent is rejected by the edge; set your own.
req.add_header("User-Agent", "meme-generator-client/1.0")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if envelope.get("error"):
raise RuntimeError(envelope["error"])
return envelope["data"]
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "paste-your-token-here"; // read it from your own secret store
async function call(method, path, body) {
const res = await fetch(API + path, {
method,
headers: {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const envelope = await res.json();
if (!res.ok || envelope.error) throw new Error(JSON.stringify(envelope.error));
return envelope.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"errors"
"io"
"net/http"
)
const api = "https://api.skillsafe.ai/v1/app-api"
const token = "paste-your-token-here" // read it from your own secret store
func call(method, path string, body any) (map[string]any, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, api+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var envelope struct {
Data map[string]any `json:"data"`
Error any `json:"error"`
}
json.NewDecoder(res.Body).Decode(&envelope)
if envelope.Error != nil {
return nil, errors.New("api error")
}
return envelope.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class MemeClient {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "paste-your-token-here"; // read from your own secret store
static final HttpClient HTTP = HttpClient.newHttpClient();
// Returns the raw JSON body. Parse it with the JSON library you already use;
// the response envelope is always {"data": ...} or {"error": ...}.
static String call(String method, String path, String jsonBody) throws Exception {
HttpRequest.BodyPublisher pub = jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody);
HttpRequest req = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub)
.build();
return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
}
require "json"
require "net/http"
require "uri"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # read it from your own secret store
def call(method, path, body = nil)
uri = URI(API + path)
klass = method == "GET" ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise envelope["error"].to_s if envelope["error"]
envelope["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "paste-your-token-here"; // read it from your own secret store
function call(string $method, string $path, ?array $body = null): array {
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
],
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$envelope = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!empty($envelope["error"])) {
throw new RuntimeException(json_encode($envelope["error"]));
}
return $envelope["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class MemeClient {
const string Api = "https://api.skillsafe.ai/v1/app-api";
const string Token = "paste-your-token-here"; // read it from your own secret store
static readonly HttpClient Http = new HttpClient();
public static async Task<JsonElement> Call(HttpMethod method, string path, object body = null) {
var req = new HttpRequestMessage(method, Api + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body != null) {
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
}
var res = await Http.SendAsync(req);
var envelope = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (envelope.TryGetProperty("error", out var err) && err.ValueKind != JsonValueKind.Null) {
throw new Exception(err.ToString());
}
return envelope.GetProperty("data");
}
}
2. Who am I
/me returns exactly three fields — subject_type, subject_id and credits. There is nothing else in it, so signed in means subject_type reads user.
call GET /me
# {"subject_type":"user","subject_id":"usr_...","credits":48210}
#
# /me returns exactly three fields. "Signed in" therefore means subject_type == "user";
# a guest token answers 200 with subject_type "guest" and no balance.
me = call("GET", "/me")
print(me["subject_type"], me["credits"])
# /me returns exactly three fields: subject_type, subject_id, credits.
# "Signed in" means subject_type == "user"; a guest token has no balance.
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
// /me returns exactly three fields: subject_type, subject_id, credits.
me, err := call("GET", "/me", nil)
if err != nil {
panic(err)
}
fmt.Println(me["subject_type"], me["credits"])
String me = MemeClient.call("GET", "/me", null);
System.out.println(me);
// {"data":{"subject_type":"user","subject_id":"usr_...","credits":48210}}
me = call("GET", "/me")
puts me["subject_type"], me["credits"]
$me = call("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], "\n";
var me = await MemeClient.Call(HttpMethod.Get, "/me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
3. What it will cost
Free, and it makes no job. Estimate both runs: their prices come from different rate blocks and adding one leg's figure to nothing is how a client ends up quoting half the real reserve.
# The writing run. `guide` carries the whole text served at /plan-prompt.js.
call POST /estimate '{
"task": "plan",
"guide": "<the full text of https://meme-generator.skillsafe.ai/plan-prompt.js>",
"idea": "the silence after somebody says quick question in a meeting",
"format": "impact",
"want": "top (top line, at most 70 characters, optional); bottom (bottom line, at most 70 characters, optional)",
"tone": "Deadpan - flat, understated, no exclamation marks"
}'
# The painting run. EXACTLY two fields; see the warning under this step.
call POST /estimate '{"instruction": "Original artwork...", "$model": "gpt-image"}'
# Both answer with hold_credits, min_credits, model, model_alias and markup_bps.
# estimate never runs anything and never charges.
write_input = {
"task": "plan",
"guide": GUIDE, # the full text of /plan-prompt.js
"idea": "the silence after somebody says quick question in a meeting",
"format": "impact",
"want": "top (top line, at most 70 characters, optional); "
"bottom (bottom line, at most 70 characters, optional)",
"tone": "Deadpan - flat, understated, no exclamation marks",
}
paint_input = {"instruction": brief, "$model": "gpt-image"}
print(call("POST", "/estimate", write_input)["hold_credits"])
print(call("POST", "/estimate", paint_input)["hold_credits"])
# estimate never runs anything and never charges.
const writeInput = {
task: "plan",
guide: GUIDE, // the full text of /plan-prompt.js
idea: "the silence after somebody says quick question in a meeting",
format: "impact",
want: "top (top line, at most 70 characters, optional); bottom (bottom line, at most 70 characters, optional)",
tone: "Deadpan - flat, understated, no exclamation marks",
};
const paintInput = { instruction: brief, "$model": "gpt-image" };
console.log((await call("POST", "/estimate", writeInput)).hold_credits);
console.log((await call("POST", "/estimate", paintInput)).hold_credits);
writeInput := map[string]any{
"task": "plan",
"guide": guide, // the full text of /plan-prompt.js
"idea": "the silence after somebody says quick question in a meeting",
"format": "impact",
"want": "top (top line, at most 70 characters, optional); bottom (bottom line, at most 70 characters, optional)",
"tone": "Deadpan - flat, understated, no exclamation marks",
}
paintInput := map[string]any{"instruction": brief, "$model": "gpt-image"}
est, _ := call("POST", "/estimate", writeInput)
fmt.Println(est["hold_credits"])
est2, _ := call("POST", "/estimate", paintInput)
fmt.Println(est2["hold_credits"])
String writeInput = """
{"task":"plan","guide":"<the full text of /plan-prompt.js>",
"idea":"the silence after somebody says quick question in a meeting",
"format":"impact",
"want":"top (top line, at most 70 characters, optional); bottom (bottom line, at most 70 characters, optional)",
"tone":"Deadpan - flat, understated, no exclamation marks"}
""";
String paintInput = "{\"instruction\":\"Original artwork...\",\"$model\":\"gpt-image\"}";
System.out.println(MemeClient.call("POST", "/estimate", writeInput));
System.out.println(MemeClient.call("POST", "/estimate", paintInput));
write_input = {
"task" => "plan",
"guide" => GUIDE, # the full text of /plan-prompt.js
"idea" => "the silence after somebody says quick question in a meeting",
"format" => "impact",
"want" => "top (top line, at most 70 characters, optional); bottom (bottom line, at most 70 characters, optional)",
"tone" => "Deadpan - flat, understated, no exclamation marks"
}
paint_input = { "instruction" => brief, "$model" => "gpt-image" }
puts call("POST", "/estimate", write_input)["hold_credits"]
puts call("POST", "/estimate", paint_input)["hold_credits"]
$writeInput = [
"task" => "plan",
"guide" => $guide, // the full text of /plan-prompt.js
"idea" => "the silence after somebody says quick question in a meeting",
"format" => "impact",
"want" => "top (top line, at most 70 characters, optional); bottom (bottom line, at most 70 characters, optional)",
"tone" => "Deadpan - flat, understated, no exclamation marks",
];
$paintInput = ["instruction" => $brief, "\$model" => "gpt-image"];
echo call("POST", "/estimate", $writeInput)["hold_credits"], "\n";
echo call("POST", "/estimate", $paintInput)["hold_credits"], "\n";
var writeInput = new Dictionary<string, object> {
["task"] = "plan",
["guide"] = guide, // the full text of /plan-prompt.js
["idea"] = "the silence after somebody says quick question in a meeting",
["format"] = "impact",
["want"] = "top (top line, at most 70 characters, optional); bottom (bottom line, at most 70 characters, optional)",
["tone"] = "Deadpan - flat, understated, no exclamation marks",
};
var paintInput = new Dictionary<string, object> {
["instruction"] = brief, ["$model"] = "gpt-image",
};
var est = await MemeClient.Call(HttpMethod.Post, "/estimate", writeInput);
Console.WriteLine(est.GetProperty("hold_credits"));
4. The writing run
Submit, then poll /jobs/{id} until status is succeeded or failed. The reply text is at output.output.
JOB=$(call POST /run "$WRITE_INPUT" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until terminal. succeeded | failed.
until [ "$(call GET /jobs/$JOB | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["status"])')" != "running" ]; do
sleep 2
done
call GET /jobs/$JOB
# The reply text is at output.output. It is ONE JSON object; see the contract below.
job = call("POST", "/run", write_input)["job_id"]
while True:
j = call("GET", "/jobs/" + job)
if j["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if j["status"] != "succeeded":
# A failed job carries `error` as a plain STRING as often as an object. Read both.
err = j.get("error")
raise RuntimeError(err if isinstance(err, str) else (err or {}).get("message", "run failed"))
plan = json.loads(j["output"]["output"]) # the reply text lives at output.output
print(plan["lines"], plan["scene"])
const { job_id } = await call("POST", "/run", writeInput);
let job;
for (;;) {
job = await call("GET", "/jobs/" + job_id);
if (job.status === "succeeded" || job.status === "failed") break;
await new Promise((r) => setTimeout(r, 2000));
}
if (job.status !== "succeeded") {
// A failed job carries `error` as a plain string as often as an object.
throw new Error(typeof job.error === "string" ? job.error : job.error?.message);
}
const plan = JSON.parse(job.output.output);
console.log(plan.lines, plan.scene);
started, _ := call("POST", "/run", writeInput)
jobID := started["job_id"].(string)
var job map[string]any
for {
job, _ = call("GET", "/jobs/"+jobID, nil)
s := job["status"].(string)
if s == "succeeded" || s == "failed" {
break
}
time.Sleep(2 * time.Second)
}
out := job["output"].(map[string]any)
var plan map[string]any
json.Unmarshal([]byte(out["output"].(string)), &plan)
fmt.Println(plan["scene"])
String started = MemeClient.call("POST", "/run", writeInput);
// pull job_id out of `started` with your JSON library, then poll:
String job;
do {
Thread.sleep(2000);
job = MemeClient.call("GET", "/jobs/" + jobId, null);
} while (job.contains("\"status\":\"running\"") || job.contains("\"status\":\"queued\""));
// The reply text is at data.output.output and is one JSON object.
System.out.println(job);
job_id = call("POST", "/run", write_input)["job_id"]
loop do
@job = call("GET", "/jobs/#{job_id}")
break if %w[succeeded failed].include?(@job["status"])
sleep 2
end
raise @job["error"].to_s unless @job["status"] == "succeeded"
plan = JSON.parse(@job["output"]["output"])
puts plan["scene"]
$jobId = call("POST", "/run", $writeInput)["job_id"];
do {
sleep(2);
$job = call("GET", "/jobs/" . $jobId);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
if ($job["status"] !== "succeeded") {
// `error` is a plain string as often as an object.
throw new RuntimeException(is_string($job["error"]) ? $job["error"] : json_encode($job["error"]));
}
$plan = json_decode($job["output"]["output"], true);
echo $plan["scene"], "\n";
var started = await MemeClient.Call(HttpMethod.Post, "/run", writeInput);
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true) {
job = await MemeClient.Call(HttpMethod.Get, "/jobs/" + jobId);
var s = job.GetProperty("status").GetString();
if (s == "succeeded" || s == "failed") break;
await Task.Delay(2000);
}
var text = job.GetProperty("output").GetProperty("output").GetString();
var plan = JsonDocument.Parse(text).RootElement;
Console.WriteLine(plan.GetProperty("scene").GetString());
5. The painting run
Same submit-and-poll, different payload and a very different output field.
# EXACTLY two fields. Any third key is concatenated into the text the renderer
# sees and gets painted into the picture as literal words.
JOB=$(call POST /run '{"instruction":"Original artwork...","$model":"gpt-image"}' \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')
call GET /jobs/$JOB | python3 -c '
import base64, json, sys
job = json.load(sys.stdin)["data"]
img = job["output"]["images"][0] # NOT output.output - that is the empty string
open("plate.png", "wb").write(base64.b64decode(img["b64"]))
print(img["content_type"], job["charged_credits"])'
import base64
# EXACTLY two fields. Any third key is concatenated into the text the renderer sees.
job_id = call("POST", "/run", {"instruction": brief, "$model": "gpt-image"})["job_id"]
while True:
j = call("GET", "/jobs/" + job_id)
if j["status"] in ("succeeded", "failed"):
break
time.sleep(2)
img = j["output"]["images"][0] # NOT output.output - that is the empty string here
open("plate.png", "wb").write(base64.b64decode(img["b64"]))
print(img.get("content_type"), j["charged_credits"])
// EXACTLY two fields. Any third key is concatenated into the text the renderer sees.
const { job_id } = await call("POST", "/run", { instruction: brief, "$model": "gpt-image" });
let job;
for (;;) {
job = await call("GET", "/jobs/" + job_id);
if (job.status === "succeeded" || job.status === "failed") break;
await new Promise((r) => setTimeout(r, 2000));
}
const img = job.output.images[0]; // NOT output.output - that is "" on an image run
const dataUri = "data:" + img.content_type + ";base64," + img.b64;
// EXACTLY two fields. Any third key is concatenated into the renderer's prompt.
started, _ := call("POST", "/run", map[string]any{
"instruction": brief, "$model": "gpt-image",
})
jobID := started["job_id"].(string)
// ... poll as above ...
out := job["output"].(map[string]any)
images := out["images"].([]any)
img := images[0].(map[string]any)
raw, _ := base64.StdEncoding.DecodeString(img["b64"].(string))
os.WriteFile("plate.png", raw, 0o644)
// EXACTLY two fields. Any third key is concatenated into the renderer's prompt.
String paintInput = "{\"instruction\":\"Original artwork...\",\"$model\":\"gpt-image\"}";
String started = MemeClient.call("POST", "/run", paintInput);
// ... poll as above, then read data.output.images[0].b64 (NOT data.output.output) ...
byte[] png = Base64.getDecoder().decode(b64);
Files.write(Path.of("plate.png"), png);
require "base64"
# EXACTLY two fields. Any third key is concatenated into the renderer's prompt.
job_id = call("POST", "/run", { "instruction" => brief, "$model" => "gpt-image" })["job_id"]
loop do
@job = call("GET", "/jobs/#{job_id}")
break if %w[succeeded failed].include?(@job["status"])
sleep 2
end
img = @job["output"]["images"][0] # NOT output.output
File.binwrite("plate.png", Base64.decode64(img["b64"]))
// EXACTLY two fields. Any third key is concatenated into the renderer's prompt.
$jobId = call("POST", "/run", ["instruction" => $brief, "\$model" => "gpt-image"])["job_id"];
do {
sleep(2);
$job = call("GET", "/jobs/" . $jobId);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
$img = $job["output"]["images"][0]; // NOT output.output
file_put_contents("plate.png", base64_decode($img["b64"]));
// EXACTLY two fields. Any third key is concatenated into the renderer's prompt.
var paintInput = new Dictionary<string, object> {
["instruction"] = brief, ["$model"] = "gpt-image",
};
var started = await MemeClient.Call(HttpMethod.Post, "/run", paintInput);
// ... poll as above ...
var img = job.GetProperty("output").GetProperty("images")[0];
File.WriteAllBytes("plate.png", Convert.FromBase64String(img.GetProperty("b64").GetString()));
The writing run's output contract
The reply is one JSON object. The app parses it with a walker that strips a markdown fence if one is present and, if the run hit its output cap mid-string, discards the trailing fragment and closes the open brackets — so a truncated reply still yields whatever parsed. If you build your own client, do the same; models truncate.
{
"title": "Quick Question, Instant Silence",
"lines": [
{"slot": "top", "text": "I open with quick question."},
{"slot": "bottom", "text": "The room goes quiet for exactly as long as it needs to."}
],
"alts": [
{"label": "flatter", "lines": [{"slot": "top", "text": "…"}, {"slot": "bottom", "text": "…"}]},
{"label": "specific", "lines": [{"slot": "top", "text": "…"}, {"slot": "bottom", "text": "…"}]}
],
"scene": "A cramped video-call grid of six coworkers frozen mid-blink…",
"why": "Everyone recognizes the exact length of a silence nobody will admit they're counting."
}
A refusal comes back instead as {"refused": true, "reason": "…"} and carries
nothing else. The app refuses the same five categories in the browser before the run is made —
harassment and pile-ons, contempt aimed at a group for what they are, a meme aimed at a real
named private person, sexual content, and anything sexual, suggestive or hostile involving a
child — so in practice the model's refusal is the second line, not the first.
Slot ids not belonging to the requested format are dropped. Captions over their limit are cut and flagged. Both are reported back to the user rather than absorbed silently.
The image run's input contract
Exactly two fields. instruction and $model. Every
additional key is concatenated into the text the renderer sees and painted into the picture as
literal words — a format field will get you a picture with the word "impact" in it.
$files is rejected outright on an image run, so there is no image-in / image-out
single call; anything that has to read a picture needs a text model with $files to
write a brief first.
Output is at output.images[0] as {content_type, b64}.
output.output is the empty string on an image run, and reading it is the first
mistake a text-lane habit produces here.
Pricing
Two runs, priced differently. The text run is priced per token and its hold moves with the length of the guide and the idea. The image run is priced per image out of a separate rate block, and its hold does not move with prompt length at all — so one estimate covers every brief you will ever send.
Quote the hold, never a settled price. The settled charge on an image run
varies by several times from picture to picture on the same alias with the same hold, so any
number written down here would be wrong for most runs. /estimate is free, makes no
job, and echoes back model, model_alias, markup_bps,
hold_credits and min_credits.
6. Streaming the writing run
Server-sent events. Frames are separated by a blank line; within a frame an event: line names the event and a data: line carries JSON. The event names are job, delta, done, pending and error. There is no {"type":"delta"} envelope, and a parser written against one never fires.
curl -N -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data "$WRITE_INPUT"
# Frames are separated by a BLANK LINE, and each frame is an `event:` line followed
# by a `data:` line. Event names: job, delta, done, pending, error.
# There is no {"type":"delta"} envelope - a parser written against that never fires.
#
# Note also: the browser SDK is served ticks rather than text deltas on this endpoint,
# so an in-page onDelta callback does not fire. Server-side clients get real deltas.
req = urllib.request.Request(API + "/run-stream", data=json.dumps(write_input).encode(),
method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
name, payload = None, None
with urllib.request.urlopen(req) as res:
for raw in res: # frames end at a BLANK line
line = raw.decode().rstrip("\n")
if line.startswith("event:"):
name = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
elif line == "" and name:
if name == "delta":
print(payload["text"], end="")
elif name == "done":
print("\n", payload["charged_credits"])
name, payload = None, None
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: { "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json" },
body: JSON.stringify(writeInput),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const frames = buf.split("\n\n"); // a BLANK line ends a frame
buf = frames.pop();
for (const frame of frames) {
let name = "", data = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) name = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (name === "delta") process.stdout.write(JSON.parse(data).text);
}
}
req, _ := http.NewRequest("POST", api+"/run-stream", bytes.NewReader(bodyJSON))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var name, data string
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
name = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
data = strings.TrimSpace(line[5:])
case line == "" && name != "":
fmt.Println(name, data) // a BLANK line ends the frame
name, data = "", ""
}
}
HttpRequest req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(writeInput))
.build();
String name = null, data = null;
for (String line : HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().toList()) {
if (line.startsWith("event:")) name = line.substring(6).trim();
else if (line.startsWith("data:")) data = line.substring(5).trim();
else if (line.isEmpty() && name != null) { // a BLANK line ends the frame
System.out.println(name + " " + data);
name = null; data = null;
}
}
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(write_input)
name = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event:") then name = line[6..].strip
elsif line.start_with?("data:") && name == "delta"
print JSON.parse(line[5..].strip)["text"]
elsif line.empty? then name = nil # a BLANK line ends the frame
end
end
end
end
end
$ch = curl_init(API . "/run-stream");
$name = null;
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($writeInput),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$name) {
foreach (explode("\n", $chunk) as $line) {
$line = rtrim($line);
if (str_starts_with($line, "event:")) {
$name = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:") && $name === "delta") {
echo json_decode(trim(substr($line, 5)), true)["text"];
} elseif ($line === "") {
$name = null; // a BLANK line ends the frame
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Content = new StringContent(JsonSerializer.Serialize(writeInput),
Encoding.UTF8, "application/json");
using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string name = null, data = null;
while (await reader.ReadLineAsync() is string line) {
if (line.StartsWith("event:")) name = line[6..].Trim();
else if (line.StartsWith("data:")) data = line[5..].Trim();
else if (line.Length == 0 && name != null) { // a BLANK line ends the frame
Console.WriteLine($"{name} {data}");
name = null; data = null;
}
}
A frame on the wire looks like this:
event: job
data: {"job_id":"job_..."}
event: delta
data: {"text":"{\"title\":\"Quick"}
event: done
data: {"status":"succeeded","output":{"output":"..."},"charged_credits":41}
The content guard does not run here
The app refuses five categories of request — harassment and pile-ons, contempt aimed at a group for what they are, a meme aimed at a real named individual, sexual content, and anything sexual, suggestive or hostile involving a child. That guard is client-side. It runs in the page, before any estimate or hold, and it is described that way rather than as a property of the service.
Calling this API directly puts you outside it. What still applies is the app's own system prompt, which refuses the same things at the model — a different check, and not a substitute for the first. If you are building a client on top of this app, port the guard or write your own; do not assume the endpoint enforces one. The rules the guard implements are set out in llms.txt, and its source ships at /guard.js.
The writing guide, verbatim
The guide field is not a summary — it is the whole instruction set, served at
/plan-prompt.js and readable by anybody. Fetch it, send it, or
write your own; the run has no hidden half.
Idempotency
Pass an Idempotency-Key header on every /run. One caution learned
the expensive way: an idempotent replay returns the original job even when that job
failed. A key that repeats after a restart echoes the old failure back and no new job
appears. Salt the key with something that changes per process, not only with a hash of the
input.
Getting a token
The token page shows the token this browser already holds, whose
session it is, and how to replace it — no DevTools needed. A guest token is minted by
POST /guest with {"slug": "meme-generator"} and carries no balance,
so it can read but not run.