Long Text Summarizer

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

taskBody label(s) in the replyWhat you get
tldrSUMMARY:Prose paragraphs; every sentence ends with tags like [§3].
bulletsPOINTS:- **Lead:** point [§2] lines, most important first.
chaptersCHAPTERS:## §1-§3 | Title headings, each followed by its summary. Send chapter_hint to suggest a count.
actionsACTIONS: DECISIONS: OPEN_QUESTIONS:- task | owner: Name | due: date [§4]; - None stated for an empty block.
qaQA:Q: / A: ... [§5] pairs.
notesNOTES: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

FieldTypeNotes
taskstring, requiredOne of the six above.
sourcestring, requiredWith 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_kindstringfull_text (default) or notes.
lengthstringshort, standard (default) or detailed.
doc_title, doc_kindstringTitle, and document or transcript (transcripts get speaker attribution).
sectionsstringThe range sent, e.g. §1-§24 (24 sections).
focusstringOptional audience or emphasis, e.g. a busy executive.
chapter_hintnumberOptional, for chapters.
partstringOptional, for notes: 2 of 5.
omitted, retry_notestringOptional. 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

HTTPCodeMeaning
400VALIDATION_ERRORThe body is not a JSON object.
401UNAUTHORIZEDMissing or expired token.
402INSUFFICIENT_CREDITSBalance below the run's minimum. Top up at skillsafe.ai.
403FORBIDDENA guest token tried to run. Sign in for a personal token.
429RATE_LIMITEDToo 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);