City Guide

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

HTTPcodeWhat it means
400VALIDATION_ERRORThe body was not the shape the endpoint wanted. On /search this is also what an undeclared provider returns.
401UNAUTHENTICATEDNo token, or a token this app does not accept. Mint a guest token or sign in.
402INSUFFICIENT_CREDITSThe balance is below min_credits. Call /estimate first and you will not hit this.
403FORBIDDENA 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.
404NOT_FOUNDWrong slug, or a job id that does not belong to this app.
409CONFLICTAn idempotency key was reused with a different body.
429RATE_LIMITEDBack off. /search is 30 requests a minute per IP; the data endpoints share 120.
503UPSTREAM_UNAVAILABLEThe 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.