Long Text Summarizer API
The app reads and sections the text in the browser, then sends one run per summary (or, for a long text, one notes run per part plus a final run). The API takes exactly the same input, so you can summarise from a script: you cut the text into numbered sections, and every point in the reply comes back tagged with the sections it came from.
Base URL: https://api.skillsafe.ai/v1/app-api. Every response is {"data": ...} on success or {"error": {"code", "message"}} on failure. The request body is the input object - do not wrap it in {"input": ...}.
The task field comes first
task | Body label(s) in the reply | What you get |
|---|---|---|
tldr | SUMMARY: | Prose paragraphs; every sentence ends with tags like [§3]. |
bullets | POINTS: | - **Lead:** point [§2] lines, most important first. |
chapters | CHAPTERS: | ## §1-§3 | Title headings, each followed by its summary. Send chapter_hint to suggest a count. |
actions | ACTIONS: DECISIONS: OPEN_QUESTIONS: | - task | owner: Name | due: date [§4]; - None stated for an empty block. |
qa | QA: | Q: / A: ... [§5] pairs. |
notes | NOTES: | The first pass for long texts: one §n | dense notes line per section sent. Feed the joined notes back with source_kind: "notes" and a final task. |
Every reply except notes is FORMAT:, TITLE:, GIST:, the body block(s), then a line END. A missing END means the reply was cut short.
Input
| Field | Type | Notes |
|---|---|---|
task | string, required | One of the six above. |
source | string, required | With full_text: sections, each a header line [§n] time · page · heading (any part optional) then its text, separated by a blank line. With notes: lines §n | notes. Keep one run under about 140,000 characters. |
source_kind | string | full_text (default) or notes. |
length | string | short, standard (default) or detailed. |
doc_title, doc_kind | string | Title, and document or transcript (transcripts get speaker attribution). |
sections | string | The range sent, e.g. §1-§24 (24 sections). |
focus | string | Optional audience or emphasis, e.g. a busy executive. |
chapter_hint | number | Optional, for chapters. |
part | string | Optional, for notes: 2 of 5. |
omitted, retry_note | string | Optional. The app sets them when the middle of a huge text was left out, and on its one automatic reformat retry. |
Output
The finished job's output.output is plain text. For the example below (bullets, short):
FORMAT: bullets
TITLE: Checkout delay and staged rollout
GIST: Checkout slipped two weeks after a payments API change; a new adapter by the 14th sets up a staged rollout. [§1][§2]
POINTS:
- **Delay:** Checkout is two weeks behind because the payments vendor changed its API. [§1]
- **Fix:** Raj will finish the new adapter by Friday the 14th. [§1]
- **Rollout:** 10% of users on the 18th, everyone on the 25th if refunds stay under 1%. [§2]
END
Check the tags against what you sent: the app flags tags naming a section that was not sent, points with no tag, and sections nothing in the summary draws on.
Errors
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object. |
| 401 | UNAUTHORIZED | Missing or expired token. |
| 402 | INSUFFICIENT_CREDITS | Balance below the run's minimum. Top up at skillsafe.ai. |
| 403 | FORBIDDEN | A guest token tried to run. Sign in for a personal token. |
| 429 | RATE_LIMITED | Too many requests. Back off and retry. |
1. A tiny client
# Every call: Authorization: Bearer $TOKEN, JSON in, {"data": ...} or {"error": ...} out.
TOKEN="YOUR_TOKEN"
BASE="https://api.skillsafe.ai/v1/app-api"
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json", **(headers or {})})
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body, headers = {}) {
const res = await fetch(BASE + path, {
method,
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json", ...headers },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error ? json.error.code + ": " + json.error.message : res.status);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func call(method, path string, body any, out any) error {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct{ Data json.RawMessage `json:"data"` }
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
return json.Unmarshal(env.Data, out)
}
import java.net.URI;
import java.net.http.*;
public class Api {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static final HttpClient http = HttpClient.newHttpClient();
static String call(String method, String path, String json) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json");
b = json == null ? b.method(method, HttpRequest.BodyPublishers.noBody())
: b.method(method, HttpRequest.BodyPublishers.ofString(json));
return http.send(b.build(), HttpResponse.BodyHandlers.ofString()).body(); // {"data": ...}
}
}
require "net/http"
require "json"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
def call(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
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) { |h| h.request(req) }
JSON.parse(res.body)["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
function call($method, $path, $body = null, $headers = []) {
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array_merge(["Authorization: Bearer " . TOKEN, "Content-Type: application/json"], $headers),
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$out = json_decode(curl_exec($ch), true);
return $out["data"] ?? $out;
}
using System.Net.Http;
using System.Text;
using System.Text.Json;
var http = new HttpClient { BaseAddress = new Uri("https://api.skillsafe.ai/v1/app-api/") };
http.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_TOKEN");
async Task<JsonElement> Call(HttpMethod m, string path, object? body = null) {
var req = new HttpRequestMessage(m, path.TrimStart('/'));
if (body != null) req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("data").Clone();
}
2. Get a token
Guests can price a run; running needs a personal token. The easiest way to get one is the token page.
# A guest token (enough for /me and /estimate). A run needs a personal token:
# sign in on https://long-text-summarizer.skillsafe.ai/tokens.html and copy it from there.
curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"long-text-summarizer"}'
guest = json.load(urllib.request.urlopen(urllib.request.Request(
BASE + "/guest", data=json.dumps({"slug": "long-text-summarizer"}).encode(),
headers={"Content-Type": "application/json"})))["data"]
print(guest["token"]) # guest: /me and /estimate only. Personal token: tokens.html
const g = await fetch(BASE + "/guest", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "long-text-summarizer" }),
}).then((r) => r.json());
console.log(g.data.token); // personal token for runs: tokens.html
// POST {"slug":"long-text-summarizer"} to base+"/guest" without a token; read data.token.
// For runs, copy a personal token from https://long-text-summarizer.skillsafe.ai/tokens.html
// POST {"slug":"long-text-summarizer"} to BASE + "/guest" (no Authorization header) and read data.token.
// Runs need a personal token from https://long-text-summarizer.skillsafe.ai/tokens.html
uri = URI(BASE + "/guest")
res = Net::HTTP.post(uri, { slug: "long-text-summarizer" }.to_json, "Content-Type" => "application/json")
puts JSON.parse(res.body)["data"]["token"]
$ch = curl_init(BASE . "/guest");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode(["slug" => "long-text-summarizer"])]);
echo json_decode(curl_exec($ch), true)["data"]["token"];
var g = await new HttpClient().PostAsync("https://api.skillsafe.ai/v1/app-api/guest",
new StringContent("{\"slug\":\"long-text-summarizer\"}", Encoding.UTF8, "application/json"));
Console.WriteLine(await g.Content.ReadAsStringAsync()); // data.token
3. Who am I, and my balance
curl -s "$BASE/me" -H "Authorization: Bearer $TOKEN"
me = call("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
var me map[string]any
call("GET", "/me", nil, &me)
System.out.println(Api.call("GET", "/me", null));
me = call("GET", "/me")
puts me["credits"]
$me = call("GET", "/me");
echo $me["credits"];
var me = await Call(HttpMethod.Get, "/me");
Console.WriteLine(me);
4. Price a run (free)
hold_credits is the most a run can cost, reserved up front; you are charged only what it uses. 10,000 credits = $1.
curl -s -X POST "$BASE/estimate" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"task": "bullets", "length": "short", "source_kind": "full_text", "doc_title": "Checkout sync", "doc_kind": "transcript", "sections": "§1-§2 (2 sections)", "source": "[§1] 0:00–4:10\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\nRaj: I can have the new adapter done by Friday the 14th.\n\n[§2] 4:10–8:30\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%."}'
body = {
"task": "bullets",
"length": "short",
"source_kind": "full_text",
"doc_title": "Checkout sync",
"doc_kind": "transcript",
"sections": "§1-§2 (2 sections)",
"source": "[§1] 0:00–4:10\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\nRaj: I can have the new adapter done by Friday the 14th.\n\n[§2] 4:10–8:30\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%."
}
est = call("POST", "/estimate", body)
print(est["hold_credits"], est["model_alias"], est.get("warnings"))
const body = {
"task": "bullets",
"length": "short",
"source_kind": "full_text",
"doc_title": "Checkout sync",
"doc_kind": "transcript",
"sections": "§1-§2 (2 sections)",
"source": "[§1] 0:00–4:10\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\nRaj: I can have the new adapter done by Friday the 14th.\n\n[§2] 4:10–8:30\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%."
};
const est = await call("POST", "/estimate", body);
console.log(est.hold_credits, est.model_alias, est.warnings);
body := map[string]string{
"task": "bullets",
"length": "short",
"source_kind": "full_text",
"doc_title": "Checkout sync",
"source": "[§1] 0:00\nDana: We're two weeks behind...",
}
var est map[string]any
call("POST", "/estimate", body, &est)
String body = "{\"task\": \"bullets\", \"length\": \"short\", \"source_kind\": \"full_text\", \"doc_title\": \"Checkout sync\", \"doc_kind\": \"transcript\", \"sections\": \"§1-§2 (2 sections)\", \"source\": \"[§1] 0:00–4:10\\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\\nRaj: I can have the new adapter done by Friday the 14th.\\n\\n[§2] 4:10–8:30\\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%.\"}";
System.out.println(Api.call("POST", "/estimate", body));
body = JSON.parse("{\"task\": \"bullets\", \"length\": \"short\", \"source_kind\": \"full_text\", \"doc_title\": \"Checkout sync\", \"doc_kind\": \"transcript\", \"sections\": \"§1-§2 (2 sections)\", \"source\": \"[§1] 0:00–4:10\\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\\nRaj: I can have the new adapter done by Friday the 14th.\\n\\n[§2] 4:10–8:30\\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%.\"}")
est = call("POST", "/estimate", body)
puts est["hold_credits"]
$body = json_decode("{\"task\": \"bullets\", \"length\": \"short\", \"source_kind\": \"full_text\", \"doc_title\": \"Checkout sync\", \"doc_kind\": \"transcript\", \"sections\": \"§1-§2 (2 sections)\", \"source\": \"[§1] 0:00–4:10\\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\\nRaj: I can have the new adapter done by Friday the 14th.\\n\\n[§2] 4:10–8:30\\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%.\"}", true);
$est = call("POST", "/estimate", $body);
echo $est["hold_credits"];
var body = JsonSerializer.Deserialize<JsonElement>("{\"task\": \"bullets\", \"length\": \"short\", \"source_kind\": \"full_text\", \"doc_title\": \"Checkout sync\", \"doc_kind\": \"transcript\", \"sections\": \"§1-§2 (2 sections)\", \"source\": \"[§1] 0:00–4:10\\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\\nRaj: I can have the new adapter done by Friday the 14th.\\n\\n[§2] 4:10–8:30\\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%.\"}");
var est = await Call(HttpMethod.Post, "/estimate", body);
5. Run it and poll
Always send an Idempotency-Key: a retry with the same key never bills twice.
curl -s -X POST "$BASE/run" -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: my-sum-001" \
-H "Content-Type: application/json" -d '{"task": "bullets", "length": "short", "source_kind": "full_text", "doc_title": "Checkout sync", "doc_kind": "transcript", "sections": "§1-§2 (2 sections)", "source": "[§1] 0:00–4:10\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\nRaj: I can have the new adapter done by Friday the 14th.\n\n[§2] 4:10–8:30\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%."}'
# -> data.job_id; then poll until status is succeeded or failed:
curl -s "$BASE/jobs/JOB_ID" -H "Authorization: Bearer $TOKEN"
import time
job = call("POST", "/run", body, {"Idempotency-Key": "my-sum-001"})
while job.get("status") not in ("succeeded", "failed"):
time.sleep(1)
job = call("GET", "/jobs/" + job["job_id"])
print(job["output"]["output"]) # FORMAT / TITLE / GIST / POINTS / END
let job = await call("POST", "/run", body, { "Idempotency-Key": "my-sum-001" });
while (!["succeeded", "failed"].includes(job.status)) {
await new Promise((r) => setTimeout(r, 1000));
job = await call("GET", "/jobs/" + job.job_id);
}
console.log(job.output.output);
var job map[string]any
call("POST", "/run", body, &job) // set Idempotency-Key on the request too
for job["status"] != "succeeded" && job["status"] != "failed" {
time.Sleep(time.Second)
call("GET", "/jobs/"+job["job_id"].(string), nil, &job)
}
String job = Api.call("POST", "/run", body); // add header Idempotency-Key
// read data.job_id, then GET /jobs/{job_id} every second until status is succeeded or failed
job = call("POST", "/run", body, "Idempotency-Key" => "my-sum-001")
until %w[succeeded failed].include?(job["status"])
sleep 1
job = call("GET", "/jobs/#{job['job_id']}")
end
puts job["output"]["output"]
$job = call("POST", "/run", $body, ["Idempotency-Key: my-sum-001"]);
while (!in_array($job["status"], ["succeeded", "failed"])) {
sleep(1);
$job = call("GET", "/jobs/" . $job["job_id"]);
}
echo $job["output"]["output"];
var job = await Call(HttpMethod.Post, "/run", body); // add Idempotency-Key header
while (job.GetProperty("status").GetString() is not ("succeeded" or "failed")) {
await Task.Delay(1000);
job = await Call(HttpMethod.Get, "/jobs/" + job.GetProperty("job_id").GetString());
}
6. Or stream it
curl -N -X POST "$BASE/run-stream" -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: my-sum-002" \
-H "Content-Type: application/json" -d '{"task": "bullets", "length": "short", "source_kind": "full_text", "doc_title": "Checkout sync", "doc_kind": "transcript", "sections": "§1-§2 (2 sections)", "source": "[§1] 0:00–4:10\nDana: We're two weeks behind on checkout because the payments vendor changed their API.\nRaj: I can have the new adapter done by Friday the 14th.\n\n[§2] 4:10–8:30\nDana: Then we ship to 10% of users on the 18th and to everyone on the 25th if refunds stay under 1%."}'
# event: job / event: delta {"text":...} / event: done {charged_credits, truncated, output}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json", "Idempotency-Key": "my-sum-002"})
with urllib.request.urlopen(req) as r:
for line in r:
print(line.decode().rstrip()) # event: / data: lines
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json", "Idempotency-Key": "my-sum-002" },
body: JSON.stringify(body),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
console.log(dec.decode(value)); // frames split on a blank line
}
// POST to base+"/run-stream" with the same headers plus Idempotency-Key,
// then read res.Body with bufio.Scanner: lines are "event: ..." and "data: {...}".
// POST to BASE + "/run-stream" and read HttpResponse.BodyHandlers.ofLines():
// "event: delta" lines carry data: {"text": ...}; "event: done" carries the final job.
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json", "Idempotency-Key" => "my-sum-002")
req.body = body.to_json
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) { |res| res.read_body { |c| print c } } }
// Use CURLOPT_WRITEFUNCTION on a POST to BASE . "/run-stream" to receive SSE frames as they arrive.
var req = new HttpRequestMessage(HttpMethod.Post, "run-stream") {
Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json") };
req.Headers.Add("Idempotency-Key", "my-sum-002");
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await res.Content.ReadAsStreamAsync());
while (await sr.ReadLineAsync() is string line) Console.WriteLine(line);