Driving City Guide from code
Base URL https://api.skillsafe.ai/v1/app-api. Every response is
{"ok": true, "data": {...}} or
{"ok": false, "error": {"code": "...", "message": "..."}} — check
ok before reading data.
The app's contract is unusual in one respect and it is the thing to understand before
you parse anything: every claim comes back with a stability label saying how
fast it goes out of date, and the model is instructed never to state a price, an opening
time, a timetable or the current operating status of a named business. If your caller
needs one of those, it is in check_before_you_go as a question and a named
authority, not as a value. That is deliberate, and treating a missing figure as a bug
will lead you somewhere unhelpful.
Errors
| HTTP | code | What it means |
|---|---|---|
| 400 | VALIDATION_ERROR | The body was not the shape the endpoint wanted. On /search this is also what an undeclared provider returns. |
| 401 | UNAUTHENTICATED | No token, or a token this app does not accept. Mint a guest token or sign in. |
| 402 | INSUFFICIENT_CREDITS | The balance is below min_credits. Call /estimate first and you will not hit this. |
| 403 | FORBIDDEN | A guest token tried to do something only a signed-in user can: /run, /run-stream or /search. Note that the auth gate runs before provider validation, so a guest gets this identical 403 whether the provider is declared, undeclared or nonsense. |
| 404 | NOT_FOUND | Wrong slug, or a job id that does not belong to this app. |
| 409 | CONFLICT | An idempotency key was reused with a different body. |
| 429 | RATE_LIMITED | Back off. /search is 30 requests a minute per IP; the data endpoints share 120. |
| 503 | UPSTREAM_UNAVAILABLE | The model or a dependency is briefly unavailable. Retry with the same idempotency key — it will not double-bill. |
1 Get a token
Anything below needs a bearer token. The easiest way to get one for this app is the token page, which reads the one your browser already holds and gives you a copy button. To mint a guest token from code, call /guest. A guest token is enough for /me and /estimate; /run, /run-stream and /search all need a signed-in personal token and return 403 otherwise.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"slug": "city-guide"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
URL = "https://api.skillsafe.ai/v1/app-api/guest"
payload = json.dumps({
"slug": "city-guide"
}).encode()
req = urllib.request.Request(URL, data=payload, method="POST", headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
})
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN";
const URL_ = "https://api.skillsafe.ai/v1/app-api/guest";
const res = await fetch(URL_, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"slug": "city-guide"
}),
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"slug": "city-guide"}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
public class CityGuide {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String payload = "{\"slug\": \"city-guide\"}";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> res =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = {
"slug" => "city-guide"
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$url = "https://api.skillsafe.ai/v1/app-api/guest";
$payload = json_encode([
"slug" => "city-guide"
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
"Content-Type: application/json",
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class CityGuide {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Token);
var payload = @"{""slug"": ""city-guide""}";
var content = new StringContent(payload, Encoding.UTF8,
"application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/guest", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
2 Check who the token is
/me returns exactly three fields — subject_type, subject_id and credits. There is no email, no name and no id beyond the subject. The signed-in test is subject_type === "user"; anything else is a guest.
curl -s "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer YOUR_TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN"
URL = "https://api.skillsafe.ai/v1/app-api/me"
req = urllib.request.Request(URL, headers={
"Authorization": "Bearer " + TOKEN,
})
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN";
const URL_ = "https://api.skillsafe.ai/v1/app-api/me";
const res = await fetch(URL_, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
public class CityGuide {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + TOKEN)
.GET().build();
HttpResponse<String> res =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$url = "https://api.skillsafe.ai/v1/app-api/me";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class CityGuide {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Token);
var res = await client.GetStringAsync("https://api.skillsafe.ai/v1/app-api/me");
Console.WriteLine(res);
}
}
3 Price the run before you make it
/estimate is free, makes no job and charges nothing. It returns model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled. Treat hold_credits as a reservation, not a price — the settled charge is usually much lower, because the hold prices the full output cap. Estimate each input shape you send, not just once: a bare city, a city with a question, and a follow-up are structurally different bodies.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"city": "La Paz", "country": "Bolivia", "question": "Why do the neighbourhoods feel so different from each other?", "angle": "both", "depth": "standard", "sources": [{"id": "SRC-1", "title": "La Paz", "abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city", "url": "https://en.wikipedia.org/wiki/La_Paz"}], "page_facts": {"records_retrieved": 1, "grounding_available": true, "volatile_ask": false, "ambiguous_sources": false}, "ask_flags": []}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
URL = "https://api.skillsafe.ai/v1/app-api/estimate"
payload = json.dumps({
"city": "La Paz",
"country": "Bolivia",
"question": "Why do the neighbourhoods feel so different from each other?",
"angle": "both",
"depth": "standard",
"sources": [
{
"id": "SRC-1",
"title": "La Paz",
"abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url": "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts": {
"records_retrieved": 1,
"grounding_available": true,
"volatile_ask": false,
"ambiguous_sources": false
},
"ask_flags": []
}).encode()
req = urllib.request.Request(URL, data=payload, method="POST", headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
})
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN";
const URL_ = "https://api.skillsafe.ai/v1/app-api/estimate";
const res = await fetch(URL_, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"city": "La Paz",
"country": "Bolivia",
"question": "Why do the neighbourhoods feel so different from each other?",
"angle": "both",
"depth": "standard",
"sources": [
{
"id": "SRC-1",
"title": "La Paz",
"abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url": "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts": {
"records_retrieved": 1,
"grounding_available": true,
"volatile_ask": false,
"ambiguous_sources": false
},
"ask_flags": []
}),
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"city": "La Paz", "country": "Bolivia", "question": "Why do the neighbourhoods feel so different from each other?", "angle": "both", "depth": "standard", "sources": [{"id": "SRC-1", "title": "La Paz", "abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city", "url": "https://en.wikipedia.org/wiki/La_Paz"}], "page_facts": {"records_retrieved": 1, "grounding_available": true, "volatile_ask": false, "ambiguous_sources": false}, "ask_flags": []}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
public class CityGuide {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String payload = "{\"city\": \"La Paz\", \"country\": \"Bolivia\", \"question\": \"Why do the neighbourhoods feel so different from each other?\", \"angle\": \"both\", \"depth\": \"standard\", \"sources\": [{\"id\": \"SRC-1\", \"title\": \"La Paz\", \"abstract\": \"La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city\", \"url\": \"https://en.wikipedia.org/wiki/La_Paz\"}], \"page_facts\": {\"records_retrieved\": 1, \"grounding_available\": true, \"volatile_ask\": false, \"ambiguous_sources\": false}, \"ask_flags\": []}";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> res =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = {
"city" => "La Paz",
"country" => "Bolivia",
"question" => "Why do the neighbourhoods feel so different from each other?",
"angle" => "both",
"depth" => "standard",
"sources" => [
{
"id" => "SRC-1",
"title" => "La Paz",
"abstract" => "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url" => "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts" => {
"records_retrieved" => 1,
"grounding_available" => true,
"volatile_ask" => false,
"ambiguous_sources" => false
},
"ask_flags" => []
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$url = "https://api.skillsafe.ai/v1/app-api/estimate";
$payload = json_encode([
"city" => "La Paz",
"country" => "Bolivia",
"question" => "Why do the neighbourhoods feel so different from each other?",
"angle" => "both",
"depth" => "standard",
"sources" => [
[
"id" => "SRC-1",
"title" => "La Paz",
"abstract" => "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url" => "https://en.wikipedia.org/wiki/La_Paz"
]
],
"page_facts" => [
"records_retrieved" => 1,
"grounding_available" => true,
"volatile_ask" => false,
"ambiguous_sources" => false
],
"ask_flags" => []
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
"Content-Type: application/json",
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class CityGuide {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Token);
var payload = @"{""city"": ""La Paz"", ""country"": ""Bolivia"", ""question"": ""Why do the neighbourhoods feel so different from each other?"", ""angle"": ""both"", ""depth"": ""standard"", ""sources"": [{""id"": ""SRC-1"", ""title"": ""La Paz"", ""abstract"": ""La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city"", ""url"": ""https://en.wikipedia.org/wiki/La_Paz""}], ""page_facts"": {""records_retrieved"": 1, ""grounding_available"": true, ""volatile_ask"": false, ""ambiguous_sources"": false}, ""ask_flags"": []}";
var content = new StringContent(payload, Encoding.UTF8,
"application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/estimate", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
4 Ground the city (optional, and worth it)
The city is named in your input, which is the whole reason a single retrieval is worth making here: one call, a near-unique query, and the abstract genuinely carries the drifting anchors. Pass the records through as sources with ids of the form SRC-1.
The snippet caveat matters more than the retrieval. This returns a MediaWiki snippet, not an infobox. A snippet about the right city very often does not mention the population, the founding year or the province — so a "cite only retrieved records" rule is satisfied by citing a real page for a fact the page never mentions. If you build on this, re-read the cited abstract and confirm it literally contains the value claimed, and treat a citation that fails that check as worse than no citation at all. Checking that a record_id was cited proves nothing.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/search" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider": "web.wikipedia", "query": "La Paz Bolivia", "limit": 5}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
URL = "https://api.skillsafe.ai/v1/app-api/search"
payload = json.dumps({
"provider": "web.wikipedia",
"query": "La Paz Bolivia",
"limit": 5
}).encode()
req = urllib.request.Request(URL, data=payload, method="POST", headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
})
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN";
const URL_ = "https://api.skillsafe.ai/v1/app-api/search";
const res = await fetch(URL_, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"provider": "web.wikipedia",
"query": "La Paz Bolivia",
"limit": 5
}),
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"provider": "web.wikipedia", "query": "La Paz Bolivia", "limit": 5}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/search", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
public class CityGuide {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String payload = "{\"provider\": \"web.wikipedia\", \"query\": \"La Paz Bolivia\", \"limit\": 5}";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/search"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> res =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/search")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = {
"provider" => "web.wikipedia",
"query" => "La Paz Bolivia",
"limit" => 5
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$url = "https://api.skillsafe.ai/v1/app-api/search";
$payload = json_encode([
"provider" => "web.wikipedia",
"query" => "La Paz Bolivia",
"limit" => 5
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
"Content-Type: application/json",
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class CityGuide {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Token);
var payload = @"{""provider"": ""web.wikipedia"", ""query"": ""La Paz Bolivia"", ""limit"": 5}";
var content = new StringContent(payload, Encoding.UTF8,
"application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/search", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
5 Run it
Metered. Returns {"job_id": "..."}; poll /jobs/{id} until it reaches a terminal state. Always send an idempotency key — a content hash of the input plus an attempt counter. A network blip must never double-bill, and a retry with the same key is free.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"city": "La Paz", "country": "Bolivia", "question": "Why do the neighbourhoods feel so different from each other?", "angle": "both", "depth": "standard", "sources": [{"id": "SRC-1", "title": "La Paz", "abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city", "url": "https://en.wikipedia.org/wiki/La_Paz"}], "page_facts": {"records_retrieved": 1, "grounding_available": true, "volatile_ask": false, "ambiguous_sources": false}, "ask_flags": []}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
URL = "https://api.skillsafe.ai/v1/app-api/run"
payload = json.dumps({
"city": "La Paz",
"country": "Bolivia",
"question": "Why do the neighbourhoods feel so different from each other?",
"angle": "both",
"depth": "standard",
"sources": [
{
"id": "SRC-1",
"title": "La Paz",
"abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url": "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts": {
"records_retrieved": 1,
"grounding_available": true,
"volatile_ask": false,
"ambiguous_sources": false
},
"ask_flags": []
}).encode()
req = urllib.request.Request(URL, data=payload, method="POST", headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
})
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN";
const URL_ = "https://api.skillsafe.ai/v1/app-api/run";
const res = await fetch(URL_, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"city": "La Paz",
"country": "Bolivia",
"question": "Why do the neighbourhoods feel so different from each other?",
"angle": "both",
"depth": "standard",
"sources": [
{
"id": "SRC-1",
"title": "La Paz",
"abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url": "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts": {
"records_retrieved": 1,
"grounding_available": true,
"volatile_ask": false,
"ambiguous_sources": false
},
"ask_flags": []
}),
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"city": "La Paz", "country": "Bolivia", "question": "Why do the neighbourhoods feel so different from each other?", "angle": "both", "depth": "standard", "sources": [{"id": "SRC-1", "title": "La Paz", "abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city", "url": "https://en.wikipedia.org/wiki/La_Paz"}], "page_facts": {"records_retrieved": 1, "grounding_available": true, "volatile_ask": false, "ambiguous_sources": false}, "ask_flags": []}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
public class CityGuide {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String payload = "{\"city\": \"La Paz\", \"country\": \"Bolivia\", \"question\": \"Why do the neighbourhoods feel so different from each other?\", \"angle\": \"both\", \"depth\": \"standard\", \"sources\": [{\"id\": \"SRC-1\", \"title\": \"La Paz\", \"abstract\": \"La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city\", \"url\": \"https://en.wikipedia.org/wiki/La_Paz\"}], \"page_facts\": {\"records_retrieved\": 1, \"grounding_available\": true, \"volatile_ask\": false, \"ambiguous_sources\": false}, \"ask_flags\": []}";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> res =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = {
"city" => "La Paz",
"country" => "Bolivia",
"question" => "Why do the neighbourhoods feel so different from each other?",
"angle" => "both",
"depth" => "standard",
"sources" => [
{
"id" => "SRC-1",
"title" => "La Paz",
"abstract" => "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url" => "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts" => {
"records_retrieved" => 1,
"grounding_available" => true,
"volatile_ask" => false,
"ambiguous_sources" => false
},
"ask_flags" => []
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$url = "https://api.skillsafe.ai/v1/app-api/run";
$payload = json_encode([
"city" => "La Paz",
"country" => "Bolivia",
"question" => "Why do the neighbourhoods feel so different from each other?",
"angle" => "both",
"depth" => "standard",
"sources" => [
[
"id" => "SRC-1",
"title" => "La Paz",
"abstract" => "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url" => "https://en.wikipedia.org/wiki/La_Paz"
]
],
"page_facts" => [
"records_retrieved" => 1,
"grounding_available" => true,
"volatile_ask" => false,
"ambiguous_sources" => false
],
"ask_flags" => []
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
"Content-Type: application/json",
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class CityGuide {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Token);
var payload = @"{""city"": ""La Paz"", ""country"": ""Bolivia"", ""question"": ""Why do the neighbourhoods feel so different from each other?"", ""angle"": ""both"", ""depth"": ""standard"", ""sources"": [{""id"": ""SRC-1"", ""title"": ""La Paz"", ""abstract"": ""La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city"", ""url"": ""https://en.wikipedia.org/wiki/La_Paz""}], ""page_facts"": {""records_retrieved"": 1, ""grounding_available"": true, ""volatile_ask"": false, ""ambiguous_sources"": false}, ""ask_flags"": []}";
var content = new StringContent(payload, Encoding.UTF8,
"application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/run", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
Poll the job
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID" \
-H "Authorization: Bearer YOUR_TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN"
URL = "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID"
req = urllib.request.Request(URL, headers={
"Authorization": "Bearer " + TOKEN,
})
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN";
const URL_ = "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID";
const res = await fetch(URL_, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
public class CityGuide {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID"))
.header("Authorization", "Bearer " + TOKEN)
.GET().build();
HttpResponse<String> res =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$url = "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class CityGuide {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Token);
var res = await client.GetStringAsync("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID");
Console.WriteLine(res);
}
}
6 Or stream it
Same body, same idempotency key, an SSE response instead of a job id. The request is identical in every language; what differs is that you read the response as a stream of data: lines rather than parsing one JSON body. The answer is a single JSON object, so accumulate the deltas and parse at the end — or, if you want to survive a cut stream, walk the accumulated text's string state and bracket depth, cut back to the last point where a value completed, and close what is open. A fixed closing suffix recovers almost nothing; the generic walker recovers almost everything.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"city": "La Paz", "country": "Bolivia", "question": "Why do the neighbourhoods feel so different from each other?", "angle": "both", "depth": "standard", "sources": [{"id": "SRC-1", "title": "La Paz", "abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city", "url": "https://en.wikipedia.org/wiki/La_Paz"}], "page_facts": {"records_retrieved": 1, "grounding_available": true, "volatile_ask": false, "ambiguous_sources": false}, "ask_flags": []}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
URL = "https://api.skillsafe.ai/v1/app-api/run-stream"
payload = json.dumps({
"city": "La Paz",
"country": "Bolivia",
"question": "Why do the neighbourhoods feel so different from each other?",
"angle": "both",
"depth": "standard",
"sources": [
{
"id": "SRC-1",
"title": "La Paz",
"abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url": "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts": {
"records_retrieved": 1,
"grounding_available": true,
"volatile_ask": false,
"ambiguous_sources": false
},
"ask_flags": []
}).encode()
req = urllib.request.Request(URL, data=payload, method="POST", headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
})
with urllib.request.urlopen(req) as r:
print(json.load(r)["data"])
const TOKEN = "YOUR_TOKEN";
const URL_ = "https://api.skillsafe.ai/v1/app-api/run-stream";
const res = await fetch(URL_, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"city": "La Paz",
"country": "Bolivia",
"question": "Why do the neighbourhoods feel so different from each other?",
"angle": "both",
"depth": "standard",
"sources": [
{
"id": "SRC-1",
"title": "La Paz",
"abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url": "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts": {
"records_retrieved": 1,
"grounding_available": true,
"volatile_ask": false,
"ambiguous_sources": false
},
"ask_flags": []
}),
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"city": "La Paz", "country": "Bolivia", "question": "Why do the neighbourhoods feel so different from each other?", "angle": "both", "depth": "standard", "sources": [{"id": "SRC-1", "title": "La Paz", "abstract": "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city", "url": "https://en.wikipedia.org/wiki/La_Paz"}], "page_facts": {"records_retrieved": 1, "grounding_available": true, "volatile_ask": false, "ambiguous_sources": false}, "ask_flags": []}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run-stream", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.*;
public class CityGuide {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String payload = "{\"city\": \"La Paz\", \"country\": \"Bolivia\", \"question\": \"Why do the neighbourhoods feel so different from each other?\", \"angle\": \"both\", \"depth\": \"standard\", \"sources\": [{\"id\": \"SRC-1\", \"title\": \"La Paz\", \"abstract\": \"La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city\", \"url\": \"https://en.wikipedia.org/wiki/La_Paz\"}], \"page_facts\": {\"records_retrieved\": 1, \"grounding_available\": true, \"volatile_ask\": false, \"ambiguous_sources\": false}, \"ask_flags\": []}";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> res =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = {
"city" => "La Paz",
"country" => "Bolivia",
"question" => "Why do the neighbourhoods feel so different from each other?",
"angle" => "both",
"depth" => "standard",
"sources" => [
{
"id" => "SRC-1",
"title" => "La Paz",
"abstract" => "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url" => "https://en.wikipedia.org/wiki/La_Paz"
}
],
"page_facts" => {
"records_retrieved" => 1,
"grounding_available" => true,
"volatile_ask" => false,
"ambiguous_sources" => false
},
"ask_flags" => []
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$url = "https://api.skillsafe.ai/v1/app-api/run-stream";
$payload = json_encode([
"city" => "La Paz",
"country" => "Bolivia",
"question" => "Why do the neighbourhoods feel so different from each other?",
"angle" => "both",
"depth" => "standard",
"sources" => [
[
"id" => "SRC-1",
"title" => "La Paz",
"abstract" => "La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city",
"url" => "https://en.wikipedia.org/wiki/La_Paz"
]
],
"page_facts" => [
"records_retrieved" => 1,
"grounding_available" => true,
"volatile_ask" => false,
"ambiguous_sources" => false
],
"ask_flags" => []
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
"Content-Type: application/json",
]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class CityGuide {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Token);
var payload = @"{""city"": ""La Paz"", ""country"": ""Bolivia"", ""question"": ""Why do the neighbourhoods feel so different from each other?"", ""angle"": ""both"", ""depth"": ""standard"", ""sources"": [{""id"": ""SRC-1"", ""title"": ""La Paz"", ""abstract"": ""La Paz, officially Nuestra Senora de La Paz, is the seat of government of Bolivia. With 755,732 residents as of 2024, it is the third-most populous city"", ""url"": ""https://en.wikipedia.org/wiki/La_Paz""}], ""page_facts"": {""records_retrieved"": 1, ""grounding_available"": true, ""volatile_ask"": false, ""ambiguous_sources"": false}, ""ask_flags"": []}";
var content = new StringContent(payload, Encoding.UTF8,
"application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/run-stream", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
7 What comes back
One JSON object, no code fence, no prose around it. Every key is present; unused sections are "" or [], never missing and never null.
Three things are worth knowing before you write a parser. One: stability is one of durable, slow-drift or current-state, and an empty stability on a row that carries a claim is a defect in the answer, not a default — do not normalise it to durable. Two: every slow-drift row should carry an as_of year, and one that does not is worth surfacing. Three: each anchor fact in identity is paired with a _source that is either a SRC-n id or the literal string model-knowledge; verify the first kind against the abstract rather than trusting it.
{
"found": true,
"confidence": "grounded",
"not_found": {
"reason": "",
"did_you_mean": [],
"what_would_help": ""
},
"identity": {
"name": "",
"name_source": "",
"country": "",
"country_source": "",
"region": "",
"region_source": "",
"population": "",
"population_source": "",
"population_as_of": "",
"setting": "",
"setting_source": "",
"founded": "",
"founded_source": "",
"note": ""
},
"answer": {
"headline": "",
"body": "",
"stability": "durable",
"as_of": ""
},
"shape": "",
"sorting_axis": "",
"districts": [
{
"name": "",
"character": "",
"who_lives_there": "",
"why_it_feels_that_way": "",
"caveat": "",
"stability": "durable",
"as_of": ""
}
],
"getting_around": [
{
"claim": "",
"stability": "slow-drift",
"as_of": "",
"check": ""
}
],
"worth_it": [
{
"thing": "",
"verdict": "",
"why": "",
"local_view": "",
"stability": "durable"
}
],
"overrated": [
{
"thing": "",
"what_people_expect": "",
"what_it_is": "",
"instead": ""
}
],
"good_at": "",
"tiresome": "",
"misreadings": [
{
"assumption": "",
"reality": "",
"cost": ""
}
],
"changing": [
{
"what": "",
"direction": "",
"as_of": "",
"stability": "slow-drift"
}
],
"check_before_you_go": [
{
"question": "",
"why_volatile": "",
"where_to_check": ""
}
],
"declined": [
{
"asked": "",
"why": "",
"instead": ""
}
],
"limits": ""
}
8 Storing answers
The app declares one collection, answers, with acl_read: "owner" and acl_write: "user". Two behaviours will cost you an afternoon if you do not know them. query() resolves to {records, next_cursor} but similar() resolves to the records array itself, so destructuring it as {records} yields undefined and a semantic search silently returns nothing. And a declared timestamp field rejects epoch milliseconds — only ISO-8601 with a Z suffix is accepted. Records nest under doc; read rec.doc.city, never rec.city.