Driving Haiku Generator from your own code
Haiku Generator is a SkillSafe app, so everything the web page does is available over HTTP. There is
one endpoint that matters — a run — and one JSON contract in each direction. This page
documents both exactly as app.js implements them, not as they were once intended.
Base URL: https://api.skillsafe.ai/v1/app-api
One thing the API cannot give you: the syllable counting. That runs in the browser, in
syllable.js, and it is what turns the model's claim that a line is five syllables
into a checked fact. Over the API you get the model's own syllables array, and you
should treat it the way this app does — as a claim to verify, not a measurement.
The envelope
Every response is {"data": ...} on success or {"error": ...} on failure.
{"data": {"job_id": "job_...", "status": "succeeded", "charged_credits": 812, "output": {"output": "{...}"}}}
{"error": {"code": "INSUFFICIENT_CREDITS", "message": "balance below the minimum for this run"}}
Error codes you will actually meet
| HTTP | code | What to do |
|---|---|---|
| 401 | UNAUTHORIZED | The token is missing, expired or from another app. Get a fresh one — see your token. |
| 402 | INSUFFICIENT_CREDITS | The balance is under min_credits. Call /estimate first and you will never see this. |
| 403 | FORBIDDEN | A guest token tried to run. Runs need a personal token; /me and /estimate do not. |
| 400 | VALIDATION_ERROR | The input object was malformed. text is the only required field. |
| 429 | RATE_LIMITED | Back off and retry. Do not tight-loop. |
| 200 | — with "truncated": true | The balance sat between min_credits and hold_credits, so the run executed with a reduced output cap. Some poems will be missing. Top up and re-run. |
The input contract
There is one input shape, not a lane router. text is the work object whether it is a
subject line or a draft haiku; a draft simply arrives with a draft object attached.
| Field | Type | Notes |
|---|---|---|
text | string | Required. A subject, a mood, a rough line — or the three lines of a draft, newline separated. |
form | string | "strict" (5-7-5), "modern" (short-long-short, 8–15 total), or "both". |
count | number | 1 to 6. How many poems. |
voice | string | "plain", "classical", "lyrical", "stark", "playful". |
season | string | "auto", "spring", "summer", "autumn", "winter", "none". |
notes | string | Optional. Words to keep, things to avoid, who it is for. |
draft | object | Optional. Present when text is the user's own draft: lines, syllables, target, scans, problems. Its presence is what makes the model revise rather than invent, and what makes changed come back populated. |
syllable_facts | object | What the client-side counter makes of the input. The web app always sends it; over the API you can send a minimal version or omit it. Sending it measurably improves how often the model hits the count. |
retry_note | string | Optional. Send on a reformat retry, naming exactly what was wrong with the previous reply. |
The output contract
The model returns one JSON object as a string in data.output.output. Parse it, then read:
{
"title": "Late August, fire escape",
"reading": "One sentence on how the subject was read.",
"form": "strict",
"poems": [
{
"id": "H-01",
"form": "strict",
"lines": ["...", "...", "..."],
"syllables": [5, 7, 5],
"season_word": "late August",
"cut_after_line": 1,
"note": "One sentence on the two images and the turn.",
"changed": []
}
],
"craft_notes": ["..."],
"set_aside": [{"line": "...", "why": "..."}],
"summary": "..."
}
Two fields carry warnings. syllables is the model's own count and is
wrong often enough to be worth checking — the web app recounts every line and shows the
disagreement. changed is empty unless you sent a draft; if you sent one
and it comes back empty, the model ignored the draft and you should retry with a
retry_note saying so.
1. Get a token
Open the token page in a browser: it shows the token this browser holds, offers a shell export to copy, and can mint a fresh guest token. A guest token is enough for /me and /estimate; a run needs a personal token, which comes from signing in. Keep it in an environment variable — the examples below read SKILLSAFE_APP_TOKEN, and where a language cannot, they use a literal "YOUR_TOKEN" placeholder you should replace.
2. Check who you are
GET /me returns exactly three fields: subject_type, subject_id and credits. There is no email and no name, so the test for "signed in" is subject_type === "user".
curl -s "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $SKILLSAFE_APP_TOKEN"import os, requests
TOKEN = os.environ.get("SKILLSAFE_APP_TOKEN", "YOUR_TOKEN")
headers = {"Authorization": f"Bearer {TOKEN}"}
r = requests.get("https://api.skillsafe.ai/v1/app-api/me", headers=headers, timeout=120)
r.raise_for_status()
print(r.json()["data"])const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
method: "GET",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
}
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
console.log(data);package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_APP_TOKEN")
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
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)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("SKILLSAFE_APP_TOKEN");
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.GET()
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'net/http'
require 'json'
require 'uri'
token = ENV.fetch('SKILLSAFE_APP_TOKEN', 'YOUR_TOKEN')
uri = URI('https://api.skillsafe.ai/v1/app-api/me')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{token}"
req['Content-Type'] = 'application/json'
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)['data']<?php
$token = getenv("SKILLSAFE_APP_TOKEN") ?: "YOUR_TOKEN";
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
],
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("SKILLSAFE_APP_TOKEN") ?? "YOUR_TOKEN";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var res = await http.GetAsync("https://api.skillsafe.ai/v1/app-api/me");
Console.WriteLine(await res.Content.ReadAsStringAsync());{"data": {"subject_type": "user", "subject_id": "usr_...", "credits": 184920}}
3. Price the run — free, no job
POST /estimate costs nothing and creates nothing. It returns hold_credits (what is reserved, priced against the full output cap), min_credits (below which the run will not start), the resolved model, the model_alias and markup_bps. Compare hold_credits against your balance before you run and you will never meet a 402.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "rain on a fire escape in late August", "form": "strict", "count": 3, "voice": "plain", "season": "auto", "notes": "keep the word gutter if you can", "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "form_description": "5 / 7 / 5, seventeen syllables", "target": [5, 7, 5], "total_range": [17, 17], "seed_word_syllables": [{"word": "rain", "syllables": 1}, {"word": "fire", "syllables": 1, "alternative": 2}, {"word": "escape", "syllables": 2}, {"word": "august", "syllables": 2}]}}'import os, requests
TOKEN = os.environ.get("SKILLSAFE_APP_TOKEN", "YOUR_TOKEN")
headers = {"Authorization": f"Bearer {TOKEN}"}
payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/estimate", headers=headers, json=payload, timeout=120)
r.raise_for_status()
print(r.json()["data"])const TOKEN = "YOUR_TOKEN";
const payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
};
const res = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
console.log(data);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
token := os.Getenv("SKILLSAFE_APP_TOKEN")
payload := `{"text": "rain on a fire escape in late August", "form": "strict", "count": 3, "voice": "plain", "season": "auto", "notes": "keep the word gutter if you can", "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "form_description": "5 / 7 / 5, seventeen syllables", "target": [5, 7, 5], "total_range": [17, 17], "seed_word_syllables": [{"word": "rain", "syllables": 1}, {"word": "fire", "syllables": 1, "alternative": 2}, {"word": "escape", "syllables": 2}, {"word": "august", "syllables": 2}]}}`
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", strings.NewReader(payload))
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)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("SKILLSAFE_APP_TOKEN");
String payload = """
{
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'net/http'
require 'json'
require 'uri'
token = ENV.fetch('SKILLSAFE_APP_TOKEN', 'YOUR_TOKEN')
uri = URI('https://api.skillsafe.ai/v1/app-api/estimate')
payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{token}"
req['Content-Type'] = 'application/json'
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)['data']<?php
$token = getenv("SKILLSAFE_APP_TOKEN") ?: "YOUR_TOKEN";
$payload = [
"text" => "rain on a fire escape in late August",
"form" => "strict",
"count" => 3,
"voice" => "plain",
"season" => "auto",
"notes" => "keep the word gutter if you can",
"syllable_facts" => [
"counter" => "haiku-generator client-side syllable counter v1.0",
"form" => "strict",
"form_description" => "5 / 7 / 5, seventeen syllables",
"target" => [
5,
7,
5
],
"total_range" => [
17,
17
],
"seed_word_syllables" => [
[
"word" => "rain",
"syllables" => 1
],
[
"word" => "fire",
"syllables" => 1,
"alternative" => 2
],
[
"word" => "escape",
"syllables" => 2
],
[
"word" => "august",
"syllables" => 2
]
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("SKILLSAFE_APP_TOKEN") ?? "YOUR_TOKEN";
var payload = @"{""text"": ""rain on a fire escape in late August"", ""form"": ""strict"", ""count"": 3, ""voice"": ""plain"", ""season"": ""auto"", ""notes"": ""keep the word gutter if you can"", ""syllable_facts"": {""counter"": ""haiku-generator client-side syllable counter v1.0"", ""form"": ""strict"", ""form_description"": ""5 / 7 / 5, seventeen syllables"", ""target"": [5, 7, 5], ""total_range"": [17, 17], ""seed_word_syllables"": [{""word"": ""rain"", ""syllables"": 1}, {""word"": ""fire"", ""syllables"": 1, ""alternative"": 2}, {""word"": ""escape"", ""syllables"": 2}, {""word"": ""august"", ""syllables"": 2}]}}";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(payload, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());{"data": {"model": "gpt-5.6-terra", "model_alias": "gpt-terra", "markup_bps": 1000,
"hold_credits": 3120, "min_credits": 240, "sponsor_enabled": false}}
The hold is not the price. You are charged charged_credits from the run result, which is usually a fraction of the hold because a set of haiku is short.
4. Run it, and poll
POST /run returns a job. Always send an Idempotency-Key: it is what stops a network blip from billing you twice, and what makes a retry replay rather than re-run. Derive it from a hash of the input plus an attempt counter, exactly as the web app does.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "rain on a fire escape in late August", "form": "strict", "count": 3, "voice": "plain", "season": "auto", "notes": "keep the word gutter if you can", "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "form_description": "5 / 7 / 5, seventeen syllables", "target": [5, 7, 5], "total_range": [17, 17], "seed_word_syllables": [{"word": "rain", "syllables": 1}, {"word": "fire", "syllables": 1, "alternative": 2}, {"word": "escape", "syllables": 2}, {"word": "august", "syllables": 2}]}}'import os, requests
TOKEN = os.environ.get("SKILLSAFE_APP_TOKEN", "YOUR_TOKEN")
headers = {"Authorization": f"Bearer {TOKEN}"}
payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/run", headers=headers, json=payload, timeout=120)
r.raise_for_status()
print(r.json()["data"])const TOKEN = "YOUR_TOKEN";
const payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
};
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
console.log(data);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
token := os.Getenv("SKILLSAFE_APP_TOKEN")
payload := `{"text": "rain on a fire escape in late August", "form": "strict", "count": 3, "voice": "plain", "season": "auto", "notes": "keep the word gutter if you can", "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "form_description": "5 / 7 / 5, seventeen syllables", "target": [5, 7, 5], "total_range": [17, 17], "seed_word_syllables": [{"word": "rain", "syllables": 1}, {"word": "fire", "syllables": 1, "alternative": 2}, {"word": "escape", "syllables": 2}, {"word": "august", "syllables": 2}]}}`
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", strings.NewReader(payload))
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)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("SKILLSAFE_APP_TOKEN");
String payload = """
{
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'net/http'
require 'json'
require 'uri'
token = ENV.fetch('SKILLSAFE_APP_TOKEN', 'YOUR_TOKEN')
uri = URI('https://api.skillsafe.ai/v1/app-api/run')
payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{token}"
req['Content-Type'] = 'application/json'
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)['data']<?php
$token = getenv("SKILLSAFE_APP_TOKEN") ?: "YOUR_TOKEN";
$payload = [
"text" => "rain on a fire escape in late August",
"form" => "strict",
"count" => 3,
"voice" => "plain",
"season" => "auto",
"notes" => "keep the word gutter if you can",
"syllable_facts" => [
"counter" => "haiku-generator client-side syllable counter v1.0",
"form" => "strict",
"form_description" => "5 / 7 / 5, seventeen syllables",
"target" => [
5,
7,
5
],
"total_range" => [
17,
17
],
"seed_word_syllables" => [
[
"word" => "rain",
"syllables" => 1
],
[
"word" => "fire",
"syllables" => 1,
"alternative" => 2
],
[
"word" => "escape",
"syllables" => 2
],
[
"word" => "august",
"syllables" => 2
]
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("SKILLSAFE_APP_TOKEN") ?? "YOUR_TOKEN";
var payload = @"{""text"": ""rain on a fire escape in late August"", ""form"": ""strict"", ""count"": 3, ""voice"": ""plain"", ""season"": ""auto"", ""notes"": ""keep the word gutter if you can"", ""syllable_facts"": {""counter"": ""haiku-generator client-side syllable counter v1.0"", ""form"": ""strict"", ""form_description"": ""5 / 7 / 5, seventeen syllables"", ""target"": [5, 7, 5], ""total_range"": [17, 17], ""seed_word_syllables"": [{""word"": ""rain"", ""syllables"": 1}, {""word"": ""fire"", ""syllables"": 1, ""alternative"": 2}, {""word"": ""escape"", ""syllables"": 2}, {""word"": ""august"", ""syllables"": 2}]}}";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(payload, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());Then poll GET /jobs/{job_id} until status is succeeded or failed.
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ" \
-H "Authorization: Bearer $SKILLSAFE_APP_TOKEN"import os, requests
TOKEN = os.environ.get("SKILLSAFE_APP_TOKEN", "YOUR_TOKEN")
headers = {"Authorization": f"Bearer {TOKEN}"}
r = requests.get("https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ", headers=headers, timeout=120)
r.raise_for_status()
print(r.json()["data"])const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ", {
method: "GET",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
}
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
console.log(data);package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_APP_TOKEN")
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ", nil)
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)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("SKILLSAFE_APP_TOKEN");
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.GET()
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'net/http'
require 'json'
require 'uri'
token = ENV.fetch('SKILLSAFE_APP_TOKEN', 'YOUR_TOKEN')
uri = URI('https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{token}"
req['Content-Type'] = 'application/json'
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)['data']<?php
$token = getenv("SKILLSAFE_APP_TOKEN") ?: "YOUR_TOKEN";
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
],
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("SKILLSAFE_APP_TOKEN") ?? "YOUR_TOKEN";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var res = await http.GetAsync("https://api.skillsafe.ai/v1/app-api/jobs/job_01J8XYZ");
Console.WriteLine(await res.Content.ReadAsStringAsync());5. Or stream it
POST /run-stream returns Server-Sent Events: delta as the text generates, job once accepted, and done at the end carrying charged_credits and the full output. This is what the web page uses, because it is what makes the progress card mean anything.
curl -N -s -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_APP_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: haiku-generator:9f3a2c:a1" \
-d '{"text": "rain on a fire escape in late August", "form": "strict", "count": 3, "voice": "plain", "season": "auto", "notes": "keep the word gutter if you can", "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "form_description": "5 / 7 / 5, seventeen syllables", "target": [5, 7, 5], "total_range": [17, 17], "seed_word_syllables": [{"word": "rain", "syllables": 1}, {"word": "fire", "syllables": 1, "alternative": 2}, {"word": "escape", "syllables": 2}, {"word": "august", "syllables": 2}]}}'import os, requests
TOKEN = os.environ.get("SKILLSAFE_APP_TOKEN", "YOUR_TOKEN")
headers = {"Authorization": f"Bearer {TOKEN}"}
payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/run-stream", headers=headers, json=payload, timeout=120)
r.raise_for_status()
print(r.json()["data"])const TOKEN = "YOUR_TOKEN";
const payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
};
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
console.log(data);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
token := os.Getenv("SKILLSAFE_APP_TOKEN")
payload := `{"text": "rain on a fire escape in late August", "form": "strict", "count": 3, "voice": "plain", "season": "auto", "notes": "keep the word gutter if you can", "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "form_description": "5 / 7 / 5, seventeen syllables", "target": [5, 7, 5], "total_range": [17, 17], "seed_word_syllables": [{"word": "rain", "syllables": 1}, {"word": "fire", "syllables": 1, "alternative": 2}, {"word": "escape", "syllables": 2}, {"word": "august", "syllables": 2}]}}`
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run-stream", strings.NewReader(payload))
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)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("SKILLSAFE_APP_TOKEN");
String payload = """
{
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run-stream"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'net/http'
require 'json'
require 'uri'
token = ENV.fetch('SKILLSAFE_APP_TOKEN', 'YOUR_TOKEN')
uri = URI('https://api.skillsafe.ai/v1/app-api/run-stream')
payload = {
"text": "rain on a fire escape in late August",
"form": "strict",
"count": 3,
"voice": "plain",
"season": "auto",
"notes": "keep the word gutter if you can",
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"form_description": "5 / 7 / 5, seventeen syllables",
"target": [
5,
7,
5
],
"total_range": [
17,
17
],
"seed_word_syllables": [
{
"word": "rain",
"syllables": 1
},
{
"word": "fire",
"syllables": 1,
"alternative": 2
},
{
"word": "escape",
"syllables": 2
},
{
"word": "august",
"syllables": 2
}
]
}
}
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{token}"
req['Content-Type'] = 'application/json'
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)['data']<?php
$token = getenv("SKILLSAFE_APP_TOKEN") ?: "YOUR_TOKEN";
$payload = [
"text" => "rain on a fire escape in late August",
"form" => "strict",
"count" => 3,
"voice" => "plain",
"season" => "auto",
"notes" => "keep the word gutter if you can",
"syllable_facts" => [
"counter" => "haiku-generator client-side syllable counter v1.0",
"form" => "strict",
"form_description" => "5 / 7 / 5, seventeen syllables",
"target" => [
5,
7,
5
],
"total_range" => [
17,
17
],
"seed_word_syllables" => [
[
"word" => "rain",
"syllables" => 1
],
[
"word" => "fire",
"syllables" => 1,
"alternative" => 2
],
[
"word" => "escape",
"syllables" => 2
],
[
"word" => "august",
"syllables" => 2
]
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run-stream");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("SKILLSAFE_APP_TOKEN") ?? "YOUR_TOKEN";
var payload = @"{""text"": ""rain on a fire escape in late August"", ""form"": ""strict"", ""count"": 3, ""voice"": ""plain"", ""season"": ""auto"", ""notes"": ""keep the word gutter if you can"", ""syllable_facts"": {""counter"": ""haiku-generator client-side syllable counter v1.0"", ""form"": ""strict"", ""form_description"": ""5 / 7 / 5, seventeen syllables"", ""target"": [5, 7, 5], ""total_range"": [17, 17], ""seed_word_syllables"": [{""word"": ""rain"", ""syllables"": 1}, {""word"": ""fire"", ""syllables"": 1, ""alternative"": 2}, {""word"": ""escape"", ""syllables"": 2}, {""word"": ""august"", ""syllables"": 2}]}}";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(payload, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run-stream", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());event: job
data: {"job_id":"job_01J8XYZ"}
event: delta
data: {"text":"{\"title\": \"Late August"}
event: delta
data: {"text":", fire escape\","}
event: done
data: {"job_id":"job_01J8XYZ","status":"succeeded","charged_credits":812,"output":{"output":"{ ... }"}}
If the stream dies mid-flight, keep what arrived. The web app parses the partial JSON and renders every poem that completed rather than discarding a run you paid for.
6. Worked example: fixing a draft
The same endpoint, with a draft object attached. This is the shape to send when the user already wrote something and wants it to scan — the poems come back keeping their images, with changed naming every alteration.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up", "form": "strict", "count": 2, "voice": "plain", "season": "winter", "notes": "keep the kettle", "draft": {"lines": ["my grandmother's kitchen in winter", "the kettle whistles quietly", "the window fogs up"], "syllables": [10, 8, 5], "target": [5, 7, 5], "scans": false, "problems": ["Line 1 is 10, the form wants 5 (5 over).", "Line 2 is 8, the form wants 7 (1 over)."]}, "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "target": [5, 7, 5]}}'import os, requests
TOKEN = os.environ.get("SKILLSAFE_APP_TOKEN", "YOUR_TOKEN")
headers = {"Authorization": f"Bearer {TOKEN}"}
payload = {
"text": "my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up",
"form": "strict",
"count": 2,
"voice": "plain",
"season": "winter",
"notes": "keep the kettle",
"draft": {
"lines": [
"my grandmother's kitchen in winter",
"the kettle whistles quietly",
"the window fogs up"
],
"syllables": [
10,
8,
5
],
"target": [
5,
7,
5
],
"scans": False,
"problems": [
"Line 1 is 10, the form wants 5 (5 over).",
"Line 2 is 8, the form wants 7 (1 over)."
]
},
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"target": [
5,
7,
5
]
}
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/run", headers=headers, json=payload, timeout=120)
r.raise_for_status()
print(r.json()["data"])const TOKEN = "YOUR_TOKEN";
const payload = {
"text": "my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up",
"form": "strict",
"count": 2,
"voice": "plain",
"season": "winter",
"notes": "keep the kettle",
"draft": {
"lines": [
"my grandmother's kitchen in winter",
"the kettle whistles quietly",
"the window fogs up"
],
"syllables": [
10,
8,
5
],
"target": [
5,
7,
5
],
"scans": false,
"problems": [
"Line 1 is 10, the form wants 5 (5 over).",
"Line 2 is 8, the form wants 7 (1 over)."
]
},
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"target": [
5,
7,
5
]
}
};
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
console.log(data);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
token := os.Getenv("SKILLSAFE_APP_TOKEN")
payload := `{"text": "my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up", "form": "strict", "count": 2, "voice": "plain", "season": "winter", "notes": "keep the kettle", "draft": {"lines": ["my grandmother's kitchen in winter", "the kettle whistles quietly", "the window fogs up"], "syllables": [10, 8, 5], "target": [5, 7, 5], "scans": false, "problems": ["Line 1 is 10, the form wants 5 (5 over).", "Line 2 is 8, the form wants 7 (1 over)."]}, "syllable_facts": {"counter": "haiku-generator client-side syllable counter v1.0", "form": "strict", "target": [5, 7, 5]}}`
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", strings.NewReader(payload))
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)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = System.getenv("SKILLSAFE_APP_TOKEN");
String payload = """
{
"text": "my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up",
"form": "strict",
"count": 2,
"voice": "plain",
"season": "winter",
"notes": "keep the kettle",
"draft": {
"lines": [
"my grandmother's kitchen in winter",
"the kettle whistles quietly",
"the window fogs up"
],
"syllables": [
10,
8,
5
],
"target": [
5,
7,
5
],
"scans": false,
"problems": [
"Line 1 is 10, the form wants 5 (5 over).",
"Line 2 is 8, the form wants 7 (1 over)."
]
},
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"target": [
5,
7,
5
]
}
}""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'net/http'
require 'json'
require 'uri'
token = ENV.fetch('SKILLSAFE_APP_TOKEN', 'YOUR_TOKEN')
uri = URI('https://api.skillsafe.ai/v1/app-api/run')
payload = {
"text": "my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up",
"form": "strict",
"count": 2,
"voice": "plain",
"season": "winter",
"notes": "keep the kettle",
"draft": {
"lines": [
"my grandmother's kitchen in winter",
"the kettle whistles quietly",
"the window fogs up"
],
"syllables": [
10,
8,
5
],
"target": [
5,
7,
5
],
"scans": false,
"problems": [
"Line 1 is 10, the form wants 5 (5 over).",
"Line 2 is 8, the form wants 7 (1 over)."
]
},
"syllable_facts": {
"counter": "haiku-generator client-side syllable counter v1.0",
"form": "strict",
"target": [
5,
7,
5
]
}
}
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{token}"
req['Content-Type'] = 'application/json'
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)['data']<?php
$token = getenv("SKILLSAFE_APP_TOKEN") ?: "YOUR_TOKEN";
$payload = [
"text" => "my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up",
"form" => "strict",
"count" => 2,
"voice" => "plain",
"season" => "winter",
"notes" => "keep the kettle",
"draft" => [
"lines" => [
"my grandmother's kitchen in winter",
"the kettle whistles quietly",
"the window fogs up"
],
"syllables" => [
10,
8,
5
],
"target" => [
5,
7,
5
],
"scans" => false,
"problems" => [
"Line 1 is 10, the form wants 5 (5 over).",
"Line 2 is 8, the form wants 7 (1 over)."
]
],
"syllable_facts" => [
"counter" => "haiku-generator client-side syllable counter v1.0",
"form" => "strict",
"target" => [
5,
7,
5
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = Environment.GetEnvironmentVariable("SKILLSAFE_APP_TOKEN") ?? "YOUR_TOKEN";
var payload = @"{""text"": ""my grandmother's kitchen in winter\nthe kettle whistles quietly\nthe window fogs up"", ""form"": ""strict"", ""count"": 2, ""voice"": ""plain"", ""season"": ""winter"", ""notes"": ""keep the kettle"", ""draft"": {""lines"": [""my grandmother's kitchen in winter"", ""the kettle whistles quietly"", ""the window fogs up""], ""syllables"": [10, 8, 5], ""target"": [5, 7, 5], ""scans"": false, ""problems"": [""Line 1 is 10, the form wants 5 (5 over)."", ""Line 2 is 8, the form wants 7 (1 over).""]}, ""syllable_facts"": {""counter"": ""haiku-generator client-side syllable counter v1.0"", ""form"": ""strict"", ""target"": [5, 7, 5]}}";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(payload, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());{
"title": "Grandmother's kitchen, winter",
"form": "strict",
"poems": [
{
"id": "H-01",
"form": "strict",
"lines": [
"grandmother's kitchen",
"the kettle whistles alone",
"the window fogs white"
],
"syllables": [
5,
7,
5
],
"season_word": "winter",
"cut_after_line": 1,
"note": "The empty room is set against the kettle nobody has taken off the heat.",
"changed": [
"line 1: dropped 'my' and 'in winter', which were the five syllables over",
"line 2: 'quietly' became 'alone', keeping the stillness and fitting the seven",
"line 3: added 'white' to close the count and give the fog a colour"
]
}
],
"craft_notes": [
"The draft explained the kitchen; the revisions let one object stand for it."
],
"set_aside": [
{
"line": "the kettle sings to no one",
"why": "seven syllables, but it names the feeling instead of showing it"
}
],
"summary": "Three routes to 5-7-5, all keeping the kettle and the fogged window."
}
Rate limits and the things that will bite you
- Guests cannot run.
/meand/estimatework with a guest token;/runand/run-streamreturn 403. This is the same line the web page draws. - Every
POST /guestmints a new identity. A fresh guest token sees an empty history, because the old rows belong to the old guest. Reuse one token across a session. - The
syllablesarray is a claim. Count the lines yourself before you present them as 5-7-5. The whole reason this app has a client-side counter is that models miscount their own output. - Send
syllable_factsif you can. Even a minimal version — the form and the target array — is worth sending; it is the difference between the model guessing at the target and being told it. - Idempotency keys must include the attempt. A retry that reuses the key exactly will replay the first (bad) result rather than re-running. The web app uses
haiku-generator:{hash}:a{attempt}.