Driving Travel Guide from code
Base URL https://api.skillsafe.ai/v1/app-api. Every response is an envelope:
{"data": {...}} on success, {"error": {"code": "...", "message": "..."}}
on failure. Every call carries Authorization: Bearer <token>.
The contract is the same one the page uses, so anything the browser can do you can do. What
the app will not do for you either: it does not state prices, opening hours, closing days,
booking windows, timetables, or safety, entry and health requirements, and it does not name
individual places to eat. Those come back in
check_before_you_go and declined with an authority attached, and
that is deliberate rather than a gap to work around.
Errors
| HTTP | code | What it means |
|---|---|---|
401 | UNAUTHENTICATED | No token, or a token that has expired. Mint a new guest token, or sign in. |
402 | INSUFFICIENT_CREDITS | The balance is below min_credits. Top up, or shorten the trip. |
403 | FORBIDDEN | A guest token tried to run or to search. Both need a signed-in user. |
404 | NOT_FOUND | Wrong slug, or a job id that never existed. |
409 | IDEMPOTENCY_CONFLICT | The same Idempotency-Key was reused with a different body. |
422 | INVALID_INPUT | The input object did not match the app's contract. Check field names. |
429 | RATE_LIMITED | Too many calls. Back off; do not retry in a tight loop. |
503 | UPSTREAM_UNAVAILABLE | The model tier is briefly unavailable. Retry with the same idempotency key. |
1 Get a token
Every call needs a bearer token. A guest token is free and mints in one call — it can read and price, but it cannot run and it cannot search, both of which need a signed-in user. The token page will hand you a personal one without touching developer tools.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"travel-guide"}'
import requests
TOKEN = "YOUR_TOKEN"
r = requests.post(
"https://api.skillsafe.ai/v1/app-api/guest",
headers={"Authorization": f"Bearer {TOKEN}"}, json={"slug":"travel-guide"},
timeout=60,
)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({"slug":"travel-guide"})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("POST",
"https://api.skillsafe.ai/v1/app-api/guest",
strings.NewReader(`{"slug":"travel-guide"}`))
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 Main {
static final String TOKEN = "YOUR_TOKEN";
static final String BODY = """
{"slug":"travel-guide"}
""";
public static void main(String[] args) throws Exception {
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(BODY))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
TOKEN = "YOUR_TOKEN"
BODY = <<~JSON
{"slug":"travel-guide"}
JSON
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 = BODY
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = 'YOUR_TOKEN';
$body = <<<'JSON'
{"slug":"travel-guide"}
JSON;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.skillsafe.ai/v1/app-api/guest',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$out = curl_exec($ch);
curl_close($ch);
print_r(json_decode($out, true)['data']);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program {
const string Token = "YOUR_TOKEN";
const string BODY = @"{""slug"":""travel-guide""}";
static async Task Main() {
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
var body = new StringContent(BODY, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/guest", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
2 Check what the token is
Returns exactly three fields: subject_type, subject_id and credits. There is no name, email or id — the signed-in test is subject_type == "user" and nothing else. A 401 here on a cold start is the correct answer for a visitor who has never signed in.
curl -s -X GET "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer YOUR_TOKEN"
import requests
TOKEN = "YOUR_TOKEN"
r = requests.get(
"https://api.skillsafe.ai/v1/app-api/me",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=60,
)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
method: "GET",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
}
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
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)
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 Main {
static final String TOKEN = "YOUR_TOKEN";
public static void main(String[] args) throws Exception {
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
TOKEN = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = 'YOUR_TOKEN';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.skillsafe.ai/v1/app-api/me',
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
]);
$out = curl_exec($ch);
curl_close($ch);
print_r(json_decode($out, true)['data']);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program {
const string Token = "YOUR_TOKEN";
static async Task Main() {
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
var res = await http.GetAsync("https://api.skillsafe.ai/v1/app-api/me");
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
3 Price the run before making it
Free, and it creates no job. It returns hold_credits (what gets reserved, priced against the full output cap), min_credits (the floor below which the run will not start), model and model_alias. Note that the request body is the input object — there is no input wrapper and no X-App-Slug header. Wrapping it returns 200 and quietly hides every field from the model.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}'
import requests
TOKEN = "YOUR_TOKEN"
r = requests.post(
"https://api.skillsafe.ai/v1/app-api/estimate",
headers={"Authorization": f"Bearer {TOKEN}"}, json={"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":False,"ambiguous_sources":False,"volatile_ask":False,"hard_flags":0,"notes_clipped":False},"ask_flags":[]},
timeout=60,
)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("POST",
"https://api.skillsafe.ai/v1/app-api/estimate",
strings.NewReader(`{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}`))
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 Main {
static final String TOKEN = "YOUR_TOKEN";
static final String BODY = """
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
""";
public static void main(String[] args) throws Exception {
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(BODY))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
TOKEN = "YOUR_TOKEN"
BODY = <<~JSON
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
JSON
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 = BODY
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = 'YOUR_TOKEN';
$body = <<<'JSON'
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
JSON;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.skillsafe.ai/v1/app-api/estimate',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$out = curl_exec($ch);
curl_close($ch);
print_r(json_decode($out, true)['data']);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program {
const string Token = "YOUR_TOKEN";
const string BODY = @"{""destination"":""Trieste"",""country"":""Italy"",""days"":4,""month"":""late October"",""party"":""two adults and my mother, who is 78"",""interests"":[""food"",""history"",""walking""],""pace"":""gentle"",""budget"":""moderate"",""notes"":""We land mid-afternoon on the first day."",""sources"":[],""page_facts"":{""records_retrieved"":0,""grounding_available"":false,""ambiguous_sources"":false,""volatile_ask"":false,""hard_flags"":0,""notes_clipped"":false},""ask_flags"":[]}";
static async Task Main() {
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
var body = new StringContent(BODY, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
4 Ground the destination
Optional and worth doing. One call, against the destination that is already in your input — which is the whole case for it. Records come back as {record_id, title, abstract, url}; pass them through as the sources array with ids S1, S2 and so on, and the app will cite them. Do not search for restaurants or opening hours: a snippet cannot support one, and citing it anyway launders a guess behind a real URL.
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":"Trieste Italy","limit":5}'
import requests
TOKEN = "YOUR_TOKEN"
r = requests.post(
"https://api.skillsafe.ai/v1/app-api/search",
headers={"Authorization": f"Bearer {TOKEN}"}, json={"provider":"web.wikipedia","query":"Trieste Italy","limit":5},
timeout=60,
)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/search", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({"provider":"web.wikipedia","query":"Trieste Italy","limit":5})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("POST",
"https://api.skillsafe.ai/v1/app-api/search",
strings.NewReader(`{"provider":"web.wikipedia","query":"Trieste Italy","limit":5}`))
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 Main {
static final String TOKEN = "YOUR_TOKEN";
static final String BODY = """
{"provider":"web.wikipedia","query":"Trieste Italy","limit":5}
""";
public static void main(String[] args) throws Exception {
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/search"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(BODY))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
TOKEN = "YOUR_TOKEN"
BODY = <<~JSON
{"provider":"web.wikipedia","query":"Trieste Italy","limit":5}
JSON
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 = BODY
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = 'YOUR_TOKEN';
$body = <<<'JSON'
{"provider":"web.wikipedia","query":"Trieste Italy","limit":5}
JSON;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.skillsafe.ai/v1/app-api/search',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$out = curl_exec($ch);
curl_close($ch);
print_r(json_decode($out, true)['data']);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program {
const string Token = "YOUR_TOKEN";
const string BODY = @"{""provider"":""web.wikipedia"",""query"":""Trieste Italy"",""limit"":5}";
static async Task Main() {
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
var body = new StringContent(BODY, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/search", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
5 Run it
Two calls: submit, then poll. Always send an Idempotency-Key — a network blip on a paid run must never bill twice. Hash the input plus an attempt counter. The job returns status, then output (a JSON string), charged_credits and truncated.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}'
# then poll until status is "succeeded" or "failed"
curl -s "https://api.skillsafe.ai/v1/app-api/run/JOB_ID" \
-H "Authorization: Bearer YOUR_TOKEN"
import requests
TOKEN = "YOUR_TOKEN"
r = requests.post(
"https://api.skillsafe.ai/v1/app-api/run",
headers={"Authorization": f"Bearer {TOKEN}"}, json={"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":False,"ambiguous_sources":False,"volatile_ask":False,"hard_flags":0,"notes_clipped":False},"ask_flags":[]},
timeout=60,
)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("POST",
"https://api.skillsafe.ai/v1/app-api/run",
strings.NewReader(`{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}`))
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 Main {
static final String TOKEN = "YOUR_TOKEN";
static final String BODY = """
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
""";
public static void main(String[] args) throws Exception {
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(BODY))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
TOKEN = "YOUR_TOKEN"
BODY = <<~JSON
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
JSON
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 = BODY
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = 'YOUR_TOKEN';
$body = <<<'JSON'
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
JSON;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.skillsafe.ai/v1/app-api/run',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$out = curl_exec($ch);
curl_close($ch);
print_r(json_decode($out, true)['data']);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program {
const string Token = "YOUR_TOKEN";
const string BODY = @"{""destination"":""Trieste"",""country"":""Italy"",""days"":4,""month"":""late October"",""party"":""two adults and my mother, who is 78"",""interests"":[""food"",""history"",""walking""],""pace"":""gentle"",""budget"":""moderate"",""notes"":""We land mid-afternoon on the first day."",""sources"":[],""page_facts"":{""records_retrieved"":0,""grounding_available"":false,""ambiguous_sources"":false,""volatile_ask"":false,""hard_flags"":0,""notes_clipped"":false},""ask_flags"":[]}";
static async Task Main() {
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
var body = new StringContent(BODY, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
6 Or stream it
Server-sent events over the same body. Deltas arrive as data: lines; accumulate them and parse once the stream ends. If it ends early, walk the accumulated string back to the last completed value and close the open brackets — a plan cut off after day four is four usable days, and discarding it throws away a run the user paid for.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}'
import requests
TOKEN = "YOUR_TOKEN"
r = requests.post(
"https://api.skillsafe.ai/v1/app-api/run-stream",
headers={"Authorization": f"Bearer {TOKEN}"}, json={"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":False,"ambiguous_sources":False,"volatile_ask":False,"hard_flags":0,"notes_clipped":False},"ask_flags":[]},
timeout=60,
)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("POST",
"https://api.skillsafe.ai/v1/app-api/run-stream",
strings.NewReader(`{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}`))
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 Main {
static final String TOKEN = "YOUR_TOKEN";
static final String BODY = """
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
""";
public static void main(String[] args) throws Exception {
var req = HttpRequest.newBuilder(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(BODY))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
TOKEN = "YOUR_TOKEN"
BODY = <<~JSON
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
JSON
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 = BODY
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = 'YOUR_TOKEN';
$body = <<<'JSON'
{"destination":"Trieste","country":"Italy","days":4,"month":"late October","party":"two adults and my mother, who is 78","interests":["food","history","walking"],"pace":"gentle","budget":"moderate","notes":"We land mid-afternoon on the first day.","sources":[],"page_facts":{"records_retrieved":0,"grounding_available":false,"ambiguous_sources":false,"volatile_ask":false,"hard_flags":0,"notes_clipped":false},"ask_flags":[]}
JSON;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.skillsafe.ai/v1/app-api/run-stream',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$out = curl_exec($ch);
curl_close($ch);
print_r(json_decode($out, true)['data']);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program {
const string Token = "YOUR_TOKEN";
const string BODY = @"{""destination"":""Trieste"",""country"":""Italy"",""days"":4,""month"":""late October"",""party"":""two adults and my mother, who is 78"",""interests"":[""food"",""history"",""walking""],""pace"":""gentle"",""budget"":""moderate"",""notes"":""We land mid-afternoon on the first day."",""sources"":[],""page_facts"":{""records_retrieved"":0,""grounding_available"":false,""ambiguous_sources"":false,""volatile_ask"":false,""hard_flags"":0,""notes_clipped"":false},""ask_flags"":[]}";
static async Task Main() {
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
var body = new StringContent(BODY, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run-stream", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
7 Store the plan
The app declares one collection, plans. Two things about it are easy to get wrong: a declared timestamp field rejects epoch milliseconds and accepts only ISO-8601 with a Z, and records nest under doc on read — {record_id, doc:{...}}. Reading a field flat off the record returns undefined for every one of them.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/collections/plans/records" -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"uid":"abc123","destination":"Trieste","country":"Italy","days":4,"confidence":"recalled","found":true,"ran_at":"2026-08-26T09:14:00Z"}'
import requests
TOKEN = "YOUR_TOKEN"
r = requests.post(
"https://api.skillsafe.ai/v1/app-api/collections/plans/records",
headers={"Authorization": f"Bearer {TOKEN}"}, json={"uid":"abc123","destination":"Trieste","country":"Italy","days":4,"confidence":"recalled","found":True,"ran_at":"2026-08-26T09:14:00Z"},
timeout=60,
)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/collections/plans/records", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({"uid":"abc123","destination":"Trieste","country":"Italy","days":4,"confidence":"recalled","found":true,"ran_at":"2026-08-26T09:14:00Z"})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("POST",
"https://api.skillsafe.ai/v1/app-api/collections/plans/records",
strings.NewReader(`{"uid":"abc123","destination":"Trieste","country":"Italy","days":4,"confidence":"recalled","found":true,"ran_at":"2026-08-26T09:14:00Z"}`))
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 Main {
static final String TOKEN = "YOUR_TOKEN";
static final String BODY = """
{"uid":"abc123","destination":"Trieste","country":"Italy","days":4,"confidence":"recalled","found":true,"ran_at":"2026-08-26T09:14:00Z"}
""";
public static void main(String[] args) throws Exception {
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/collections/plans/records"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(BODY))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
TOKEN = "YOUR_TOKEN"
BODY = <<~JSON
{"uid":"abc123","destination":"Trieste","country":"Italy","days":4,"confidence":"recalled","found":true,"ran_at":"2026-08-26T09:14:00Z"}
JSON
uri = URI("https://api.skillsafe.ai/v1/app-api/collections/plans/records")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = BODY
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = 'YOUR_TOKEN';
$body = <<<'JSON'
{"uid":"abc123","destination":"Trieste","country":"Italy","days":4,"confidence":"recalled","found":true,"ran_at":"2026-08-26T09:14:00Z"}
JSON;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.skillsafe.ai/v1/app-api/collections/plans/records',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$out = curl_exec($ch);
curl_close($ch);
print_r(json_decode($out, true)['data']);
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program {
const string Token = "YOUR_TOKEN";
const string BODY = @"{""uid"":""abc123"",""destination"":""Trieste"",""country"":""Italy"",""days"":4,""confidence"":""recalled"",""found"":true,""ran_at"":""2026-08-26T09:14:00Z""}";
static async Task Main() {
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
var body = new StringContent(BODY, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/collections/plans/records", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
The input object, field by field
| Field | Type | Notes |
|---|---|---|
destination | string | Required. Up to 120 characters. |
country | string | Optional, and worth sending when the name is shared. |
days | number | 1–21. The plan returns exactly this many day objects. |
month | string | A month or season, not a date. |
party | string | Who is going. The single field that changes the plan most. |
interests | string[] | Up to 10. A filter, not a checklist — sending all of them is the same as sending none. |
pace | enum | gentle, steady, packed. |
budget | enum | shoestring, moderate, comfortable, no-ceiling. A level, never a number. |
notes | string | Up to 1400 characters. Clipped from the middle, both ends kept, with a marker. |
sources | object[] | Retrieved records as {id, title, abstract, url}. May be empty. |
page_facts | object | What the client already established. Reconciled against the reply. |
ask_flags | object[] | Volatile asks the client caught in notes. Every hard one must appear in declined. |
revise | object | Optional second run: {instruction, plan}. |
What comes back
output is a JSON string. Parse it, then read:
| Key | Shape | What it carries |
|---|---|---|
found | boolean | False when the destination could not be identified; days is then empty. |
confidence | enum | grounded, recalled, uncertain. About the destination's identity only. |
destination | object | Each anchor plus a _source naming a record id or model-knowledge. |
spine | string | How the place is laid out, and what that does to any plan. |
base | object | {area, why, tradeoff}. Every base costs something. |
seasonality | object | {note, stability, as_of}. |
days[] | object[] | {day, theme, district, light, light_reason, on_foot, blocks[], meals[], evening}. |
days[].blocks[] | object[] | {when, what, why_here, duration, stability, as_of, booking, booking_note, check, authority}. duration is a span, never a clock time. |
days[].meals[] | object[] | {when, area, kind, why, note}. An area and a kind of place — never a name. |
skipped[] | object[] | {what, why_not}. What was left out on purpose. |
moving_between[] | object[] | {claim, stability, as_of, check, authority}. |
book_ahead[] | object[] | {what, why, authority, note}. Whether, not how far ahead. |
check_before_you_go[] | object[] | {what, why_volatile, authority}. authority is never empty. |
declined[] | object[] | {asked, why, instead, authority}. |
budget_note | string | Which choices move the total. No figures, no arithmetic. |
limits | string | What the plan does not know, and which parts are thinner. |
Three fields carry a stability label — blocks, seasonality and
moving_between — and each takes one of durable,
slow-drift or current-state. The label classifies the
claim and never the place. slow-drift requires as_of;
a block labelled current-state is a defect, because a day should not rest on a
fact that can move before the trip.