Drive Social Post Writer from your own code
Everything the web app does goes through one public surface. Base URL:
https://api.skillsafe.ai/v1/app-api
Every request carries Authorization: Bearer <token> and Content-Type: application/json.
Every response is a JSON envelope: {"ok":true,"data":{…}} on success and
{"ok":false,"error":{"code":"…","message":"…"}} on failure. Read error.code, not only the HTTP status.
Errors
| Code | Meaning | What to do |
|---|---|---|
UNAUTHORIZED | Missing, expired or wrong-app token. | Mint a guest token or sign in again. |
INSUFFICIENT_CREDITS | Balance below min_credits. | Top up. /estimate is free and tells you the hold. |
VALIDATION_ERROR | The input object failed validation. | Check error.details. The body is the input object itself — never wrap it in {"input": …}. |
RATE_LIMITED | Too many requests. | Back off and retry. |
JOB_FAILED | The run started and did not complete. | Retry with the same Idempotency-Key. |
The task field: two lanes
Every input carries task. "write" turns a topic or source text into several variants, each written for every platform you name; "refine" rewrites one platform version of one variant. Every field is a string; arrays and objects travel JSON-encoded.
| Field | Lane | Meaning |
|---|---|---|
task | both, required | "write" or "refine". |
mode | write | "topic" (write from an idea) or "source" (every claim comes from the pasted text). |
text | write, required | The topic or source text. The web app sends at most 12,000 characters, keeping the opening and the ending with a [... N characters cut ...] marker. |
platforms | write, required | JSON array, a subset of ["x","threads","linkedin","bluesky"]. |
format | write | "auto", "single" or "thread". |
tone | both | professional · casual · witty · bold · educational · inspiring · storytelling · announcement · empathetic |
variants | write | "1" to "4". |
emoji | both | "no", "some" (at most one) or "yes" (at most three). |
audience, goal, cta | write (cta both) | Optional. goal is awareness · engagement · clicks · followers · hiring; cta is a link or a call to action. |
voice | write | Optional: a few of your own past posts, to match rhythm and habits. Never copied. |
language | both | "auto" (the language of the text) or a language name. |
limits | both | JSON object of character limits, e.g. {"x":280,"threads":500,"bluesky":300,"linkedin":3000}. |
platform | refine, required | The one platform being rewritten. |
posts | refine, required | JSON array of the current post texts (one post or a thread). |
instruction | refine, required | shorter · punchier · more_formal · more_casual · stronger_hook · add_cta · to_thread · to_single · custom |
note | refine | Your own instruction; required when instruction is custom. |
retry_note | both | Sent only on the one automatic reformat retry. |
The output contract
The output (output.output) is one JSON object. For write: task, status (ok · need_more · refused), message, title,
variants[] (each: angle, hook_type, platforms as {"x":{"posts":[...]}, ...}, hashtags[], image_idea, alt_text), placeholders[] and notes.
Posts carry no hashtags and no "1/5" numbering: add the tags you want and number the thread yourself. Square-bracket placeholders such as [link] mark a fact that was not given; fill them in before posting. The web app counts each post the way its platform does (on X a link counts as 23 and CJK or emoji as two) and offers a split when one runs over.
For refine: task, status, message, platform, posts[], hashtags[], change_note.
1. Get a token
A guest token is enough for /me and /estimate. Running is metered and needs a personal token from signing in. The tokens page shows yours and copies it. Replace YOUR_TOKEN below with it.
2. Check the session and the balance
/me returns subject_type, subject_id and credits. Signed in means subject_type === "user".
curl -s "https://api.skillsafe.ai/v1/app-api/me" \ -H "Authorization: Bearer YOUR_TOKEN"
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
r = requests.get(f"{BASE}/me", headers=H, timeout=30).json()
print(r["data"]["subject_type"], r["data"]["credits"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const r = await (await fetch(`${BASE}/me`, { headers: H })).json();
console.log(r.data.subject_type, r.data.credits);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("GET", base+"/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
_ = bytes.NewReader
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/me"))
.header("Authorization", "Bearer " + token).GET().build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init("$base/me");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await http.GetStringAsync($"{baseUrl}/me"));
3. Price it before you run it
/estimate is free and starts no job. hold_credits is a reservation priced against the longest possible answer, not the price. Estimate each lane you use. Write:
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "write", "mode": "topic", "text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.", "platforms": "[\"x\", \"linkedin\", \"bluesky\"]", "format": "auto", "tone": "announcement", "variants": "3", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}'
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
body = {
"task": "write",
"mode": "topic",
"text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.",
"platforms": "[\"x\", \"linkedin\", \"bluesky\"]",
"format": "auto",
"tone": "announcement",
"variants": "3",
"emoji": "some",
"language": "auto",
"limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"
}
r = requests.post(f"{BASE}/estimate", headers={**H}, json=body, timeout=120).json()
print(r["data"]["hold_credits"], r["data"]["model_alias"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const body = {
"task": "write",
"mode": "topic",
"text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.",
"platforms": "[\"x\", \"linkedin\", \"bluesky\"]",
"format": "auto",
"tone": "announcement",
"variants": "3",
"emoji": "some",
"language": "auto",
"limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"
};
const r = await (await fetch(`${BASE}/estimate`, { method: "POST", headers: { ...H }, body: JSON.stringify(body) })).json();
console.log(r.data.hold_credits, r.data.model_alias);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"task": "write", "mode": "topic", "text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.", "platforms": "[\"x\", \"linkedin\", \"bluesky\"]", "format": "auto", "tone": "announcement", "variants": "3", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}`)
req, _ := http.NewRequest("POST", base+"/estimate", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}";
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/estimate"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = "{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$body = "{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}";
$ch = curl_init("$base/estimate");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent("{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}", Encoding.UTF8, "application/json");
var res = await http.PostAsync($"{baseUrl}/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Refine (worked example for the second lane):
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "refine", "platform": "x", "posts": "[\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\"]", "instruction": "to_thread", "note": "keep it under four posts", "tone": "announcement", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}'
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
body = {
"task": "refine",
"platform": "x",
"posts": "[\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\"]",
"instruction": "to_thread",
"note": "keep it under four posts",
"tone": "announcement",
"emoji": "some",
"language": "auto",
"limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"
}
r = requests.post(f"{BASE}/estimate", headers={**H}, json=body, timeout=120).json()
print(r["data"]["hold_credits"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const body = {
"task": "refine",
"platform": "x",
"posts": "[\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\"]",
"instruction": "to_thread",
"note": "keep it under four posts",
"tone": "announcement",
"emoji": "some",
"language": "auto",
"limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"
};
const r = await (await fetch(`${BASE}/estimate`, { method: "POST", headers: { ...H }, body: JSON.stringify(body) })).json();
console.log(r.data.hold_credits);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"task": "refine", "platform": "x", "posts": "[\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\"]", "instruction": "to_thread", "note": "keep it under four posts", "tone": "announcement", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}`)
req, _ := http.NewRequest("POST", base+"/estimate", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"task\": \"refine\", \"platform\": \"x\", \"posts\": \"[\\\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\\\"]\", \"instruction\": \"to_thread\", \"note\": \"keep it under four posts\", \"tone\": \"announcement\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}";
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/estimate"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = "{\"task\": \"refine\", \"platform\": \"x\", \"posts\": \"[\\\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\\\"]\", \"instruction\": \"to_thread\", \"note\": \"keep it under four posts\", \"tone\": \"announcement\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$body = "{\"task\": \"refine\", \"platform\": \"x\", \"posts\": \"[\\\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\\\"]\", \"instruction\": \"to_thread\", \"note\": \"keep it under four posts\", \"tone\": \"announcement\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}";
$ch = curl_init("$base/estimate");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent("{\"task\": \"refine\", \"platform\": \"x\", \"posts\": \"[\\\"We just shipped dark mode in v2.4. It follows your OS setting, has a manual toggle, and every theme passed a WCAG AA contrast check.\\\"]\", \"instruction\": \"to_thread\", \"note\": \"keep it under four posts\", \"tone\": \"announcement\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}", Encoding.UTF8, "application/json");
var res = await http.PostAsync($"{baseUrl}/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
4. Run and poll
/run returns a job. Always send an Idempotency-Key — the same key is the same run, so a retry cannot bill twice. The web app derives it from the lane and a hash of the input.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: social-post-writer:write:8f3ka2-1x:mh2k1:a1" \
-d '{"task": "write", "mode": "topic", "text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.", "platforms": "[\"x\", \"linkedin\", \"bluesky\"]", "format": "auto", "tone": "announcement", "variants": "3", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}'
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
body = {
"task": "write",
"mode": "topic",
"text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.",
"platforms": "[\"x\", \"linkedin\", \"bluesky\"]",
"format": "auto",
"tone": "announcement",
"variants": "3",
"emoji": "some",
"language": "auto",
"limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"
}
r = requests.post(f"{BASE}/run", headers={**H, "Idempotency-Key": "social-post-writer:write:8f3ka2-1x:mh2k1:a1"}, json=body, timeout=120).json()
job_id = r["data"]["job_id"]
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const body = {
"task": "write",
"mode": "topic",
"text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.",
"platforms": "[\"x\", \"linkedin\", \"bluesky\"]",
"format": "auto",
"tone": "announcement",
"variants": "3",
"emoji": "some",
"language": "auto",
"limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"
};
const r = await (await fetch(`${BASE}/run`, { method: "POST", headers: { ...H, "Idempotency-Key": "social-post-writer:write:8f3ka2-1x:mh2k1:a1" }, body: JSON.stringify(body) })).json();
const jobId = r.data.job_id;
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"task": "write", "mode": "topic", "text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.", "platforms": "[\"x\", \"linkedin\", \"bluesky\"]", "format": "auto", "tone": "announcement", "variants": "3", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}`)
req, _ := http.NewRequest("POST", base+"/run", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "social-post-writer:write:8f3ka2-1x:mh2k1:a1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}";
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/run"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.header("Idempotency-Key", "social-post-writer:write:8f3ka2-1x:mh2k1:a1")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "social-post-writer:write:8f3ka2-1x:mh2k1:a1"
req.body = "{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$body = "{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}";
$ch = curl_init("$base/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json", "Idempotency-Key: social-post-writer:write:8f3ka2-1x:mh2k1:a1"],
]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent("{\"task\": \"write\", \"mode\": \"topic\", \"text\": \"We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.\", \"platforms\": \"[\\\"x\\\", \\\"linkedin\\\", \\\"bluesky\\\"]\", \"format\": \"auto\", \"tone\": \"announcement\", \"variants\": \"3\", \"emoji\": \"some\", \"language\": \"auto\", \"limits\": \"{\\\"x\\\": 280, \\\"threads\\\": 500, \\\"bluesky\\\": 300, \\\"linkedin\\\": 3000}\"}", Encoding.UTF8, "application/json");
http.DefaultRequestHeaders.Add("Idempotency-Key", "social-post-writer:write:8f3ka2-1x:mh2k1:a1");
var res = await http.PostAsync($"{baseUrl}/run", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID" \ -H "Authorization: Bearer YOUR_TOKEN" # repeat until data.status is "succeeded" or "failed"; # the batch is the JSON string at data.output.output
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
import json, time
job_id = "JOB_ID"
while True:
job = requests.get(f"{BASE}/jobs/{job_id}", headers=H, timeout=30).json()["data"]
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1)
batch = json.loads(job["output"]["output"])
for v in batch["variants"]:
print(v["angle"], v["platforms"]["x"]["posts"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
let job;
for (;;) {
job = (await (await fetch(`${BASE}/jobs/JOB_ID`, { headers: H })).json()).data;
if (job.status === "succeeded" || job.status === "failed") break;
await new Promise((r) => setTimeout(r, 1000));
}
const batch = JSON.parse(job.output.output);
for (const v of batch.variants) console.log(v.angle, v.platforms.x.posts);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
// GET base+"/jobs/JOB_ID" every second until data.status is
// "succeeded" or "failed", then json.Unmarshal data.output.output.
func main() { fmt.Println("poll", base+"/jobs/JOB_ID"); _ = bytes.NewReader; _ = json.Marshal; _ = http.Get }
var token = "YOUR_TOKEN"; var base = "https://api.skillsafe.ai/v1/app-api"; var http = java.net.http.HttpClient.newHttpClient(); // GET base + "/jobs/JOB_ID" every second until data.status is // "succeeded" or "failed"; the batch is the JSON string at data.output.output.
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
loop do
uri = URI("#{BASE}/jobs/JOB_ID")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
job = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]
if %w[succeeded failed].include?(job["status"])
puts JSON.parse(job["output"]["output"])["variants"]
break
end
sleep 1
end
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
do {
$ch = curl_init("$base/jobs/JOB_ID");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]);
$job = json_decode(curl_exec($ch), true)["data"];
if (!in_array($job["status"], ["succeeded", "failed"])) sleep(1);
} while (!in_array($job["status"], ["succeeded", "failed"]));
print_r(json_decode($job["output"]["output"], true)["variants"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// GET {baseUrl}/jobs/JOB_ID every second until data.status is
// "succeeded" or "failed"; the batch is the JSON string at data.output.output.
5. Stream it
/run-stream answers with server-sent events and ends with a done frame carrying the whole output, charged_credits and truncated. If truncated is true the balance ran low mid-answer: parse what arrived and top up for the rest.
curl -sN -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: social-post-writer:write:8f3ka2-1x:mh2k1:a1" \
-d '{"task": "write", "mode": "topic", "text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.", "platforms": "[\"x\", \"linkedin\", \"bluesky\"]", "format": "auto", "tone": "announcement", "variants": "3", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}'
# event: job data: {"job_id": ...}
# event: delta data: {"text": "..."} (may be absent: a browser often gets only
# event: tick heartbeats and the whole answer in the final frame)
# event: done data: {"output": {"output": "<the JSON batch>"}, "charged_credits": ..., "truncated": false}
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
import json
body = {"task": "write", "mode": "topic", "text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.", "platforms": "[\"x\", \"linkedin\", \"bluesky\"]", "format": "auto", "tone": "announcement", "variants": "3", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}
with requests.post(f"{BASE}/run-stream", json=body, stream=True, timeout=300,
headers={**H, "Accept": "text/event-stream", "Idempotency-Key": "social-post-writer:write:8f3ka2-1x:mh2k1:a1"}) as r:
event, done = None, None
for line in r.iter_lines(decode_unicode=True):
if line.startswith("event:"): event = line[6:].strip()
elif line.startswith("data:") and event == "done": done = json.loads(line[5:])
batch = json.loads(done["output"]["output"])
print(len(batch["variants"]), done.get("charged_credits"), done.get("truncated"))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const res = await fetch(`${BASE}/run-stream`, { method: "POST",
headers: { ...H, Accept: "text/event-stream", "Idempotency-Key": "social-post-writer:write:8f3ka2-1x:mh2k1:a1" },
body: JSON.stringify({"task": "write", "mode": "topic", "text": "We just shipped dark mode in v2.4: follows the OS setting, manual toggle, every theme passed a WCAG AA contrast check.", "platforms": "[\"x\", \"linkedin\", \"bluesky\"]", "format": "auto", "tone": "announcement", "variants": "3", "emoji": "some", "language": "auto", "limits": "{\"x\": 280, \"threads\": 500, \"bluesky\": 300, \"linkedin\": 3000}"}) });
const text = await res.text(); // or read res.body incrementally
let done = null;
for (const frame of text.split("\n\n")) {
const ev = /^event: *(.*)$/m.exec(frame), data = /^data: *(.*)$/m.exec(frame);
if (ev && ev[1] === "done" && data) done = JSON.parse(data[1]);
}
const batch = JSON.parse(done.output.output);
console.log(batch.variants.length, done.truncated);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
// POST base+"/run-stream" with Accept: text/event-stream, read lines with
// bufio.Scanner, and json.Unmarshal the data line of the "done" event.
func main() { fmt.Println("see the cURL tab for the frame format"); _ = bytes.NewReader; _ = json.Marshal; _ = http.Get }
var token = "YOUR_TOKEN"; var base = "https://api.skillsafe.ai/v1/app-api"; var http = java.net.http.HttpClient.newHttpClient(); // POST base + "/run-stream" with Accept: text/event-stream, read the body // line by line (BodyHandlers.ofLines()), and parse the data line of the "done" event.
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
# POST to "#{BASE}/run-stream" with Accept: text/event-stream; read the body,
# split frames on blank lines, and JSON.parse the data line of the "done" event.
<?php $token = "YOUR_TOKEN"; $base = "https://api.skillsafe.ai/v1/app-api"; // POST to "$base/run-stream" with Accept: text/event-stream; split the body on // blank lines and json_decode the data line of the "done" event.
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// POST {baseUrl}/run-stream with Accept: text/event-stream; read the response
// stream line by line and parse the data line of the "done" event.