Wedding Vows over HTTP
Base URL https://api.skillsafe.ai/v1/app-api. Every response is the envelope {"ok":true,"data":{...}} or {"ok":false,"error":{"code","message","details"}}. A run is metered against the caller's SkillSafe credits; /estimate and /me are free.
The request body
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
One JSON object, posted as the whole body. There is no task field and no wrapper: an input wrapper returns 200 while hiding the request from the model.
| Field | Type | Meaning |
|---|---|---|
mode | string | write or revise |
kind | string | vows, toast, reading or opening |
speaker | object | {name, role, relation_note}. role is one of partner, best-person, maid-of-honour, parent, sibling, friend, officiant, other |
couple | object | {one, two} - how to refer to each of them |
about | string | the description of the two of them, in the requester's words. This is the material everything else is built from |
about_clipped | number | characters removed from the middle if the description exceeded 6000; 0 otherwise |
particulars | array | {kind, text} objects extracted in the browser. kind is one of name, said, time, place, object, act |
must_include | array | strings, max 8 |
avoid | array | strings, max 8 |
tone | string | plain, warm, funny, formal or lyrical |
length | string | short, medium or long |
word_budget | number | target word count for piece, derived from kind and length |
occasion | string | the shape of the day |
prescan | object | {particular_count, thin, notes}. thin being true changes what the model does - see the thin-input section of the prompt |
previous | object | revise only: {piece, change} |
The output contract
The model returns one JSON object and nothing else. This is what the renderer parses; take it from here rather than from intent.
{
"kind": "vows",
"title": "The teaspoon",
"piece": [
"First line or paragraph.",
"Second line or paragraph."
],
"delivery": [
"Two to four notes on saying it out loud."
],
"drew_on": [
"a detail from the request, in the requester's own words"
],
"left_out": [],
"alternates": [
{
"replaces": "a line exactly as it stands in piece",
"with": "a different line",
"why": "one clause"
}
],
"from_the_writer": "One short paragraph to whoever will speak this."
}
A refusal returns a different shape, and the renderer has its own path for it:
{
"kind": "declined",
"title": "short label",
"declined_why": "one or two plain sentences",
"declined_offer": "what the desk can write instead"
}
Two constraints worth knowing before you parse it. Every alternates[].replaces value is meant to appear verbatim in one of the piece entries - if it does not, treat the alternate as unusable rather than trying to fuzzy-match it. And left_out is legitimately empty; an empty array means everything supplied was used, not that the field is missing.
1. Get a token
Every call needs Authorization: Bearer <token>. The easiest way to get one is the token page, which will mint a guest token or hand you your signed-in one, with a copyable shell export. Guest tokens can call /me and /estimate; a run needs a signed-in token with credits.
2. Check who you are
Returns exactly three fields: subject_type, subject_id and credits. There is no username, email or name. The signed-in test is subject_type === "user"; a guest token returns "guest", and a 401 here on a cold start simply means no token has been minted yet.
TOKEN="YOUR_TOKEN" curl -sS -X GET https://api.skillsafe.ai/v1/app-api/me \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + 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:
env = json.load(r)
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
print(call("GET", "/me"))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, payload) {
const res = await fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: payload === undefined ? undefined : JSON.stringify(payload),
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message}`);
return env.data;
}
console.log(await call("GET", "/me"));
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN"
func call(method, path string, payload any) (map[string]any, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
func main() {
d, _ := call("GET", "/me", nil)
fmt.Println(d["subject_type"], d["credits"])
}
import java.net.URI;
import java.net.http.*;
class WeddingVows {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static String call(String method, String path, String payload) throws Exception {
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, payload == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}}
}
public static void main(String[] a) throws Exception {
System.out.println(call("GET", "/me", null));
}
}
require "json"
require "net/http"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"
def call(method, path, payload = nil)
uri = URI(BASE.to_s + path)
klass = method == "GET" ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload) if payload
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
pp call("GET", "/me")
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";
function call($method, $path, $payload = null) {
global $base, $token;
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new Exception($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
print_r(call("GET", "/me"));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
var res = await http.GetStringAsync(Base + "/me");
Console.WriteLine(res);
3. Price it, free
Returns model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled. No charge and no job.
This endpoint performs no validation of the body whatsoever. A bare string, a number, null and [] all return ok:true with a well-formed estimate and a correct model binding. So a successful estimate proves your token and the app's model binding, and proves nothing at all about whether your request body is the right shape. Validate the body on your own side; there is no server-side signal, ever.
TOKEN="YOUR_TOKEN"
curl -sS -X POST https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"mode": "write", "kind": "vows", "speaker": {"name": "Rosa", "role": "partner", "relation_note": ""}, "couple": {"one": "Rosa", "two": "Dan"}, "about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.", "about_clipped": 0, "particulars": [{"kind": "name", "text": "Rosa"}, {"kind": "name", "text": "Dan"}, {"kind": "place", "text": "at the launderette"}, {"kind": "place", "text": "on Cardigan Road"}, {"kind": "time", "text": "2011"}, {"kind": "act", "text": "fixed her bike chain"}, {"kind": "said", "text": "Teaspoon"}], "must_include": ["A promise about the letters"], "avoid": ["No jokes about the chip shop smell"], "tone": "plain", "length": "medium", "word_budget": 170, "occasion": "Sixty people in a rugby club function room in November.", "prescan": {"particular_count": 7, "thin": false, "notes": ["one object that belongs to the two of them"]}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + 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:
env = json.load(r)
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
body = {
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
est = call("POST", "/estimate", body)
print(est["hold_credits"], est["model_alias"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, payload) {
const res = await fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: payload === undefined ? undefined : JSON.stringify(payload),
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message}`);
return env.data;
}
const body = {
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
};
const est = await call("POST", "/estimate", body);
console.log(est.hold_credits, est.model_alias);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN"
func call(method, path string, payload any) (map[string]any, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
var bodyJSON = []byte(`{"mode": "write", "kind": "vows", "speaker": {"name": "Rosa", "role": "partner", "relation_note": ""}, "couple": {"one": "Rosa", "two": "Dan"}, "about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.", "about_clipped": 0, "particulars": [{"kind": "name", "text": "Rosa"}, {"kind": "name", "text": "Dan"}, {"kind": "place", "text": "at the launderette"}, {"kind": "place", "text": "on Cardigan Road"}, {"kind": "time", "text": "2011"}, {"kind": "act", "text": "fixed her bike chain"}, {"kind": "said", "text": "Teaspoon"}], "must_include": ["A promise about the letters"], "avoid": ["No jokes about the chip shop smell"], "tone": "plain", "length": "medium", "word_budget": 170, "occasion": "Sixty people in a rugby club function room in November.", "prescan": {"particular_count": 7, "thin": false, "notes": ["one object that belongs to the two of them"]}}`)
func main() {
var payload map[string]any
json.Unmarshal(bodyJSON, &payload)
d, _ := call("POST", "/estimate", payload)
fmt.Println(d["hold_credits"], d["model_alias"])
}
import java.net.URI;
import java.net.http.*;
class WeddingVows {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static String call(String method, String path, String payload) throws Exception {
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, payload == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}}
}
public static void main(String[] a) throws Exception {
String body = """
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
""";
System.out.println(call("POST", "/estimate", body));
}
}
require "json"
require "net/http"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"
def call(method, path, payload = nil)
uri = URI(BASE.to_s + path)
klass = method == "GET" ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload) if payload
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
body = JSON.parse(<<~JSON)
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
JSON
pp call("POST", "/estimate", body)
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";
function call($method, $path, $payload = null) {
global $base, $token;
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new Exception($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
$body = json_decode(<<<'JSON'
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
JSON, true);
print_r(call("POST", "/estimate", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
var body = """
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
""";
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + "/estimate", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
4. Run it, and poll
Returns {"job_id": "job_..."}. Poll GET /jobs/{job_id} until status is succeeded or failed; the finished job carries output, charged_credits and, if the balance covered only part of a run this long, truncated: true.
Send Idempotency-Key on every run. A retry after a network blip must reuse the first attempt's key or it bills twice.
TOKEN="YOUR_TOKEN"
curl -sS -X POST https://api.skillsafe.ai/v1/app-api/run \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"mode": "write", "kind": "vows", "speaker": {"name": "Rosa", "role": "partner", "relation_note": ""}, "couple": {"one": "Rosa", "two": "Dan"}, "about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.", "about_clipped": 0, "particulars": [{"kind": "name", "text": "Rosa"}, {"kind": "name", "text": "Dan"}, {"kind": "place", "text": "at the launderette"}, {"kind": "place", "text": "on Cardigan Road"}, {"kind": "time", "text": "2011"}, {"kind": "act", "text": "fixed her bike chain"}, {"kind": "said", "text": "Teaspoon"}], "must_include": ["A promise about the letters"], "avoid": ["No jokes about the chip shop smell"], "tone": "plain", "length": "medium", "word_budget": 170, "occasion": "Sixty people in a rugby club function room in November.", "prescan": {"particular_count": 7, "thin": false, "notes": ["one object that belongs to the two of them"]}}'
# the reply is {"data":{"job_id":"job_..."}} - poll it:
curl -sS https://api.skillsafe.ai/v1/app-api/jobs/job_xxx -H "Authorization: Bearer $TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + 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:
env = json.load(r)
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
body = {
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
job = call("POST", "/run", body)
while True:
j = call("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed"):
break
print(json.loads(j["output"]))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, payload) {
const res = await fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: payload === undefined ? undefined : JSON.stringify(payload),
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message}`);
return env.data;
}
const body = {
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
};
const { job_id } = await call("POST", "/run", body);
let job;
do {
await new Promise((r) => setTimeout(r, 1000));
job = await call("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
console.log(JSON.parse(job.output));
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN"
func call(method, path string, payload any) (map[string]any, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
var bodyJSON = []byte(`{"mode": "write", "kind": "vows", "speaker": {"name": "Rosa", "role": "partner", "relation_note": ""}, "couple": {"one": "Rosa", "two": "Dan"}, "about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.", "about_clipped": 0, "particulars": [{"kind": "name", "text": "Rosa"}, {"kind": "name", "text": "Dan"}, {"kind": "place", "text": "at the launderette"}, {"kind": "place", "text": "on Cardigan Road"}, {"kind": "time", "text": "2011"}, {"kind": "act", "text": "fixed her bike chain"}, {"kind": "said", "text": "Teaspoon"}], "must_include": ["A promise about the letters"], "avoid": ["No jokes about the chip shop smell"], "tone": "plain", "length": "medium", "word_budget": 170, "occasion": "Sixty people in a rugby club function room in November.", "prescan": {"particular_count": 7, "thin": false, "notes": ["one object that belongs to the two of them"]}}`)
func main() {
var payload map[string]any
json.Unmarshal(bodyJSON, &payload)
job, _ := call("POST", "/run", payload)
id := job["job_id"].(string)
for {
j, _ := call("GET", "/jobs/"+id, nil)
s := j["status"].(string)
if s == "succeeded" || s == "failed" {
fmt.Println(j["output"])
return
}
}
}
import java.net.URI;
import java.net.http.*;
class WeddingVows {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static String call(String method, String path, String payload) throws Exception {
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, payload == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}}
}
public static void main(String[] a) throws Exception {
String body = """
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
""";
System.out.println(call("POST", "/run", body));
}
}
require "json"
require "net/http"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"
def call(method, path, payload = nil)
uri = URI(BASE.to_s + path)
klass = method == "GET" ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload) if payload
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
body = JSON.parse(<<~JSON)
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
JSON
pp call("POST", "/run", body)
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";
function call($method, $path, $payload = null) {
global $base, $token;
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new Exception($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
$body = json_decode(<<<'JSON'
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
JSON, true);
print_r(call("POST", "/run", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
var body = """
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
""";
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + "/run", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
5. Or stream it
POST /run-stream returns text/event-stream.
Get the wire format right. Frames are separated by a blank line. The event NAME is on its own event: line and the JSON payload on a data: line - there is no type field inside the data object, and a parser written to dispatch on one will never fire:
event: delta
data: {"text": "..."}
event: done
data: {"job_id": "job_...", "status": "succeeded", "charged_credits": 812, "output": "{...}"}
Event names are job, delta, done, pending and error. A stream can stop mid-object, so keep the accumulated text and repair it rather than discarding it - closing the open brackets over the last complete member recovers a usable object from almost any cut point.
TOKEN="YOUR_TOKEN"
curl -sS -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d '{"mode": "write", "kind": "vows", "speaker": {"name": "Rosa", "role": "partner", "relation_note": ""}, "couple": {"one": "Rosa", "two": "Dan"}, "about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.", "about_clipped": 0, "particulars": [{"kind": "name", "text": "Rosa"}, {"kind": "name", "text": "Dan"}, {"kind": "place", "text": "at the launderette"}, {"kind": "place", "text": "on Cardigan Road"}, {"kind": "time", "text": "2011"}, {"kind": "act", "text": "fixed her bike chain"}, {"kind": "said", "text": "Teaspoon"}], "must_include": ["A promise about the letters"], "avoid": ["No jokes about the chip shop smell"], "tone": "plain", "length": "medium", "word_budget": 170, "occasion": "Sixty people in a rugby club function room in November.", "prescan": {"particular_count": 7, "thin": false, "notes": ["one object that belongs to the two of them"]}}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + 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:
env = json.load(r)
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
# /run-stream returns server-sent events. Frames are separated by a BLANK LINE;
# the event NAME is on its own `event:` line and the payload on a `data:` line.
body = {
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
import http.client
conn = http.client.HTTPSConnection("api.skillsafe.ai")
conn.request("POST", "/v1/app-api/run-stream", json.dumps(body), {
"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
resp = conn.getresponse()
buf, out = "", ""
while chunk := resp.read(1024):
buf += chunk.decode()
while "\n\n" in buf:
frame, buf = buf.split("\n\n", 1)
name, payload = "message", ""
for line in frame.split("\n"):
if line.startswith("event:"):
name = line[6:].strip()
elif line.startswith("data:"):
payload += line[5:].strip()
if not payload:
continue
d = json.loads(payload)
if name == "delta":
out += d.get("text", "")
elif name == "done":
print(json.loads(d["output"]))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, payload) {
const res = await fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: payload === undefined ? undefined : JSON.stringify(payload),
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message}`);
return env.data;
}
// Frames are separated by a BLANK LINE. The event NAME is on its own `event:`
// line and the JSON payload on a `data:` line - not a `type` field inside the data.
const body = {
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
};
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", out = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n\n")) >= 0) {
const frame = buf.slice(0, i);
buf = buf.slice(i + 2);
let name = "message", data = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) name = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (!data) continue;
const d = JSON.parse(data);
if (name === "delta") out += d.text || "";
if (name === "done") console.log(JSON.parse(d.output));
}
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN"
func call(method, path string, payload any) (map[string]any, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
// Frames are separated by a blank line; the event name is on its own `event:` line.
// bufio.Scanner over the response body, splitting on "\n\n", is the shape to use.
func main() {
fmt.Println("see the JavaScript tab for the full frame loop")
}
import java.net.URI;
import java.net.http.*;
class WeddingVows {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static String call(String method, String path, String payload) throws Exception {
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, payload == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(payload))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}}
}
public static void main(String[] a) throws Exception {
String body = """
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
""";
System.out.println(call("POST", "/run-stream", body));
}
}
require "json"
require "net/http"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"
def call(method, path, payload = nil)
uri = URI(BASE.to_s + path)
klass = method == "GET" ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload) if payload
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
body = JSON.parse(<<~JSON)
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
JSON
pp call("POST", "/run-stream", body)
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";
function call($method, $path, $payload = null) {
global $base, $token;
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new Exception($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
$body = json_decode(<<<'JSON'
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
JSON, true);
print_r(call("POST", "/run-stream", $body));
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
var body = """
{
"mode": "write",
"kind": "vows",
"speaker": {
"name": "Rosa",
"role": "partner",
"relation_note": ""
},
"couple": {
"one": "Rosa",
"two": "Dan"
},
"about": "They met at the launderette on Cardigan Road in 2011. He fixed her bike chain with a teaspoon in the car park. She has called him Teaspoon ever since.",
"about_clipped": 0,
"particulars": [
{
"kind": "name",
"text": "Rosa"
},
{
"kind": "name",
"text": "Dan"
},
{
"kind": "place",
"text": "at the launderette"
},
{
"kind": "place",
"text": "on Cardigan Road"
},
{
"kind": "time",
"text": "2011"
},
{
"kind": "act",
"text": "fixed her bike chain"
},
{
"kind": "said",
"text": "Teaspoon"
}
],
"must_include": [
"A promise about the letters"
],
"avoid": [
"No jokes about the chip shop smell"
],
"tone": "plain",
"length": "medium",
"word_budget": 170,
"occasion": "Sixty people in a rugby club function room in November.",
"prescan": {
"particular_count": 7,
"thin": false,
"notes": [
"one object that belongs to the two of them"
]
}
}
""";
var content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.PostAsync(Base + "/run-stream", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Errors
| Code | HTTP | What to do |
|---|---|---|
UNAUTHORIZED | 401 | No token, or it expired. Mint a new one. On a first-ever call this is the expected answer. |
FORBIDDEN | 403 | The token belongs to another app. Tokens are scoped per app slug. |
INSUFFICIENT_CREDITS | 402 | Balance below min_credits. Estimate first and compare against /me. |
VALIDATION_ERROR | 400 | Malformed JSON body. Note this does not fire on /estimate, which validates nothing. |
RATE_LIMITED | 429 | Back off and retry. Do not tight-loop. |
JOB_FAILED | 200 | The envelope is ok but the job's status is failed; read error on the job. |
What the app does with the reply
The browser reconciles the model's output against the request before showing it, and you may want to do the same: check every drew_on entry against your own about text, and check every capitalised name in piece against the request. A name in the finished piece that appears nowhere in the request is the failure that matters here - it is a person saying something untrue at the front of a room.