Ask Your Docs API
The app does retrieval in the browser and sends the model only a question and the passages it picked. The API takes the same input, so you bring your own retrieval (any search, any vector store) and get back an answer whose every sentence cites one of your passages, with a verbatim quote per citation you can check.
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": ...}.
Input
| Field | Type | Notes |
|---|---|---|
question | string, required | The question. The answer comes back in its language. |
passages | string, required | Numbered passages, each a header line [S1] file · p. 4 · heading followed by its text, separated by a blank line. Keep it under about 10,000 characters. |
answer_length | string | short (2-5 sentences, default) or detailed. |
conversation | string | Optional earlier turns as User: / Assistant: lines, used only to resolve "it" or "that". Not evidence. |
retry_note | string | Optional. The app sets it only on its one automatic reformat retry. |
Output
The finished job's output.output is plain text with four labelled sections:
STATUS: answered | partial | not_found
ANSWER:
Items can be returned within 30 days of delivery for a full refund [S1]. Opened software cannot be returned [S2].
QUOTES:
S1: "Items may be returned within 30 days of delivery for a full refund"
S2: "Opened software and gift cards cannot be returned"
FOLLOW_UPS:
- How is the refund paid?
Check each quote against its passage before you trust it; the app does exactly that and marks each one verified, close or not found. A sentence with no tag is uncited.
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://ask-your-docs.skillsafe.ai/tokens.html and copy it from there.
curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"ask-your-docs"}'
guest = json.load(urllib.request.urlopen(urllib.request.Request(
BASE + "/guest", data=json.dumps({"slug": "ask-your-docs"}).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: "ask-your-docs" }),
}).then((r) => r.json());
console.log(g.data.token); // personal token for runs: tokens.html
// POST {"slug":"ask-your-docs"} to base+"/guest" without a token; read data.token.
// For runs, copy a personal token from https://ask-your-docs.skillsafe.ai/tokens.html
// POST {"slug":"ask-your-docs"} to BASE + "/guest" (no Authorization header) and read data.token.
// Runs need a personal token from https://ask-your-docs.skillsafe.ai/tokens.html
uri = URI(BASE + "/guest")
res = Net::HTTP.post(uri, { slug: "ask-your-docs" }.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" => "ask-your-docs"])]);
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\":\"ask-your-docs\"}", 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 question (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 '{"question": "How long do I have to return an item?", "passages": "[S1] returns.pdf · p. 2 · Returns\nItems may be returned within 30 days of delivery for a full refund.\n\n[S2] returns.pdf · p. 3 · Exceptions\nOpened software and gift cards cannot be returned.", "answer_length": "short"}'
body = {
"question": "How long do I have to return an item?",
"passages": "[S1] returns.pdf · p. 2 · Returns\nItems may be returned within 30 days of delivery for a full refund.\n\n[S2] returns.pdf · p. 3 · Exceptions\nOpened software and gift cards cannot be returned.",
"answer_length": "short"
}
est = call("POST", "/estimate", body)
print(est["hold_credits"], est["model_alias"], est.get("warnings"))
const body = {
"question": "How long do I have to return an item?",
"passages": "[S1] returns.pdf · p. 2 · Returns\nItems may be returned within 30 days of delivery for a full refund.\n\n[S2] returns.pdf · p. 3 · Exceptions\nOpened software and gift cards cannot be returned.",
"answer_length": "short"
};
const est = await call("POST", "/estimate", body);
console.log(est.hold_credits, est.model_alias, est.warnings);
body := map[string]string{
"question": "How long do I have to return an item?",
"passages": "[S1] returns.pdf · p. 2 · Returns\nItems may be returned within 30 days of delivery for a full refund.\n\n[S2] returns.pdf · p. 3 · Exceptions\nOpened software and gift cards cannot be returned.",
"answer_length": "short",
}
var est map[string]any
call("POST", "/estimate", body, &est)
String body = "{\"question\": \"How long do I have to return an item?\", \"passages\": \"[S1] returns.pdf · p. 2 · Returns\\nItems may be returned within 30 days of delivery for a full refund.\\n\\n[S2] returns.pdf · p. 3 · Exceptions\\nOpened software and gift cards cannot be returned.\", \"answer_length\": \"short\"}";
System.out.println(Api.call("POST", "/estimate", body));
body = JSON.parse("{\"question\": \"How long do I have to return an item?\", \"passages\": \"[S1] returns.pdf · p. 2 · Returns\\nItems may be returned within 30 days of delivery for a full refund.\\n\\n[S2] returns.pdf · p. 3 · Exceptions\\nOpened software and gift cards cannot be returned.\", \"answer_length\": \"short\"}")
est = call("POST", "/estimate", body)
puts est["hold_credits"]
$body = json_decode("{\"question\": \"How long do I have to return an item?\", \"passages\": \"[S1] returns.pdf · p. 2 · Returns\\nItems may be returned within 30 days of delivery for a full refund.\\n\\n[S2] returns.pdf · p. 3 · Exceptions\\nOpened software and gift cards cannot be returned.\", \"answer_length\": \"short\"}", true);
$est = call("POST", "/estimate", $body);
echo $est["hold_credits"];
var body = JsonSerializer.Deserialize<JsonElement>("{\"question\": \"How long do I have to return an item?\", \"passages\": \"[S1] returns.pdf · p. 2 · Returns\\nItems may be returned within 30 days of delivery for a full refund.\\n\\n[S2] returns.pdf · p. 3 · Exceptions\\nOpened software and gift cards cannot be returned.\", \"answer_length\": \"short\"}");
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-q-001" \
-H "Content-Type: application/json" -d '{"question": "How long do I have to return an item?", "passages": "[S1] returns.pdf · p. 2 · Returns\nItems may be returned within 30 days of delivery for a full refund.\n\n[S2] returns.pdf · p. 3 · Exceptions\nOpened software and gift cards cannot be returned.", "answer_length": "short"}'
# -> 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-q-001"})
while job.get("status") not in ("succeeded", "failed"):
time.sleep(1)
job = call("GET", "/jobs/" + job["job_id"])
print(job["output"]["output"]) # STATUS / ANSWER / QUOTES / FOLLOW_UPS
let job = await call("POST", "/run", body, { "Idempotency-Key": "my-q-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-q-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-q-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-q-002" \
-H "Content-Type: application/json" -d '{"question": "How long do I have to return an item?", "passages": "[S1] returns.pdf · p. 2 · Returns\nItems may be returned within 30 days of delivery for a full refund.\n\n[S2] returns.pdf · p. 3 · Exceptions\nOpened software and gift cards cannot be returned.", "answer_length": "short"}'
# 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-q-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-q-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-q-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-q-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);