← Text Insight/API
Your token

Text Insight API

Entities, the graph, words, keywords, sentiment and readability all run in the browser and have no API: the page's own scripts do the work and nothing is uploaded. What you can call from code is the paid Relations run: one part of a text (at most 12,000 characters) and the entity names found in it go to an OpenAI model (gpt-terra, served by SkillSafe), which returns who-did-what triples, each with the supporting text quoted, as one JSON object.

Base URL https://api.skillsafe.ai/v1/app-api. Every response is {"ok":true,"data":…} or {"ok":false,"error":{"code","message"}}. A run is paid with SkillSafe credits from the signed-in account; /me and /estimate are free.

Errors

HTTPcodemeaning
400validation_errorBody is not a JSON object.
401unauthorizedMissing or expired token, or a guest token on a run: sign in for a personal token.
402insufficient_creditsBalance below the run's minimum. Top up at skillsafe.ai.
429rate_limitedSlow down and retry with the same Idempotency-Key.

1. Get a token

Open the token page, sign in, and copy your token. A guest token can call /me and /estimate but cannot run.

2. Check the session — GET /me

curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "Authorization: Bearer YOUR_TOKEN"
import requests
H = {"Authorization": "Bearer YOUR_TOKEN"}
print(requests.get("https://api.skillsafe.ai/v1/app-api/me", headers=H).json()["data"])
const H = { Authorization: "Bearer YOUR_TOKEN" };
const me = await (await fetch("https://api.skillsafe.ai/v1/app-api/me", { headers: H })).json();
console.log(me.data.subject_type, me.data.credits);

3. Input fields

The page builds this object for you and shows it under Exactly what is sent on the Relations tab. From code, split long texts into parts of whole paragraphs, at most 12,000 characters each, and send one part per run.

fieldtyperequiredmeaning
taskstringyesAlways "relations".
textstringyesOne part of the text, verbatim. Every quote in the reply is copied from it.
partobjectno{"index","of","chars","first_sentence","last_sentence"}: which part this is, 1-based.
entitiesarraynoUp to 150 {"name","type","also"} found in this part; type is person, place, org, date, money or percent, and also lists other spellings. Subjects and objects use these names exactly.
max_triplesnumbernoThe most triples to return; the page sends 40.
entities_notestringnoSay so when the entity list was shortened, e.g. "Showing the 150 most-mentioned of 212 entities found in this part."

4. Price it — POST /estimate (free)

hold_credits is the most the run can reserve; you are charged charged_credits, usually far less. 10,000 credits = $1.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
  -d '{"task":"relations","text":"Ada Okafor founded Brightline in Lagos in 2019. Brightline acquired Pemba Foods for $4 million.","part":{"index":1,"of":1,"chars":95,"first_sentence":1,"last_sentence":2},"entities":[{"name":"Ada Okafor","type":"person"},{"name":"Brightline","type":"org"},{"name":"Lagos","type":"place"},{"name":"Pemba Foods","type":"org"}],"max_triples":40}'
text = ("Ada Okafor founded Brightline in Lagos in 2019. "
        "Brightline acquired Pemba Foods for $4 million.")
body = {"task": "relations", "text": text,
        "part": {"index": 1, "of": 1, "chars": len(text), "first_sentence": 1, "last_sentence": 2},
        "entities": [{"name": "Ada Okafor", "type": "person"}, {"name": "Brightline", "type": "org"},
                     {"name": "Lagos", "type": "place"}, {"name": "Pemba Foods", "type": "org"}],
        "max_triples": 40}
print(requests.post("https://api.skillsafe.ai/v1/app-api/estimate", headers=H, json=body).json()["data"])
const text = "Ada Okafor founded Brightline in Lagos in 2019. Brightline acquired Pemba Foods for $4 million.";
const body = { task: "relations", text,
  part: { index: 1, of: 1, chars: text.length, first_sentence: 1, last_sentence: 2 },
  entities: [{ name: "Ada Okafor", type: "person" }, { name: "Brightline", type: "org" },
             { name: "Lagos", type: "place" }, { name: "Pemba Foods", type: "org" }],
  max_triples: 40 };
const est = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", { method: "POST",
  headers: { ...H, "Content-Type": "application/json" }, body: JSON.stringify(body) });
console.log((await est.json()).data);

5. Run — POST /run, then poll GET /jobs/{id}

The body is the same object as the estimate. Send an Idempotency-Key: a retry with the same key never bills twice.

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: relations-2026-11-01-001" \
  -d @body.json   # body.json = the object from step 4
# -> {"ok":true,"data":{"job_id":"job_...", ...}}
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 reply text is data.output.output
import json, time
r = requests.post("https://api.skillsafe.ai/v1/app-api/run",
                  headers={**H, "Idempotency-Key": "relations-2026-11-01-001"}, json=body)
job_id = r.json()["data"]["job_id"]
while True:
    job = requests.get(f"https://api.skillsafe.ai/v1/app-api/jobs/{job_id}", headers=H).json()["data"]
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(1.5)
answer = json.loads(job["output"]["output"])
for t in answer["triples"]:
    print(t["subject"], "|", t["relation"], "|", t["object"])
print(job["charged_credits"])
const run = await fetch("https://api.skillsafe.ai/v1/app-api/run", { method: "POST",
  headers: { ...H, "Content-Type": "application/json", "Idempotency-Key": "relations-2026-11-01-001" },
  body: JSON.stringify(body) });
const jobId = (await run.json()).data.job_id;
let job;
do {
  await new Promise((r) => setTimeout(r, 1500));
  job = (await (await fetch(`https://api.skillsafe.ai/v1/app-api/jobs/${jobId}`, { headers: H })).json()).data;
} while (job.status !== "succeeded" && job.status !== "failed");
const answer = JSON.parse(job.output.output);
console.log(answer.triples, job.charged_credits);

Output contract

output.output is one JSON object. If a reply is cut off, the page keeps the complete triples that arrived and says the answer was cut short; do the same, or run again.

{
  "task": "relations",
  "triples": [
    {"subject": "Ada Okafor", "relation": "founded", "object": "Brightline",
     "evidence": "Ada Okafor founded Brightline in Lagos in 2019.", "confidence": "high"},
    {"subject": "Brightline", "relation": "acquired", "object": "Pemba Foods",
     "evidence": "Brightline acquired Pemba Foods for $4 million.", "confidence": "high"}
  ],
  "notes": "A short company history."
}
keyvalues
subject, objectAn entity name exactly as sent, or the shortest noun phrase that appears word for word in the text. Never a pronoun.
relationA lowercase verb phrase of 1 to 5 words that reads subject, relation, object.
evidenceCopied from text: one sentence or a shorter span, or two exact pieces of one paragraph joined by ....
confidencehigh (stated outright), medium (a small inference such as a pronoun), low (suggested).
notesAt most 300 characters about the part, or an empty string.

The model can still misquote or misname. The page checks every evidence word for word against the text (folding case, quotes, dashes and spaces) and every end against the entity list or the text, and drops what it cannot find. Do the same check before you rely on a triple.