Guided Meditation Maker - API

Timed meditation scripts from your own code.

Write a guided meditation with timed silences

Send a goal (sleep, unwind, focus or a body scan), a length and an optional picture. You get back a script meant to be read aloud: titled sections of spoken lines with a timed silence after most of them. The app then voices it on the listener's device and mixes it over a generated ambient sound; from the API you get the text and the timings.

Basics

Base URL https://api.skillsafe.ai/v1/app-api. Every call except POST /guest sends Authorization: Bearer <token>; bodies are JSON. The token is bound to this app, so there is no app header: the slug appears only in the POST /guest body.

Responses use one envelope: {"ok":true,"data":{...}} on success and {"ok":false,"error":{"code","message","details"}} on failure. /estimate, /run and /run-stream take the input object itself as the body - never wrap it as {"input":{...}}.

Credits: 1 credit = $0.0001. /estimate is free and creates no job; a run holds up to hold_credits and charges charged_credits when it finishes.

Statuserror.codeMeaning
400validation_errorA field of the wrong type, or an object where a scalar belongs; details names it.
401unauthorizedInvalid or expired token. Get a new one (step 1).
402payment_requiredThe balance is below min_credits. Top up at skillsafe.ai.
403forbiddenA guest token cannot run metered turns unless the app sponsors guests. Sign in for a personal token.
404not_foundUnknown app slug, or a job id that is not yours.
409conflictThe Idempotency-Key was used before with a different body.
429rate_limitedBack off and retry with the same Idempotency-Key.
5xx-Transient. Retry with backoff and the same key.

Browsers enforce CORS for this API: run the samples from a server, script or terminal.

The task field first

There is one task, meditate. Always send "task": "meditate".

taskWhat it doesReply lines
meditateWrite a meditation script for the goal, length and picture, with timed silences.title, then 4-7 sections: phase followed by say and pause lines; end last.

Input fields

Every field is a scalar. The app computes target_words and pause_seconds from the length; send the values below for each length.

FieldTypeMeaning
taskstringmeditate.
goalstringsleep (ends by drifting off, no wake-up) | stress (unwind) | focus | bodyscan.
minutesnumber5, 10, 15 or 20.
target_wordsnumberSpoken words for that length: 360, 590, 790 or 920.
pause_secondsnumberTotal silence for that length: 136, 332, 541 or 782.
settingstringbreath (no imagery) | forest | shore | nightsky | warmlight.
posturestringlying for sleep and body scan, sitting otherwise.
notestringOptional: what is on the listener's mind. The app sends at most 600 characters (start and end, the middle replaced by a marker). Empty string when unused.
changestringOptional: a change to a previous version ("more silence, no counting"), up to 300 characters.
retry_notestringOptional: a format correction after a reply that could not be read; send it with a new attempt number in the key.

Reply contract

JSON Lines: one object per line with a "k" field, the last line always {"k":"end"}. A reply without it was cut short - keep the lines that arrived. Skip any line that is not a JSON object.

kFields
titletext - 2 to 6 words.
phasename: arrive | breath | body | scan | anchor | imagery | release | drift | return; heading. Opens a section; arrive is first, drift (sleep) or return (all other goals) is last.
saytext - plain sentences to read aloud.
pausesec - a whole number of seconds of silence.
end- the reply is complete.

The pause lengths are relative: the app measures how long the voice takes to read each line, then scales the silences so the whole session lands on minutes. Check the reply as the app does: spoken words near target_words, no wake-up cue in a sleep session, and calm, non-clinical wording. This is a relaxation exercise, not medical care.

Step 1 - A tiny client, and a token

POST /guest

A short helper adds the auth header, sends JSON and unwraps the envelope; every later step reuses it. For the token, open the token page, sign in with SkillSafe and copy the token. A personal token bills runs to your account. A script with no browser can mint a guest token with POST /guest and the body {"slug":"guided-meditation-maker"}, sent with no auth header; guests can estimate but not run.

export API="https://api.skillsafe.ai/v1/app-api"
export SKILLSAFE_TOKEN="YOUR_TOKEN"   # "Copy shell export" on /tokens.html

# No browser? A guest token (this call has no Authorization header):
SKILLSAFE_TOKEN=$(curl -s -X POST "$API/guest" -H "Content-Type: application/json" \
  -d '{"slug":"guided-meditation-maker"}' | jq -r '.data.token')
import json, time, requests

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"  # from tokens.html

def api(method, path, body=None, headers=None):
    h = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}", **(headers or {})}
    res = requests.request(method, API + path, json=body, headers=h, timeout=180)
    payload = res.json()
    if not res.ok or payload.get("ok") is False:
        err = payload.get("error", {})
        raise RuntimeError(f"{res.status_code} {err.get('code')}: {err.get('message')}")
    return payload["data"]
// Node 18+ (global fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from tokens.html

async function api(method, path, body, headers = {}) {
  const res = await fetch(API + path, {
    method,
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}`, ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const payload = await res.json();
  if (!res.ok || payload.ok === false) {
    throw new Error(`${res.status} ${payload.error?.code}: ${payload.error?.message}`);
  }
  return payload.data;
}
package main

import ("bytes"; "encoding/json"; "fmt"; "net/http"; "time")

const API = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from tokens.html

func call(method, path string, body any, headers map[string]string, out any) error {
	var buf bytes.Buffer
	if body != nil { json.NewEncoder(&buf).Encode(body) }
	req, _ := http.NewRequest(method, API+path, &buf)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)
	for k, v := range headers { req.Header.Set(k, v) }
	res, err := (&http.Client{Timeout: 180 * time.Second}).Do(req)
	if err != nil { return err }
	defer res.Body.Close()
	var env struct {
		Ok    bool            `json:"ok"`
		Data  json.RawMessage `json:"data"`
		Error struct{ Code, Message string } `json:"error"`
	}
	json.NewDecoder(res.Body).Decode(&env)
	if !env.Ok { return fmt.Errorf("%d %s: %s", res.StatusCode, env.Error.Code, env.Error.Message) }
	return json.Unmarshal(env.Data, out)
}
// Java 11+, Jackson for JSON
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;

static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN"; // from tokens.html
static final ObjectMapper JSON = new ObjectMapper();
static final HttpClient HTTP = HttpClient.newHttpClient();

static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
    var b = HttpRequest.newBuilder(URI.create(API + path))
        .header("Content-Type", "application/json").header("Authorization", "Bearer " + TOKEN)
        .method(method, body == null ? HttpRequest.BodyPublishers.noBody()
                                     : HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body)));
    headers.forEach(b::header);
    var res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
    JsonNode env = JSON.readTree(res.body());
    if (!env.path("ok").asBoolean()) throw new RuntimeException(res.statusCode() + " " + env.path("error"));
    return env.get("data");
}
require "net/http"
require "json"

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from tokens.html

def api(method, path, body = nil, headers = {})
  uri = URI(API + path)
  req = Net::HTTP.const_get(method.capitalize).new(uri)
  req["Content-Type"] = "application/json"
  req["Authorization"] = "Bearer #{TOKEN}"
  headers.each { |k, v| req[k] = v }
  req.body = body.to_json if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 180) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise "#{res.code} #{payload.dig('error', 'code')}: #{payload.dig('error', 'message')}" unless payload["ok"]
  payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from tokens.html

function api(string $method, string $path, ?array $body = null, array $headers = []): array {
    $h = ["Content-Type: application/json", "Authorization: Bearer " . TOKEN];
    foreach ($headers as $k => $v) $h[] = "$k: $v";
    $ch = curl_init(API . $path);
    curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h,
        CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 180]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $payload = json_decode(curl_exec($ch), true);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if (empty($payload["ok"])) throw new RuntimeException("$code " . json_encode($payload["error"] ?? null));
    return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;

static class Meditation
{
    const string Api = "https://api.skillsafe.ai/v1/app-api";
    public static string Token = "YOUR_TOKEN"; // from tokens.html
    static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(180) };

    public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null,
        Dictionary<string, string>? headers = null)
    {
        var req = new HttpRequestMessage(method, Api + path);
        req.Headers.Add("Authorization", "Bearer " + Token);
        foreach (var (k, v) in headers ?? new()) req.Headers.Add(k, v);
        if (body != null) req.Content = JsonContent.Create(body);
        var res = await Http.SendAsync(req);
        var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
        if (!env.GetProperty("ok").GetBoolean()) throw new Exception($"{(int)res.StatusCode} {env.GetProperty("error")}");
        return env.GetProperty("data").Clone();
    }
}

Step 2 - Who am I, and what is my balance?

GET /me

subject_type is user for a personal token and guest otherwise; credits is the balance.

curl -s "$API/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN" | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
	SubjectType string `json:"subject_type"`
	Credits     int    `json:"credits"`
}
if err := call("GET", "/me", nil, nil, &me); err != nil { panic(err) }
fmt.Println(me.SubjectType, me.Credits)
JsonNode me = api("GET", "/me", null, Map.of());
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts me.values_at("subject_type", "credits").inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? 0, PHP_EOL;
var me = await Meditation.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");

Step 3 - Price a session for free

POST /estimate

/estimate takes the same body as a run and creates no job. It returns hold_credits (reserved while the run is in flight) and min_credits. A run is charged charged_credits, usually far less than the hold.

jq -n '{task: "meditate", goal: "stress", minutes: 10, target_words: 590, pause_seconds: 332,
  setting: "shore", posture: "sitting", note: "a long week of deadlines"}' > session.json

curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
  -d @session.json | jq '.data | {hold_credits, min_credits, model_alias, warnings}'
session = {"task": "meditate", "goal": "stress", "minutes": 10, "target_words": 590, "pause_seconds": 332,
           "setting": "shore", "posture": "sitting", "note": "a long week of deadlines"}
est = api("POST", "/estimate", session)
print(est["hold_credits"], est["min_credits"])
const session = { task: "meditate", goal: "stress", minutes: 10, target_words: 590, pause_seconds: 332,
  setting: "shore", posture: "sitting", note: "a long week of deadlines" };
const est = await api("POST", "/estimate", session);
console.log(est.hold_credits, est.min_credits);
session := map[string]any{"task": "meditate", "goal": "stress", "minutes": 10, "target_words": 590,
	"pause_seconds": 332, "setting": "shore", "posture": "sitting", "note": "a long week of deadlines"}
var est struct {
	Hold int `json:"hold_credits"`
	Min  int `json:"min_credits"`
}
if err := call("POST", "/estimate", session, nil, &est); err != nil { panic(err) }
fmt.Println(est.Hold, est.Min)
ObjectNode session = JSON.createObjectNode()
    .put("task", "meditate").put("goal", "stress").put("minutes", 10).put("target_words", 590)
    .put("pause_seconds", 332).put("setting", "shore").put("posture", "sitting")
    .put("note", "a long week of deadlines");
JsonNode est = api("POST", "/estimate", session, Map.of());
System.out.println(est.get("hold_credits") + " " + est.get("min_credits"));
session = { task: "meditate", goal: "stress", minutes: 10, target_words: 590, pause_seconds: 332,
            setting: "shore", posture: "sitting", note: "a long week of deadlines" }
est = api("POST", "/estimate", session)
puts est.values_at("hold_credits", "min_credits").inspect
$session = ["task" => "meditate", "goal" => "stress", "minutes" => 10, "target_words" => 590,
    "pause_seconds" => 332, "setting" => "shore", "posture" => "sitting", "note" => "a long week of deadlines"];
$est = api("POST", "/estimate", $session);
echo $est["hold_credits"], " ", $est["min_credits"], PHP_EOL;
var session = new Dictionary<string, object> {
    ["task"] = "meditate", ["goal"] = "stress", ["minutes"] = 10, ["target_words"] = 590,
    ["pause_seconds"] = 332, ["setting"] = "shore", ["posture"] = "sitting", ["note"] = "a long week of deadlines" };
var est = await Meditation.ApiAsync(HttpMethod.Post, "/estimate", session);
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("min_credits")}");

Step 4 - Write the meditation

POST /run   GET /jobs/{job_id}

Send the body with an Idempotency-Key: a retry with the same key and body never bills twice. Poll the job until it is succeeded or failed, then read the JSON Lines from output.output. If truncated is true the balance cut the reply short - keep what arrived.

BODY=$(jq -c . session.json)
KEY="guided-meditation-maker:meditate:$(printf %s "$BODY" | shasum | cut -c1-16):1"
JOB_ID=$(curl -s -X POST "$API/run" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d "$BODY" | jq -r '.data.job_id')

while :; do
  JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $SKILLSAFE_TOKEN")
  STATUS=$(echo "$JOB" | jq -r '.data.status')
  [ "$STATUS" = succeeded ] || [ "$STATUS" = failed ] && break
  sleep 2
done
echo "$JOB" | jq '.data | {status, charged_credits, truncated}'
echo "$JOB" | jq -r '.data.output.output' | jq -R -c 'fromjson? | select(type == "object")' > script.jsonl
jq -r 'if .k == "say" then .text elif .k == "pause" then "   (\(.sec) s)" elif .k == "phase" then "\n== \(.heading)" else empty end' script.jsonl
import hashlib

body = json.dumps(session)
key = "guided-meditation-maker:meditate:" + hashlib.sha1(body.encode()).hexdigest()[:16] + ":1"
job = api("POST", "/run", session, {"Idempotency-Key": key})
while job.get("status") not in ("succeeded", "failed"):
    time.sleep(2)
    job = api("GET", f"/jobs/{job['job_id']}")
lines = []
for raw in (job.get("output") or {}).get("output", "").splitlines():
    try:
        lines.append(json.loads(raw))
    except ValueError:
        pass
for o in lines:
    if o.get("k") == "say": print(o["text"])
    elif o.get("k") == "pause": print(f"   ({o['sec']} s)")
print("complete" if lines and lines[-1].get("k") == "end" else "cut short", job.get("charged_credits"))
import { createHash } from "node:crypto";

const body = JSON.stringify(session);
const key = `guided-meditation-maker:meditate:${createHash("sha1").update(body).digest("hex").slice(0, 16)}:1`;
let job = await api("POST", "/run", session, { "Idempotency-Key": key });
while (!["succeeded", "failed"].includes(job.status)) {
  await new Promise((r) => setTimeout(r, 2000));
  job = await api("GET", `/jobs/${job.job_id}`);
}
const lines = (job.output?.output || "").split("\n").flatMap((l) => { try { return [JSON.parse(l)]; } catch { return []; } });
for (const o of lines) {
  if (o.k === "say") console.log(o.text);
  else if (o.k === "pause") console.log(`   (${o.sec} s)`);
}
console.log(lines.at(-1)?.k === "end" ? "complete" : "cut short", job.charged_credits);
type Job struct {
	JobID   string `json:"job_id"`
	Status  string `json:"status"`
	Charged int    `json:"charged_credits"`
	Output  struct{ Output string `json:"output"` } `json:"output"`
}
var job Job
key := "guided-meditation-maker:meditate:example-1" // any stable hash of the exact body
if err := call("POST", "/run", session, map[string]string{"Idempotency-Key": key}, &job); err != nil { panic(err) }
for job.Status != "succeeded" && job.Status != "failed" {
	time.Sleep(2 * time.Second)
	if err := call("GET", "/jobs/"+job.JobID, nil, nil, &job); err != nil { panic(err) }
}
for _, raw := range strings.Split(job.Output.Output, "\n") { // import "strings"
	var o map[string]any
	if json.Unmarshal([]byte(raw), &o) != nil { continue }
	if o["k"] == "say" { fmt.Println(o["text"]) } else if o["k"] == "pause" { fmt.Printf("   (%v s)\n", o["sec"]) }
}
String key = "guided-meditation-maker:meditate:example-1"; // any stable hash of the exact body
JsonNode job = api("POST", "/run", session, Map.of("Idempotency-Key", key));
while (!job.path("status").asText().matches("succeeded|failed")) {
    Thread.sleep(2000);
    job = api("GET", "/jobs/" + job.get("job_id").asText(), null, Map.of());
}
for (String raw : job.path("output").path("output").asText().split("\n")) {
    try {
        JsonNode o = JSON.readTree(raw);
        if ("say".equals(o.path("k").asText())) System.out.println(o.get("text").asText());
        else if ("pause".equals(o.path("k").asText())) System.out.println("   (" + o.get("sec") + " s)");
    } catch (Exception skip) { }
}
require "digest"

key = "guided-meditation-maker:meditate:#{Digest::SHA1.hexdigest(session.to_json)[0, 16]}:1"
job = api("POST", "/run", session, { "Idempotency-Key" => key })
until %w[succeeded failed].include?(job["status"])
  sleep 2
  job = api("GET", "/jobs/#{job['job_id']}")
end
job.dig("output", "output").to_s.each_line do |raw|
  o = JSON.parse(raw) rescue next
  puts o["text"] if o["k"] == "say"
  puts "   (#{o['sec']} s)" if o["k"] == "pause"
end
$key = "guided-meditation-maker:meditate:" . substr(sha1(json_encode($session)), 0, 16) . ":1";
$job = api("POST", "/run", $session, ["Idempotency-Key" => $key]);
while (!in_array($job["status"] ?? "", ["succeeded", "failed"])) {
    sleep(2);
    $job = api("GET", "/jobs/" . $job["job_id"]);
}
foreach (explode("\n", $job["output"]["output"] ?? "") as $raw) {
    $o = json_decode($raw, true);
    if (!is_array($o)) continue;
    if ($o["k"] === "say") echo $o["text"], PHP_EOL;
    elseif ($o["k"] === "pause") echo "   (", $o["sec"], " s)", PHP_EOL;
}
var key = "guided-meditation-maker:meditate:example-1"; // any stable hash of the exact body
var job = await Meditation.ApiAsync(HttpMethod.Post, "/run", session, new() { ["Idempotency-Key"] = key });
while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"))
{
    await Task.Delay(2000);
    job = await Meditation.ApiAsync(HttpMethod.Get, "/jobs/" + job.GetProperty("job_id").GetString());
}
foreach (var raw in job.GetProperty("output").GetProperty("output").GetString()!.Split('\n'))
{
    try {
        var o = JsonDocument.Parse(raw).RootElement;
        var k = o.GetProperty("k").GetString();
        if (k == "say") Console.WriteLine(o.GetProperty("text").GetString());
        else if (k == "pause") Console.WriteLine($"   ({o.GetProperty("sec")} s)");
    } catch (JsonException) { }
}

Step 5 - Stream a second take

POST /run-stream

A rewrite with a change, over Server-Sent Events: a job event, tick heartbeats, then one done event carrying the whole job. Read the reply from done. An idempotent replay answers with plain JSON instead of a stream, so check the content type.

BODY=$(jq -c '. + {change: "more silence, no counting"}' session.json)
KEY="guided-meditation-maker:meditate:$(printf %s "$BODY" | shasum | cut -c1-16):1"
curl -N -s -X POST "$API/run-stream" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d "$BODY" > stream.txt
sed -n '/^event: done/{n;s/^data: *//p;}' stream.txt | jq -r '(.job // .) | .output.output' > take2.jsonl
def run_stream(body, key):
    h = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}", "Idempotency-Key": key}
    res = requests.post(API + "/run-stream", json=body, headers=h, stream=True, timeout=300)
    if "text/event-stream" not in res.headers.get("content-type", ""):
        return res.json()["data"]
    event = None
    for line in res.iter_lines(decode_unicode=True):
        if line.startswith("event:"):
            event = line[6:].strip()
        elif line.startswith("data:") and event in ("done", "error"):
            data = json.loads(line[5:])
            if event == "error":
                raise RuntimeError(data)
            return data.get("job", data)
    raise RuntimeError("stream ended without a done event")

take2 = dict(session, change="more silence, no counting")
done = run_stream(take2, "guided-meditation-maker:meditate:take2:1")
print(done["output"]["output"])
async function runStream(body, key) {
  const res = await fetch(API + "/run-stream", {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}`, "Idempotency-Key": key },
    body: JSON.stringify(body),
  });
  if (!(res.headers.get("content-type") || "").includes("text/event-stream")) return (await res.json()).data;
  let event = "";
  for (const line of (await res.text()).split("\n")) {
    if (line.startsWith("event:")) event = line.slice(6).trim();
    else if (line.startsWith("data:") && (event === "done" || event === "error")) {
      const data = JSON.parse(line.slice(5));
      if (event === "error") throw new Error(JSON.stringify(data));
      return data.job || data;
    }
  }
  throw new Error("stream ended without a done event");
}
const done = await runStream({ ...session, change: "more silence, no counting" }, "guided-meditation-maker:meditate:take2:1");
console.log(done.output.output);
// Same request as step 4 against /run-stream; read lines with bufio.Scanner and keep the
// data: line that follows "event: done".
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "guided-meditation-maker:meditate:take2:1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<22)
event := ""
for sc.Scan() {
	l := sc.Text()
	if strings.HasPrefix(l, "event:") { event = strings.TrimSpace(l[6:]) }
	if strings.HasPrefix(l, "data:") && event == "done" { fmt.Println(l[5:]); break }
}
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Content-Type", "application/json").header("Authorization", "Bearer " + TOKEN)
    .header("Idempotency-Key", "guided-meditation-maker:meditate:take2:1")
    .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(session.put("change", "more silence"))))
    .build();
String event = "";
for (String line : HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().toList()) {
    if (line.startsWith("event:")) event = line.substring(6).trim();
    else if (line.startsWith("data:") && event.equals("done")) { System.out.println(line.substring(5)); break; }
}
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
                          "Idempotency-Key" => "guided-meditation-maker:meditate:take2:1")
req.body = session.merge(change: "more silence, no counting").to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) { |h| h.request(req) }
event = nil
res.body.each_line do |line|
  event = line[6..].strip if line.start_with?("event:")
  puts line[5..] and break if line.start_with?("data:") && event == "done"
end
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 300,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json", "Authorization: Bearer " . TOKEN,
        "Idempotency-Key: guided-meditation-maker:meditate:take2:1"],
    CURLOPT_POSTFIELDS => json_encode($session + ["change" => "more silence, no counting"])]);
$event = "";
foreach (explode("\n", curl_exec($ch)) as $line) {
    if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
    elseif (str_starts_with($line, "data:") && $event === "done") { echo substr($line, 5), PHP_EOL; break; }
}
session["change"] = "more silence, no counting";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream")
    { Content = JsonContent.Create(session) };
req.Headers.Add("Authorization", "Bearer " + Meditation.Token);
req.Headers.Add("Idempotency-Key", "guided-meditation-maker:meditate:take2:1");
using var http = new HttpClient();
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string? line, ev = "";
while ((line = await reader.ReadLineAsync()) != null)
{
    if (line.StartsWith("event:")) ev = line[6..].Trim();
    else if (line.StartsWith("data:") && ev == "done") { Console.WriteLine(line[5..]); break; }
}

The browser app does the voice and the ambient sound on the listener's device. To make audio from the API reply, feed each say line to any text-to-speech engine and insert the pause silences, scaled so the total lands on minutes.