Everything the buttons do, from a script.
Face Generator is a SkillSafe app, so everything the page does is a plain HTTPS call you can make yourself. Base URL:
https://api.skillsafe.ai/v1/app-api
Every response is an envelope. Success is {"ok": true, "data": {...}};
failure is {"ok": false, "error": {"code": "...", "message": "...", "details": {...}}}.
Read data, never the top level.
| HTTP | code | What it means here |
|---|---|---|
| 401 | unauthorized | No token, or it expired. Take a new one from tokens.html. |
| 403 | forbidden | A guest token tried to run. Runs need a signed-in account unless the publisher sponsors guests. |
| 402 | payment_required | Balance below the hold. Image runs hold per image — a six-face batch is six holds. |
| 400 | validation_error | The input shape is wrong. For a portrait run the usual cause is a missing $model. |
| 429 | rate_limited | Back off. Collection queries allow 120/min, similarity 30/min. |
| 503 | unavailable | The upstream model is down. Retry with the same idempotency key — it will not double-bill. |
Face Generator has one system prompt and two input shapes. Which one you send decides which
lane runs — there is no task router on the portrait side, because an image
run and a text run are already different calls.
| Lane | Send | Get back |
|---|---|---|
| portrait (image) | instruction — the compiled brief$model — gpt-image |
output.images[0].b64 and output.images[0].content_type. output.output is empty. |
| cast (text) | task: "cast", project, count, tone, cast_instructions |
output.output — one JSON object, schema in the instruction set. |
The $model override is the whole image lane. Without it a run
goes to the app's own text model and you get prose describing a portrait instead of a
portrait. With it, the run is priced per image rather than per token.
Keep the portrait input to those two fields. Anything else you add is concatenated into the text the image model sees and tends to get painted into the picture as literal words. That is also why this app's system prompt is four lines long.
One run is one image. A batch of six is six runs. Send them a couple at a time rather than all at once.
Open tokens.html in the browser, sign in, and copy the
shell export. Every call below sends it as Authorization: Bearer <token>.
The token is scoped to this app and can spend your credits — treat it as a
password.
/me returns exactly three fields: subject_type,
subject_id and credits. There is no email or name, so
“signed in?” is subject_type === "user". Credits are hundredths
of a cent: 10,000 credits is $1.00.
curl -s "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $FACE_FORGE_TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://face-generator.skillsafe.ai/tokens.html
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
print(call("GET", "/me"))
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
async function call(method, path, body) {
const res = await fetch("https://api.skillsafe.ai/v1/app-api" + path, {
method,
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error && json.error.message);
return json.data;
}
console.log(await call("GET", "/me"));
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
func call(method, path string, body []byte) string {
var r io.Reader
if body != nil {
r = bytes.NewReader(body)
}
req, _ := http.NewRequest(method, "https://api.skillsafe.ai/v1/app-api"+path, r)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
return string(out)
}
func main() {
fmt.Println(call("GET", "/me", nil))
}
import java.net.URI;
import java.net.http.*;
class FaceGenerator {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
var pub = body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub).build();
return HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] a) throws Exception {
System.out.println(call("GET", "/me", null));
}
}
require 'json'
require 'net/http'
TOKEN = 'YOUR_TOKEN' # from /tokens.html
BASE = 'https://api.skillsafe.ai/v1/app-api'
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = Net::HTTP.const_get(method.capitalize)
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) }
JSON.parse(res.body)['data']
end
pp call('GET', '/me')
<?php
$token = 'YOUR_TOKEN'; // from /tokens.html
$base = 'https://api.skillsafe.ai/v1/app-api';
function call($method, $path, $body = null) {
global $token, $base;
$opts = ["http" => [
"method" => $method,
"header" => "Authorization: Bearer $token\r\nContent-Type: application/json\r\n",
"content" => $body === null ? null : json_encode($body),
"ignore_errors" => true,
]];
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
return json_decode($raw, true)["data"];
}
print_r(call('GET', '/me'));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN"; // from /tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(HttpMethod method, string path, string body = null) {
var req = new HttpRequestMessage(method, Base + path);
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
Console.WriteLine(await Call(HttpMethod.Get, "/me"));
Free, and it starts no job. On an image model the hold comes back per image and does not vary with the length of the brief, so one probe covers every portrait you will ever send to that renderer. Multiply by your batch size yourself.
Measured on this app: gpt-image holds 2,652 credits
(about $0.27) per image and a real render settled at 96 credits (about
$0.01). The hold is deliberately far above the settled cost, so quote the hold as
reserved and never as the price. Read the live numbers rather than trusting
these.
flux-klein also appears in GET /v1/models with
available: true, and /estimate quotes it at 30 credits an
image. Every actual run against it on this deployment fails with
error_code: "internal", so this app does not offer it. A clean estimate is
not evidence a model runs.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $FACE_FORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"instruction": "pricing probe", "$model": "gpt-image"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://face-generator.skillsafe.ai/tokens.html
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
body = {
"instruction": "pricing probe",
"$model": "gpt-image"
}
print(call("POST", "/estimate", body))
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
async function call(method, path, body) {
const res = await fetch("https://api.skillsafe.ai/v1/app-api" + path, {
method,
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error && json.error.message);
return json.data;
}
const body = {
"instruction": "pricing probe",
"$model": "gpt-image"
};
console.log(await call("POST", "/estimate", body));
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
func call(method, path string, body []byte) string {
var r io.Reader
if body != nil {
r = bytes.NewReader(body)
}
req, _ := http.NewRequest(method, "https://api.skillsafe.ai/v1/app-api"+path, r)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
return string(out)
}
func main() {
body := []byte(`{"instruction": "pricing probe", "$model": "gpt-image"}`)
fmt.Println(call("POST", "/estimate", body))
}
import java.net.URI;
import java.net.http.*;
class FaceGenerator {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
var pub = body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub).build();
return HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] a) throws Exception {
String body = "{\"instruction\": \"pricing probe\", \"$model\": \"gpt-image\"}";
System.out.println(call("POST", "/estimate", body));
}
}
require 'json'
require 'net/http'
TOKEN = 'YOUR_TOKEN' # from /tokens.html
BASE = 'https://api.skillsafe.ai/v1/app-api'
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = Net::HTTP.const_get(method.capitalize)
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) }
JSON.parse(res.body)['data']
end
body = JSON.parse(<<~JSON)
{
"instruction": "pricing probe",
"$model": "gpt-image"
}
JSON
pp call('POST', '/estimate', body)
<?php
$token = 'YOUR_TOKEN'; // from /tokens.html
$base = 'https://api.skillsafe.ai/v1/app-api';
function call($method, $path, $body = null) {
global $token, $base;
$opts = ["http" => [
"method" => $method,
"header" => "Authorization: Bearer $token\r\nContent-Type: application/json\r\n",
"content" => $body === null ? null : json_encode($body),
"ignore_errors" => true,
]];
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
return json_decode($raw, true)["data"];
}
$body = json_decode(<<<'JSON'
{
"instruction": "pricing probe",
"$model": "gpt-image"
}
JSON, true);
print_r(call('POST', '/estimate', $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN"; // from /tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(HttpMethod method, string path, string body = null) {
var req = new HttpRequestMessage(method, Base + path);
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
var body = @"{""instruction"": ""pricing probe"", ""$model"": ""gpt-image""}";
Console.WriteLine(await Call(HttpMethod.Post, "/estimate", body));
Returns {"job_id": "..."} immediately. Pass an
Idempotency-Key header (or idempotency_key in the body) so a
network retry cannot double-bill — but vary it per logical attempt, because an
idempotent replay returns the original job even when that job failed.
Twenty to forty seconds per image on gpt-image. The brief below is
exactly what the app's compiler produces. Note the opening clause:
every brief this app sends declares the subject fictional, and the app will not send one
that asks for the likeness of a real person.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $FACE_FORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"instruction": "Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only.", "$model": "gpt-image"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://face-generator.skillsafe.ai/tokens.html
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
body = {
"instruction": "Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only.",
"$model": "gpt-image"
}
print(call("POST", "/run", body))
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
async function call(method, path, body) {
const res = await fetch("https://api.skillsafe.ai/v1/app-api" + path, {
method,
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error && json.error.message);
return json.data;
}
const body = {
"instruction": "Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only.",
"$model": "gpt-image"
};
console.log(await call("POST", "/run", body));
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
func call(method, path string, body []byte) string {
var r io.Reader
if body != nil {
r = bytes.NewReader(body)
}
req, _ := http.NewRequest(method, "https://api.skillsafe.ai/v1/app-api"+path, r)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
return string(out)
}
func main() {
body := []byte(`{"instruction": "Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only.", "$model": "gpt-image"}`)
fmt.Println(call("POST", "/run", body))
}
import java.net.URI;
import java.net.http.*;
class FaceGenerator {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
var pub = body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub).build();
return HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] a) throws Exception {
String body = "{\"instruction\": \"Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only.\", \"$model\": \"gpt-image\"}";
System.out.println(call("POST", "/run", body));
}
}
require 'json'
require 'net/http'
TOKEN = 'YOUR_TOKEN' # from /tokens.html
BASE = 'https://api.skillsafe.ai/v1/app-api'
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = Net::HTTP.const_get(method.capitalize)
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) }
JSON.parse(res.body)['data']
end
body = JSON.parse(<<~JSON)
{
"instruction": "Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only.",
"$model": "gpt-image"
}
JSON
pp call('POST', '/run', body)
<?php
$token = 'YOUR_TOKEN'; // from /tokens.html
$base = 'https://api.skillsafe.ai/v1/app-api';
function call($method, $path, $body = null) {
global $token, $base;
$opts = ["http" => [
"method" => $method,
"header" => "Authorization: Bearer $token\r\nContent-Type: application/json\r\n",
"content" => $body === null ? null : json_encode($body),
"ignore_errors" => true,
]];
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
return json_decode($raw, true)["data"];
}
$body = json_decode(<<<'JSON'
{
"instruction": "Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only.",
"$model": "gpt-image"
}
JSON, true);
print_r(call('POST', '/run', $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN"; // from /tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(HttpMethod method, string path, string body = null) {
var req = new HttpRequestMessage(method, Base + path);
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
var body = @"{""instruction"": ""Photorealistic portrait photograph of a completely fictional person who does not exist. An invented face, not a likeness or resemblance of any real, living or historical individual. Subject: an older adult in their late sixties, feminine presenting, East Asian features. Distinguishing detail: a wide philtrum. Expression: a calm neutral expression, lips closed. Framing: a head-and-shoulders crop, shot on an 85mm lens at f/1.8, compressed perspective and shallow depth of field. Lighting: Rembrandt lighting, a small lit triangle on the shadow-side cheek. Background: a seamless mid-grey studio backdrop. Treatment: black and white photography, rich mid-tones, no colour anywhere in the frame. Natural skin with visible pores, fine lines and asymmetry; catchlights in both eyes; anatomically correct ears, teeth and hands; sharp focus on the nearer eye. No text, letters, numbers, captions, watermarks, logos, signatures, borders, frames, collage panels, split screens or duplicated faces anywhere in the image. One person only."", ""$model"": ""gpt-image""}";
Console.WriteLine(await Call(HttpMethod.Post, "/run", body));
Poll until status is succeeded or failed.
Twenty to forty seconds is typical. A failed run reports
charged_credits: null — you are not billed for it.
Then read data.output.images[0].b64 — base64 image bytes, with the
MIME type in data.output.images[0].content_type. Do not look in
data.output.output; it is empty for image runs, and reading it is the first
mistake every text-lane habit produces here. data.charged_credits is what
you actually paid, usually below the hold.
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID" \
-H "Authorization: Bearer $FACE_FORGE_TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://face-generator.skillsafe.ai/tokens.html
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
print(call("GET", "/jobs/JOB_ID"))
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
async function call(method, path, body) {
const res = await fetch("https://api.skillsafe.ai/v1/app-api" + path, {
method,
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error && json.error.message);
return json.data;
}
console.log(await call("GET", "/jobs/JOB_ID"));
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
func call(method, path string, body []byte) string {
var r io.Reader
if body != nil {
r = bytes.NewReader(body)
}
req, _ := http.NewRequest(method, "https://api.skillsafe.ai/v1/app-api"+path, r)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
return string(out)
}
func main() {
fmt.Println(call("GET", "/jobs/JOB_ID", nil))
}
import java.net.URI;
import java.net.http.*;
class FaceGenerator {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
var pub = body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub).build();
return HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] a) throws Exception {
System.out.println(call("GET", "/jobs/JOB_ID", null));
}
}
require 'json'
require 'net/http'
TOKEN = 'YOUR_TOKEN' # from /tokens.html
BASE = 'https://api.skillsafe.ai/v1/app-api'
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = Net::HTTP.const_get(method.capitalize)
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) }
JSON.parse(res.body)['data']
end
pp call('GET', '/jobs/JOB_ID')
<?php
$token = 'YOUR_TOKEN'; // from /tokens.html
$base = 'https://api.skillsafe.ai/v1/app-api';
function call($method, $path, $body = null) {
global $token, $base;
$opts = ["http" => [
"method" => $method,
"header" => "Authorization: Bearer $token\r\nContent-Type: application/json\r\n",
"content" => $body === null ? null : json_encode($body),
"ignore_errors" => true,
]];
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
return json_decode($raw, true)["data"];
}
print_r(call('GET', '/jobs/JOB_ID'));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN"; // from /tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(HttpMethod method, string path, string body = null) {
var req = new HttpRequestMessage(method, Base + path);
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
Console.WriteLine(await Call(HttpMethod.Get, "/jobs/JOB_ID"));
The cast lane is an ordinary text run on the app's model. Send
cast_instructions — fetch it from
/cast-prompt.js, which builds it from the same controlled
vocabulary the portrait form uses, so the attributes it returns are always renderable.
Use /run-stream for Server-Sent Events (delta,
job, done), or /run plus polling as above. The
reply is one JSON object; each character carries an attributes block you can
feed straight back into a portrait run.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $FACE_FORGE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "cast", "project": "A six-part documentary about a fishing port losing its fleet.", "count": 5, "tone": "documentary", "cast_instructions": "<the full text served at /cast-prompt.js>"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://face-generator.skillsafe.ai/tokens.html
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
body = {
"task": "cast",
"project": "A six-part documentary about a fishing port losing its fleet.",
"count": 5,
"tone": "documentary",
"cast_instructions": "<the full text served at /cast-prompt.js>"
}
print(call("POST", "/run-stream", body))
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
async function call(method, path, body) {
const res = await fetch("https://api.skillsafe.ai/v1/app-api" + path, {
method,
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error && json.error.message);
return json.data;
}
const body = {
"task": "cast",
"project": "A six-part documentary about a fishing port losing its fleet.",
"count": 5,
"tone": "documentary",
"cast_instructions": "<the full text served at /cast-prompt.js>"
};
console.log(await call("POST", "/run-stream", body));
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
func call(method, path string, body []byte) string {
var r io.Reader
if body != nil {
r = bytes.NewReader(body)
}
req, _ := http.NewRequest(method, "https://api.skillsafe.ai/v1/app-api"+path, r)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
return string(out)
}
func main() {
body := []byte(`{"task": "cast", "project": "A six-part documentary about a fishing port losing its fleet.", "count": 5, "tone": "documentary", "cast_instructions": "<the full text served at /cast-prompt.js>"}`)
fmt.Println(call("POST", "/run-stream", body))
}
import java.net.URI;
import java.net.http.*;
class FaceGenerator {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
var pub = body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub).build();
return HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] a) throws Exception {
String body = "{\"task\": \"cast\", \"project\": \"A six-part documentary about a fishing port losing its fleet.\", \"count\": 5, \"tone\": \"documentary\", \"cast_instructions\": \"<the full text served at /cast-prompt.js>\"}";
System.out.println(call("POST", "/run-stream", body));
}
}
require 'json'
require 'net/http'
TOKEN = 'YOUR_TOKEN' # from /tokens.html
BASE = 'https://api.skillsafe.ai/v1/app-api'
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = Net::HTTP.const_get(method.capitalize)
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) }
JSON.parse(res.body)['data']
end
body = JSON.parse(<<~JSON)
{
"task": "cast",
"project": "A six-part documentary about a fishing port losing its fleet.",
"count": 5,
"tone": "documentary",
"cast_instructions": "<the full text served at /cast-prompt.js>"
}
JSON
pp call('POST', '/run-stream', body)
<?php
$token = 'YOUR_TOKEN'; // from /tokens.html
$base = 'https://api.skillsafe.ai/v1/app-api';
function call($method, $path, $body = null) {
global $token, $base;
$opts = ["http" => [
"method" => $method,
"header" => "Authorization: Bearer $token\r\nContent-Type: application/json\r\n",
"content" => $body === null ? null : json_encode($body),
"ignore_errors" => true,
]];
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
return json_decode($raw, true)["data"];
}
$body = json_decode(<<<'JSON'
{
"task": "cast",
"project": "A six-part documentary about a fishing port losing its fleet.",
"count": 5,
"tone": "documentary",
"cast_instructions": "<the full text served at /cast-prompt.js>"
}
JSON, true);
print_r(call('POST', '/run-stream', $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN"; // from /tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(HttpMethod method, string path, string body = null) {
var req = new HttpRequestMessage(method, Base + path);
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
var body = @"{""task"": ""cast"", ""project"": ""A six-part documentary about a fishing port losing its fleet."", ""count"": 5, ""tone"": ""documentary"", ""cast_instructions"": ""<the full text served at /cast-prompt.js>""}";
Console.WriteLine(await Call(HttpMethod.Post, "/run-stream", body));
Each character's attributes uses the app's controlled vocabulary. To render
one, compile a brief from those values — the app does it in
brief.js, and the fixed opening and closing clauses are exported as
FaceBrief.FICTIONAL_LEAD and FaceBrief.NEGATIVES. If you build the
text yourself, keep both: the first is what makes the subject fictional, the second is what
keeps captions and watermarks out of the frame.
Keep briefs inside the renderer's input cap — about 12,000 characters for GPT Image 2. Past the cap the tail is dropped silently, and the tail is where the negative constraints live.
The likeness guard runs in the browser, not on the server, so a script can send a brief the app itself would refuse. Do not. Face Generator exists to make faces of people who do not exist; generating a synthetic likeness of a real individual is outside what this app is for, and the guard's rules are readable in /guard.js if you want to apply them on your side.