Ask the Web

Ask the Web API

The app gathers sources with its own search step, ranks paragraphs in the browser and sends the model only the question and the passages it picked. The API takes the same input, so you can use the app's source search or bring your own retrieval (any search engine, 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

FieldTypeNotes
questionstring, requiredThe question. The answer comes back in its language.
passagesstring, requiredNumbered passages, each a header line [S1] title · section (where it came from) followed by its text, separated by a blank line. Keep it under about 12,000 characters.
answer_lengthstringshort (2-5 sentences, default) or detailed.
todaystringOptional date, YYYY-MM-DD. Lets the answer say "as of" for facts that change.
conversationstringOptional earlier turns as User: / Assistant: lines, used only to resolve "it" or "that". Not evidence.
retry_notestringOptional. 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:
The tower was designed and built by Gustave Eiffel's company between 1887 and 1889 [S1]. It was built as the centrepiece of the 1889 World's Fair [S2].
QUOTES:
S1: "whose company designed and built the tower from 1887 to 1889"
S2: "It was constructed as the centrepiece of the 1889 World's Fair"
FOLLOW_UPS:
- How tall is the Eiffel Tower?

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

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://ask-the-web.skillsafe.ai/tokens.html and copy it from there.
curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"ask-the-web"}'
guest = json.load(urllib.request.urlopen(urllib.request.Request(
    BASE + "/guest", data=json.dumps({"slug": "ask-the-web"}).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-the-web" }),
}).then((r) => r.json());
console.log(g.data.token); // personal token for runs: tokens.html
// POST {"slug":"ask-the-web"} to base+"/guest" without a token; read data.token.
// For runs, copy a personal token from https://ask-the-web.skillsafe.ai/tokens.html
// POST {"slug":"ask-the-web"} to BASE + "/guest" (no Authorization header) and read data.token.
// Runs need a personal token from https://ask-the-web.skillsafe.ai/tokens.html
uri = URI(BASE + "/guest")
res = Net::HTTP.post(uri, { slug: "ask-the-web" }.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-the-web"])]);
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-the-web\"}", 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. Gather sources (free, optional)

The app's own source search is a function on the app's origin, not on the API host. It needs a personal (signed-in) token and returns the top Wikipedia articles whole, as plain text with == Section == lines. Rank and number the passages yourself, as the page does, before step 5. Languages: en, de, fr, es, it, pt, nl, pl, ru, ja, zh.

curl -s -X POST "https://ask-the-web.skillsafe.ai/api/sources" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"q": "Who designed the Eiffel Tower?", "lang": "en", "pages": 3}'
# -> {"ok": true, "sources": [{"title", "url", "text", "snippet", "chars", "clipped"}], "notes": []}

5. 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": "Who designed the Eiffel Tower, and why was it built?", "passages": "[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\n\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\nIt was constructed as the centrepiece of the 1889 World's Fair.", "answer_length": "short", "today": "2026-10-31"}'
body = {
    "question": "Who designed the Eiffel Tower, and why was it built?",
    "passages": "[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\n\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\nIt was constructed as the centrepiece of the 1889 World's Fair.",
    "answer_length": "short",
    "today": "2026-10-31"
}
est = call("POST", "/estimate", body)
print(est["hold_credits"], est["model_alias"], est.get("warnings"))
const body = {
  "question": "Who designed the Eiffel Tower, and why was it built?",
  "passages": "[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\n\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\nIt was constructed as the centrepiece of the 1889 World's Fair.",
  "answer_length": "short",
  "today": "2026-10-31"
};
const est = await call("POST", "/estimate", body);
console.log(est.hold_credits, est.model_alias, est.warnings);
body := map[string]string{
	"question": "Who designed the Eiffel Tower, and why was it built?",
	"passages": "[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\n\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\nIt was constructed as the centrepiece of the 1889 World's Fair.",
	"answer_length": "short",
}
var est map[string]any
call("POST", "/estimate", body, &est)
String body = "{\"question\": \"Who designed the Eiffel Tower, and why was it built?\", \"passages\": \"[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\\n\\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\\nIt was constructed as the centrepiece of the 1889 World's Fair.\", \"answer_length\": \"short\", \"today\": \"2026-10-31\"}";
System.out.println(Api.call("POST", "/estimate", body));
body = JSON.parse("{\"question\": \"Who designed the Eiffel Tower, and why was it built?\", \"passages\": \"[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\\n\\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\\nIt was constructed as the centrepiece of the 1889 World's Fair.\", \"answer_length\": \"short\", \"today\": \"2026-10-31\"}")
est = call("POST", "/estimate", body)
puts est["hold_credits"]
$body = json_decode("{\"question\": \"Who designed the Eiffel Tower, and why was it built?\", \"passages\": \"[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\\n\\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\\nIt was constructed as the centrepiece of the 1889 World's Fair.\", \"answer_length\": \"short\", \"today\": \"2026-10-31\"}", true);
$est = call("POST", "/estimate", $body);
echo $est["hold_credits"];
var body = JsonSerializer.Deserialize<JsonElement>("{\"question\": \"Who designed the Eiffel Tower, and why was it built?\", \"passages\": \"[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\\n\\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\\nIt was constructed as the centrepiece of the 1889 World's Fair.\", \"answer_length\": \"short\", \"today\": \"2026-10-31\"}");
var est = await Call(HttpMethod.Post, "/estimate", body);

6. 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": "Who designed the Eiffel Tower, and why was it built?", "passages": "[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\n\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\nIt was constructed as the centrepiece of the 1889 World's Fair.", "answer_length": "short", "today": "2026-10-31"}'
# -> 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());
}

7. 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": "Who designed the Eiffel Tower, and why was it built?", "passages": "[S1] Eiffel Tower (Wikipedia, retrieved 2026-10-31)\nThe Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower from 1887 to 1889.\n\n[S2] Eiffel Tower · History (Wikipedia, retrieved 2026-10-31)\nIt was constructed as the centrepiece of the 1889 World's Fair.", "answer_length": "short", "today": "2026-10-31"}'
# 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);