Travel Guide

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

HTTPcodeWhat it means
401UNAUTHENTICATEDNo token, or a token that has expired. Mint a new guest token, or sign in.
402INSUFFICIENT_CREDITSThe balance is below min_credits. Top up, or shorten the trip.
403FORBIDDENA guest token tried to run or to search. Both need a signed-in user.
404NOT_FOUNDWrong slug, or a job id that never existed.
409IDEMPOTENCY_CONFLICTThe same Idempotency-Key was reused with a different body.
422INVALID_INPUTThe input object did not match the app's contract. Check field names.
429RATE_LIMITEDToo many calls. Back off; do not retry in a tight loop.
503UPSTREAM_UNAVAILABLEThe 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

FieldTypeNotes
destinationstringRequired. Up to 120 characters.
countrystringOptional, and worth sending when the name is shared.
daysnumber1–21. The plan returns exactly this many day objects.
monthstringA month or season, not a date.
partystringWho is going. The single field that changes the plan most.
interestsstring[]Up to 10. A filter, not a checklist — sending all of them is the same as sending none.
paceenumgentle, steady, packed.
budgetenumshoestring, moderate, comfortable, no-ceiling. A level, never a number.
notesstringUp to 1400 characters. Clipped from the middle, both ends kept, with a marker.
sourcesobject[]Retrieved records as {id, title, abstract, url}. May be empty.
page_factsobjectWhat the client already established. Reconciled against the reply.
ask_flagsobject[]Volatile asks the client caught in notes. Every hard one must appear in declined.
reviseobjectOptional second run: {instruction, plan}.

What comes back

output is a JSON string. Parse it, then read:

KeyShapeWhat it carries
foundbooleanFalse when the destination could not be identified; days is then empty.
confidenceenumgrounded, recalled, uncertain. About the destination's identity only.
destinationobjectEach anchor plus a _source naming a record id or model-knowledge.
spinestringHow the place is laid out, and what that does to any plan.
baseobject{area, why, tradeoff}. Every base costs something.
seasonalityobject{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_notestringWhich choices move the total. No figures, no arithmetic.
limitsstringWhat 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.