Drive Pumpkin Stencil from your own code
The one metered lane, design, is available over HTTP. It takes an idea for a jack-o'-lantern - who or what goes on the pumpkin - with the mood, who is carving, whether kids help and the pumpkin's size, and returns a face built only from the app's fixed drawer of parts: eyes, brows, nose, mouth and up to three extra motifs, each cut through or shaved. It says whether the idea was kept as-asked, simplified (lettering, a realistic look or a whole scene cut down to one bold element) or swapped (a logo, a real person or a studio's character replaced by an original face), and adds a carving order and tips. With tweak and current it changes an existing design instead.
The division of labour first: the model picks parts; the page draws, measures and prints them. stencilkit.js runs in node as well as in the browser. It turns a design into polygons, measures the thinnest bridge of pumpkin between two holes and the finest stroke at the pumpkin's real size, gives a difficulty and a rough carving time (estimates), tiles a real-size print, and its check function holds each reply to the brief: drawer values, the carver's limits, a step for the kids, numbers and pronouns not in the idea, and prose that names a part the design does not have.
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"pumpkin-stencil"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, idea) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the lane is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field and the fit
Every run sends task: "design". The reply's fit is decided by three ordered rules - the first that applies wins:
| Order | Fit | When |
|---|---|---|
| 1 | swapped | The idea asks for a real person's face, a brand, company or team logo, or a character owned by a studio, publisher or game company. The reply designs an original face in the same spirit and never names what it replaced. |
| 2 | simplified | The drawer cannot draw it as asked: lettering or names, a realistic or shaded look, a whole scene or more than one character, details the parts cannot show. One element is kept. |
| 3 | as-asked | Otherwise. |
The carver's limits: first-time - at most 6 cut pieces (each eye and brow counts), nothing shaved, no pinned pupils, one extra, no stitched, zigzag, fangs or scream mouth; some-practice - at most 9 cut and 2 shaved pieces, no pinned pupils, two extras; confident - anything, three extras.
The declared input schema carries scalars only, so the object-valued current travels JSON-encoded as a string (StencilKit.toWire does this). Every value is a string.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | Always design. Required. |
idea | string | Who or what goes on the pumpkin. Past 600 characters the middle is left out with a marker. Required. |
mood | string | friendly, silly, spooky or scary. |
skill | string | first-time, some-practice or confident. |
crew | string | solo or with-kids (a grown-up does every cut; a step says what the kids do). |
size | string | small, medium or large - about 5, 7 or 9 inches of carvable face. |
tweak, current | string, string (JSON) | Optional, together: a change to make, and the design to change (same shape as the reply's design). Empty otherwise. |
page_guess | string | The page's keyword guess at fit - a hint the model may overrule. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object: task, fit, fit_note, title, concept, design, steps (3-7, in carving order), tips (2-3) and reads_as. The design is {eyes: {shape, size, tilt, pupils}, brows: {shape, mode}, nose: {shape, mode}, mouth: {shape, teeth}, extras: [{motif, spot, mode}]} with values only from the drawer:
| Part | Values |
|---|---|
| eyes.shape | triangle round almond crescent slit star heart diamond cross; size s m l; tilt level angry sad wink; pupils none pinned |
| brows.shape | none flat angry worried arched; mode cut shave |
| nose.shape | none triangle round heart skull button; mode cut shave |
| mouth.shape | grin smile open zigzag stitched fangs frown smirk scream wavy; teeth 0-6 (grin), 3-6 (zigzag), 0-2 (scream), else 0 |
| extras | up to 3; motif bat moon star ghost spider web hat bone heart leaf whiskers freckles tear scar candy; spot forehead top-left top-right left-cheek right-cheek chin; mode cut shave |
The page replaces any value outside the drawer and says so. Left and right are as you face the pumpkin.
Worked examples
Replies produced against the app's prompt for the four sample runs over three made-up ideas (long fields shortened; the full replies ship in example-result.js). Together they cover every fit and the change-a-design loop.
design - a friendly ghost for a toddler's porch: as-asked
Input:
{
"task": "design",
"idea": "A friendly little ghost for our toddler's first Halloween, to sit on the front porch",
"mood": "friendly",
"skill": "first-time",
"crew": "with-kids",
"size": "medium",
"tweak": "",
"current": "",
"page_guess": "as-asked"
}
Reply:
{
"task": "design",
"fit": "as-asked",
"fit_note": "Kept the ghost sweet and simple: big round friendly eyes, a gentle smile, and a small ghost shape for your porch design.",
"title": "Friendly Little Ghost",
"concept": "A round-eyed, smiling ghost face sits on your front porch for your toddler's first Halloween, with a small ghost shape tucked near the chin to keep it playful and simple.",
"design": {
"eyes": {
"shape": "round",
"size": "l",
"tilt": "level",
"pupils": "none"
},
"brows": {
"shape": "none",
"mode": "cut"
},
"nose": {
"shape": "none",
"mode": "cut"
},
"mouth": {
"shape": "smile",
"teeth": 0
},
"extras": [
{
"motif": "ghost",
"spot": "chin",
"mode": "cut"
}
]
},
"steps": [
"Tape the stencil onto the clean, dry pumpkin and smooth out any bubbles.",
"Let your toddler poke holes along every line with a poking tool while you guide their hand.",
"Peel off the stencil, then have the grown-up carve the small ghost shape near the chin first.",
"... (6 steps in all)"
],
"tips": [
"A small serrated pumpkin-carving saw gives gentle control on soft curves.",
"Rub petroleum jelly on every cut edge to keep the pumpkin fresh longer.",
"Light it with an LED candle so there is no open flame near your toddler."
],
"reads_as": "From the sidewalk, two big round glowing eyes and a soft curved smile glow like a friendly little ghost, with a tiny ghost shape glimmering near the chin."
}
design - a family hayride with a last name, as realistic as possible: simplified
Input:
{
"task": "design",
"idea": "Our whole family of five and the dog riding on a hay wagon under a full moon, as realistic as possible, with our last name across the bottom",
"mood": "spooky",
"skill": "some-practice",
"crew": "solo",
"size": "large",
"tweak": "",
"current": "",
"page_guess": "simplified"
}
Reply:
{
"task": "design",
"fit": "simplified",
"fit_note": "This keeps the full moon you mentioned and builds a classic spooky jack-o'-lantern face around it; carving can't show a realistic family portrait, a dog, a hay wagon, or lettering, so those are left out.",
"title": "Full Moon Frown",
"concept": "A simple spooky jack-o'-lantern face anchored by a glowing full moon, with pointed triangle eyes and nose, a faint web tucked in the corner, and a frowning mouth.",
"design": {
"eyes": {
"shape": "triangle",
"size": "l",
"tilt": "level",
"pupils": "none"
},
"brows": {
"shape": "none",
"mode": "cut"
},
"nose": {
"shape": "triangle",
"mode": "cut"
},
"mouth": {
"shape": "frown",
"teeth": 0
},
"extras": [
{
"motif": "moon",
"spot": "top-right",
"mode": "cut"
},
{
"motif": "web",
"spot": "top-left",
"mode": "shave"
}
]
},
"steps": [
"Tape the printed stencil onto the front of the pumpkin and smooth out the wrinkles.",
"Poke small holes along every line of the pattern to transfer it onto the skin.",
"Shave the web in the top-left corner first, since it is the most delicate piece.",
"... (7 steps in all)"
],
"tips": [
"A craft knife gives cleaner triangle points than a serrated kit saw.",
"Use a small lino gouge or a vegetable peeler to shave the web so you do not cut all the way through.",
"Rub petroleum jelly on every cut edge to slow browning, and light it with an LED candle for a steady glow."
],
"reads_as": "From the sidewalk, this reads as a classic, frowning jack-o'-lantern face glowing under a full moon, with a faint web tucked in the corner."
}
design - the logo of a burger chain: swapped
Input:
{
"task": "design",
"idea": "The logo of the burger chain I work at, for the staff pumpkin contest at the restaurant",
"mood": "silly",
"skill": "confident",
"crew": "solo",
"size": "medium",
"tweak": "",
"current": "",
"page_guess": "swapped"
}
Reply:
{
"task": "design",
"fit": "swapped",
"fit_note": "Since a real company logo can't be carved from these parts, this is an all-original silly face made for your staff pumpkin contest instead.",
"title": "Staff Contest Smirk",
"concept": "An original silly face for your staff pumpkin contest, pairing a winking round eye with arched brows, a button nose and a sly smirk in place of the logo you can't carve directly.",
"design": {
"eyes": {
"shape": "round",
"size": "m",
"tilt": "wink",
"pupils": "pinned"
},
"brows": {
"shape": "arched",
"mode": "cut"
},
"nose": {
"shape": "button",
"mode": "cut"
},
"mouth": {
"shape": "smirk",
"teeth": 0
},
"extras": []
},
"steps": [
"Tape the printed stencil onto the front of the pumpkin and press out any wrinkles.",
"Poke pin holes through the paper along every line of the brows, eyes, nose and mouth to transfer the pattern onto the skin.",
"Carve the small button nose first, since it is the smallest and most central piece.",
"... (7 steps in all)"
],
"tips": [
"A craft knife or small lino gouge works well for the thin smirk line.",
"Use a serrated pumpkin-carving saw for the bigger brow pieces.",
"Rub petroleum jelly on every cut edge and light the inside with an LED candle so it glows through the contest."
],
"reads_as": "From the sidewalk it reads as a round winking eye, arched brows, a tiny button nose and a sly smirking mouth glowing in the dark."
}
design with tweak - the ghost, a bit spookier (starts from the first reply's design)
Input:
{
"task": "design",
"idea": "A friendly little ghost for our toddler's first Halloween, to sit on the front porch",
"mood": "friendly",
"skill": "first-time",
"crew": "with-kids",
"size": "medium",
"tweak": "Make it a bit spookier, but keep it easy enough for the kids",
"current": "{\"eyes\":{\"shape\":\"round\",\"size\":\"l\",\"tilt\":\"level\",\"pupils\":\"none\"},\"brows\":{\"shape\":\"none ... (235 characters)",
"page_guess": "as-asked"
}
Reply:
{
"task": "design",
"fit": "as-asked",
"fit_note": "Kept the round eyes and the ghost on the chin, and gave the mouth a spookier open shape for a bit more spark.",
"title": "Spooky Little Porch Ghost",
"concept": "A big, friendly round-eyed face with a wide, spooky-open mouth, plus a small ghost tucked on the chin, simple enough for your toddler's first Halloween pumpkin on the porch.",
"design": {
"eyes": {
"shape": "round",
"size": "l",
"tilt": "level",
"pupils": "none"
},
"brows": {
"shape": "none",
"mode": "cut"
},
"nose": {
"shape": "none",
"mode": "cut"
},
"mouth": {
"shape": "open",
"teeth": 0
},
"extras": [
{
"motif": "ghost",
"spot": "chin",
"mode": "cut"
}
]
},
"steps": [
"Tape the printed stencil onto the clean, dry pumpkin and have the kids smooth it flat.",
"Poke holes along every line with a pin to transfer the pattern onto the pumpkin skin.",
"Cut open the top and let the kids scoop out the seeds and pulp.",
"... (7 steps in all)"
],
"tips": [
"A small serrated pumpkin saw makes quick work of the eyes and mouth; the tip of a craft knife is best for the tiny ghost shape.",
"Rub a little petroleum jelly on all the cut edges afterward to help the pumpkin stay fresh longer.",
"Use a flickering LED candle instead of an open flame, especially with kids nearby."
],
"reads_as": "From the sidewalk, it reads as a big round-eyed face with a wide, startled open mouth and a tiny ghost peeking out near the chin."
}
Build the input yourself
Load the script in node (global.window = global; require("./stencilkit.js")), then:
const K = StencilKit;
const brief = K.brief({ idea: "Our grumpy cat who hates Mondays", mood: "silly", skill: "some-practice", crew: "solo", size: "medium" });
console.log(K.prescan(brief.idea)); // the page's keyword guess at the fit
const wire = K.toWire(K.buildInput(brief).input); // POST this to /estimate and /run
// then: const n = K.normalize(K.parseReply(job.output.output));
// const m = K.measure(n.design, brief.size); // bridges, fine cuts, difficulty, rough minutes
// const { notes } = K.check(n, brief); // the page's re-check of the reply
// K.stencilFile(n.design, brief.size) // a real-size stencil as an SVG file
// and to change it: K.toWire(K.buildInput(Object.assign({}, brief, { tweak: "Add a bat" }), n.design).input)
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://pumpkin-stencil.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://pumpkin-stencil.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// pumpkin-stencil.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://pumpkin-stencil.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://pumpkin-stencil.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// PumpkinStencil.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class PumpkinStencil {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://pumpkin-stencil.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://pumpkin-stencil.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://pumpkin-stencil.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://pumpkin-stencil.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"pumpkin-stencil"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "pumpkin-stencil"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "pumpkin-stencil" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "pumpkin-stencil"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "pumpkin-stencil"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "pumpkin-stencil" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "pumpkin-stencil"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "pumpkin-stencil" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "design", "idea": "A friendly little ghost for our toddler's first Halloween, to sit on the front porch", "mood": "friendly", "skill": "first-time", "crew": "with-kids", "size": "medium", "tweak": "", "current": "", "page_guess": "as-asked"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "design", "idea": "A friendly little ghost for our toddler's first Halloween, to sit on the front porch", "mood": "friendly", "skill": "first-time", "crew": "with-kids", "size": "medium", "tweak": "", "current": "", "page_guess": "as-asked"}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "design", idea, mood, skill, crew, size, ... (see the field table)}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the task and the input: a retry with the same key never bills twice. The page uses pumpkin-stencil:<task>:<inputHash>:a<attempt> (the task is always design), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="pumpkin-stencil:design:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "pumpkin-stencil:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the design as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("fit"))
import { createHash } from "node:crypto";
const key = `pumpkin-stencil:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.fit);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "pumpkin-stencil:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "pumpkin-stencil:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "pumpkin-stencil:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "pumpkin-stencil:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"pumpkin-stencil:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: pumpkin-stencil:design:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "design", "idea": "A friendly little ghost for our toddler's first Halloween, to sit on the front porch", "mood": "friendly", "skill": "first-time", "crew": "with-kids", "size": "medium", "tweak": "", "current": "", "page_guess": "as-asked"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "pumpkin-stencil:design:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `pumpkin-stencil:design:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "pumpkin-stencil:design:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "pumpkin-stencil:design:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "pumpkin-stencil:design:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: pumpkin-stencil:design:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"pumpkin-stencil:design:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);