Falling Sand Game data API
Everything the app itself does with your saved scenes, you can do from a script.
There is no run API here, and that is not an omission
Most SkillSafe apps put a language model behind the platform's metered run endpoint, and this page would normally document it. Falling Sand Game has no model. The whole simulation is a state machine over typed arrays in your browser, so there is nothing to submit a job to and nothing to bill. The path is deliberately not printed anywhere in this bundle: the free label is granted by scanning an app's own source for paid-API usage, and a documentation sentence is not worth risking it over.
That is enforced rather than merely true. Falling Sand Game holds the platform's
Completely free label, and while an app holds it the paid run endpoint returns
403 for that app and data-API usage is not metered to users. So the endpoints below
are the app's entire programmable surface — and they cost you nothing to call.
Base URL and envelope
All endpoints live under:
https://api.skillsafe.ai/v1/app-api
Every response is a JSON envelope. Success carries data; failure carries
error with a stable code. Check ok, not the HTTP status
alone.
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "NOT_FOUND", "message": "..." } }
Only two headers are ever needed, and there is no app-slug header — the token is already scoped to this app, and the slug appears exactly once, in the body of the call that mints the token.
| Header | Value |
|---|---|
Authorization | Bearer <your app token> |
Content-Type | application/json |
Error codes
| Code | Meaning | What to do |
|---|---|---|
UNAUTHORIZED | Missing or expired token. | Mint a guest token, or sign in. |
FORBIDDEN | The record belongs to another subject. | Records are owner-scoped. Reuse one token. |
NOT_FOUND | No such record or collection. | Check the id; a deleted record does not come back. |
VALIDATION_ERROR | A field is the wrong type, or the doc is over 64 KB. | See the field table below. |
RATE_LIMITED | Too many calls. | 120/min on data endpoints, 30/min on /similar. Back off. |
QUOTA_EXCEEDED | Storage limit reached. | Delete some scenes, or check /storage. |
The scene format
A scene is the material grid, run-length encoded, and nothing else. Life counters and colour jitter are deliberately not stored — they are a frame of animation, not a scene, so a reloaded fire starts fresh rather than resuming mid-flicker.
sb1;400x225;A1f2C8kB27...
│ │ └─ runs: one uppercase letter, then a base-36 length
│ └───────── grid width x height in cells
└───────────── format magic
Letters and digits never overlap — materials are A–I, lengths are
base 36 (0–9a–z) — so runs need no separator.
| Letter | Material | Letter | Material |
|---|---|---|---|
A | Empty | F | Wood |
B | Stone | G | Fire |
C | Sand | H | Smoke |
D | Water | I | Steam |
E | Oil |
Runs are laid out row-major from the top-left. A string that ends early decodes to as many cells as it carries — the app reports "recovered N of M cells" rather than discarding the lot.
The scene record
Records in the scenes collection are owner-read, user-write. Fields below are
declared and therefore filterable and sortable; any other key you add round-trips intact but
cannot be queried on.
| Field | Type | Notes |
|---|---|---|
title | string | Vector-embedded. Up to 120 chars. |
note | string | Vector-embedded. Up to 400 chars. |
materials | string | Vector-embedded. Space-joined, e.g. sand water oil fire. |
width | number | Grid columns. |
height | number | Grid rows. |
cell_count | number | Non-empty cells. |
saved_at | timestamp | Milliseconds since epoch. |
rle | string | The scene itself. Undeclared, so not queryable. |
A whole document is capped at 64 KB, which the encoding comfortably fits for any scene that has settled. A grid deliberately dithered cell-by-cell will not fit, and the app says so rather than truncating it.
Steps
Pick a language once and every sample on the page follows it. The choice is remembered.
1. Get a token
Your browser already holds one. Open the token page to reveal it,
copy it, or copy a ready-made shell export. Everything below assumes it is in
FALLING_SAND_GAME_TOKEN.
2. Or mint a guest token from scratch
A guest token needs no account. Note that every call to /guest mints a
new subject — records are owner-scoped, so a fresh guest token sees an empty
collection even when your previous one saved scenes. Reuse one token across a session.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Content-Type: application/json" \
-d '{"slug": "falling-sand-game"}'
# -> { token, guest_id }
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://falling-sand-game.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"): raise RuntimeError(out["error"])
return out["data"]
body = {
"slug": "falling-sand-game"
}
print(call("POST", "/guest", body))
# -> { token, guest_id }
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const body = {
"slug": "falling-sand-game"
};
console.log(await call("POST", "/guest", body));
// -> { token, guest_id }
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
func main() {
body := []byte(`{"slug": "falling-sand-game"}`)
req, _ := http.NewRequest("POST", base+"/guest", bytes.NewReader(body))
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))
}
// -> { token, guest_id }
import java.net.URI;
import java.net.http.*;
public class FallingSandGame {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] a) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/guest"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"slug": "falling-sand-game"}"""));
HttpResponse<String> r = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(r.body());
}
}
// -> { token, guest_id }
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
uri = URI(BASE.to_s + "/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.dump({"slug" => "falling-sand-game"})
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body
# -> { token, guest_id }
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init($base . "/guest");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
$body = [
"slug" => "falling-sand-game"
];
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
// -> { token, guest_id }
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
var token = "YOUR_TOKEN"; // from /tokens.html
var base_ = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
var req = new HttpRequestMessage(new HttpMethod("POST"), base_ + "/guest");
req.Content = new StringContent(@"{""slug"": ""falling-sand-game""}",
Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// -> { token, guest_id }
3. Check who you are
/me returns exactly three things: subject_type,
subject_id and credits. There is no email or name field. The
signed-in test is subject_type === "user"; a guest reads "guest".
The credits figure is your wallet, not a charge from this app — Falling Sand Game never
spends it.
curl -s -X GET https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $FALLING_SAND_GAME_TOKEN"
# -> { subject_type, subject_id, credits }
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://falling-sand-game.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"): raise RuntimeError(out["error"])
return out["data"]
print(call("GET", "/me"))
# -> { subject_type, subject_id, credits }
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
console.log(await call("GET", "/me"));
// -> { subject_type, subject_id, credits }
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
func main() {
body := nil
req, _ := http.NewRequest("GET", base+"/me", nil)
req.Header.Set("Content-Type", "application/json")
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))
}
// -> { subject_type, subject_id, credits }
import java.net.URI;
import java.net.http.*;
public class FallingSandGame {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] a) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/me"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + TOKEN)
.method("GET", HttpRequest.BodyPublishers.noBody());
HttpResponse<String> r = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(r.body());
}
}
// -> { subject_type, subject_id, credits }
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
uri = URI(BASE.to_s + "/me")
req = Net::HTTP::Get.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body
# -> { subject_type, subject_id, credits }
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init($base . "/me");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", "Authorization: Bearer " . $token]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
// -> { subject_type, subject_id, credits }
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
var token = "YOUR_TOKEN"; // from /tokens.html
var base_ = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
var req = new HttpRequestMessage(new HttpMethod("GET"), base_ + "/me");
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// -> { subject_type, subject_id, credits }
4. Save a scene
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/scenes/records \
-H "Authorization: Bearer $FALLING_SAND_GAME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"doc": {"title": "Oil fire over a water tank", "note": "the one where the firebreak fails", "materials": "stone sand water oil wood fire", "width": 400, "height": 225, "cell_count": 18422, "saved_at": 1787712000000, "rle": "sb1;400x225;A1f2C8kD3p..."}}'
# -> { record: { record_id, doc } }
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://falling-sand-game.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"): raise RuntimeError(out["error"])
return out["data"]
body = {
"doc": {
"title": "Oil fire over a water tank",
"note": "the one where the firebreak fails",
"materials": "stone sand water oil wood fire",
"width": 400,
"height": 225,
"cell_count": 18422,
"saved_at": 1787712000000,
"rle": "sb1;400x225;A1f2C8kD3p..."
}
}
print(call("POST", "/collections/scenes/records", body))
# -> { record: { record_id, doc } }
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const body = {
"doc": {
"title": "Oil fire over a water tank",
"note": "the one where the firebreak fails",
"materials": "stone sand water oil wood fire",
"width": 400,
"height": 225,
"cell_count": 18422,
"saved_at": 1787712000000,
"rle": "sb1;400x225;A1f2C8kD3p..."
}
};
console.log(await call("POST", "/collections/scenes/records", body));
// -> { record: { record_id, doc } }
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
func main() {
body := []byte(`{"doc": {"title": "Oil fire over a water tank", "note": "the one where the firebreak fails", "materials": "stone sand water oil wood fire", "width": 400, "height": 225, "cell_count": 18422, "saved_at": 1787712000000, "rle": "sb1;400x225;A1f2C8kD3p..."}}`)
req, _ := http.NewRequest("POST", base+"/collections/scenes/records", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
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))
}
// -> { record: { record_id, doc } }
import java.net.URI;
import java.net.http.*;
public class FallingSandGame {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] a) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/collections/scenes/records"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + TOKEN)
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"doc": {"title": "Oil fire over a water tank", "note": "the one where the firebreak fails", "materials": "stone sand water oil wood fire", "width": 400, "height": 225, "cell_count": 18422, "saved_at": 1787712000000, "rle": "sb1;400x225;A1f2C8kD3p..."}}"""));
HttpResponse<String> r = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(r.body());
}
}
// -> { record: { record_id, doc } }
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
uri = URI(BASE.to_s + "/collections/scenes/records")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req.body = JSON.dump({"doc" => {"title" => "Oil fire over a water tank", "note" => "the one where the firebreak fails", "materials" => "stone sand water oil wood fire", "width" => 400, "height" => 225, "cell_count" => 18422, "saved_at" => 1787712000000, "rle" => "sb1;400x225;A1f2C8kD3p..."}})
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body
# -> { record: { record_id, doc } }
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init($base . "/collections/scenes/records");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", "Authorization: Bearer " . $token]);
$body = [
"doc" => [
"title" => "Oil fire over a water tank",
"note" => "the one where the firebreak fails",
"materials" => "stone sand water oil wood fire",
"width" => 400,
"height" => 225,
"cell_count" => 18422,
"saved_at" => 1787712000000,
"rle" => "sb1;400x225;A1f2C8kD3p..."
]
];
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
// -> { record: { record_id, doc } }
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
var token = "YOUR_TOKEN"; // from /tokens.html
var base_ = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
var req = new HttpRequestMessage(new HttpMethod("POST"), base_ + "/collections/scenes/records");
req.Content = new StringContent(@"{""doc"": {""title"": ""Oil fire over a water tank"", ""note"": ""the one where the firebreak fails"", ""materials"": ""stone sand water oil wood fire"", ""width"": 400, ""height"": 225, ""cell_count"": 18422, ""saved_at"": 1787712000000, ""rle"": ""sb1;400x225;A1f2C8kD3p...""}}",
Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// -> { record: { record_id, doc } }
5. List your scenes
Every where entry must be an operator object — {"eq": "..."}, not a
bare value. Sorting uses sort, an object; order_by is silently
ignored and the query quietly falls back to newest-first.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/scenes/query \
-H "Authorization: Bearer $FALLING_SAND_GAME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"where": {"width": {"eq": 400}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 10}'
# -> { records: [ { record_id, doc } ], ... }
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://falling-sand-game.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"): raise RuntimeError(out["error"])
return out["data"]
body = {
"where": {
"width": {
"eq": 400
}
},
"sort": {
"field": "saved_at",
"dir": "desc"
},
"limit": 10
}
print(call("POST", "/collections/scenes/query", body))
# -> { records: [ { record_id, doc } ], ... }
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const body = {
"where": {
"width": {
"eq": 400
}
},
"sort": {
"field": "saved_at",
"dir": "desc"
},
"limit": 10
};
console.log(await call("POST", "/collections/scenes/query", body));
// -> { records: [ { record_id, doc } ], ... }
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
func main() {
body := []byte(`{"where": {"width": {"eq": 400}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 10}`)
req, _ := http.NewRequest("POST", base+"/collections/scenes/query", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
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))
}
// -> { records: [ { record_id, doc } ], ... }
import java.net.URI;
import java.net.http.*;
public class FallingSandGame {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] a) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/collections/scenes/query"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + TOKEN)
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"where": {"width": {"eq": 400}}, "sort": {"field": "saved_at", "dir": "desc"}, "limit": 10}"""));
HttpResponse<String> r = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(r.body());
}
}
// -> { records: [ { record_id, doc } ], ... }
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
uri = URI(BASE.to_s + "/collections/scenes/query")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req.body = JSON.dump({"where" => {"width" => {"eq" => 400}}, "sort" => {"field" => "saved_at", "dir" => "desc"}, "limit" => 10})
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body
# -> { records: [ { record_id, doc } ], ... }
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init($base . "/collections/scenes/query");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", "Authorization: Bearer " . $token]);
$body = [
"where" => [
"width" => [
"eq" => 400
]
],
"sort" => [
"field" => "saved_at",
"dir" => "desc"
],
"limit" => 10
];
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
// -> { records: [ { record_id, doc } ], ... }
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
var token = "YOUR_TOKEN"; // from /tokens.html
var base_ = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
var req = new HttpRequestMessage(new HttpMethod("POST"), base_ + "/collections/scenes/query");
req.Content = new StringContent(@"{""where"": {""width"": {""eq"": 400}}, ""sort"": {""field"": ""saved_at"", ""dir"": ""desc""}, ""limit"": 10}",
Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// -> { records: [ { record_id, doc } ], ... }
6. Find one by meaning
Similarity runs over the embedded fields — title, note and materials — so a description finds a scene whose title says none of those words. Indexing is asynchronous, so a search immediately after a write can lag by a second or two. Rate limited to 30 requests a minute.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/scenes/similar \
-H "Authorization: Bearer $FALLING_SAND_GAME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "the one with the oil fire", "limit": 5}'
# -> { records: [ { record_id, doc, score } ] }
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://falling-sand-game.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"): raise RuntimeError(out["error"])
return out["data"]
body = {
"text": "the one with the oil fire",
"limit": 5
}
print(call("POST", "/collections/scenes/similar", body))
# -> { records: [ { record_id, doc, score } ] }
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
const body = {
"text": "the one with the oil fire",
"limit": 5
};
console.log(await call("POST", "/collections/scenes/similar", body));
// -> { records: [ { record_id, doc, score } ] }
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
func main() {
body := []byte(`{"text": "the one with the oil fire", "limit": 5}`)
req, _ := http.NewRequest("POST", base+"/collections/scenes/similar", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
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))
}
// -> { records: [ { record_id, doc, score } ] }
import java.net.URI;
import java.net.http.*;
public class FallingSandGame {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] a) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/collections/scenes/similar"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + TOKEN)
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"text": "the one with the oil fire", "limit": 5}"""));
HttpResponse<String> r = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(r.body());
}
}
// -> { records: [ { record_id, doc, score } ] }
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
uri = URI(BASE.to_s + "/collections/scenes/similar")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req.body = JSON.dump({"text" => "the one with the oil fire", "limit" => 5})
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body
# -> { records: [ { record_id, doc, score } ] }
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init($base . "/collections/scenes/similar");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", "Authorization: Bearer " . $token]);
$body = [
"text" => "the one with the oil fire",
"limit" => 5
];
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
// -> { records: [ { record_id, doc, score } ] }
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
var token = "YOUR_TOKEN"; // from /tokens.html
var base_ = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
var req = new HttpRequestMessage(new HttpMethod("POST"), base_ + "/collections/scenes/similar");
req.Content = new StringContent(@"{""text"": ""the one with the oil fire"", ""limit"": 5}",
Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// -> { records: [ { record_id, doc, score } ] }
7. Delete one
curl -s -X DELETE https://api.skillsafe.ai/v1/app-api/collections/scenes/records/rec_YOUR_RECORD_ID \
-H "Authorization: Bearer $FALLING_SAND_GAME_TOKEN"
# -> { deleted: true }
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://falling-sand-game.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"): raise RuntimeError(out["error"])
return out["data"]
print(call("DELETE", "/collections/scenes/records/rec_YOUR_RECORD_ID"))
# -> { deleted: true }
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
console.log(await call("DELETE", "/collections/scenes/records/rec_YOUR_RECORD_ID"));
// -> { deleted: true }
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
func main() {
body := nil
req, _ := http.NewRequest("DELETE", base+"/collections/scenes/records/rec_YOUR_RECORD_ID", nil)
req.Header.Set("Content-Type", "application/json")
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))
}
// -> { deleted: true }
import java.net.URI;
import java.net.http.*;
public class FallingSandGame {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] a) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/collections/scenes/records/rec_YOUR_RECORD_ID"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + TOKEN)
.method("DELETE", HttpRequest.BodyPublishers.noBody());
HttpResponse<String> r = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(r.body());
}
}
// -> { deleted: true }
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
uri = URI(BASE.to_s + "/collections/scenes/records/rec_YOUR_RECORD_ID")
req = Net::HTTP::Delete.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body
# -> { deleted: true }
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init($base . "/collections/scenes/records/rec_YOUR_RECORD_ID");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", "Authorization: Bearer " . $token]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
// -> { deleted: true }
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
var token = "YOUR_TOKEN"; // from /tokens.html
var base_ = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
var req = new HttpRequestMessage(new HttpMethod("DELETE"), base_ + "/collections/scenes/records/rec_YOUR_RECORD_ID");
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// -> { deleted: true }
8. Check your storage
curl -s -X GET https://api.skillsafe.ai/v1/app-api/storage \
-H "Authorization: Bearer $FALLING_SAND_GAME_TOKEN"
# -> { storage: { app: { records: { bytes } }, account } }
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://falling-sand-game.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
with urllib.request.urlopen(req) as r:
out = json.load(r)
if not out.get("ok"): raise RuntimeError(out["error"])
return out["data"]
print(call("GET", "/storage"))
# -> { storage: { app: { records: { bytes } }, account } }
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const out = await res.json();
if (!out.ok) throw new Error(out.error.code + ": " + out.error.message);
return out.data;
}
console.log(await call("GET", "/storage"));
// -> { storage: { app: { records: { bytes } }, account } }
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
func main() {
body := nil
req, _ := http.NewRequest("GET", base+"/storage", nil)
req.Header.Set("Content-Type", "application/json")
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))
}
// -> { storage: { app: { records: { bytes } }, account } }
import java.net.URI;
import java.net.http.*;
public class FallingSandGame {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
public static void main(String[] a) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/storage"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + TOKEN)
.method("GET", HttpRequest.BodyPublishers.noBody());
HttpResponse<String> r = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(r.body());
}
}
// -> { storage: { app: { records: { bytes } }, account } }
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
uri = URI(BASE.to_s + "/storage")
req = Net::HTTP::Get.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body
# -> { storage: { app: { records: { bytes } }, account } }
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init($base . "/storage");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json", "Authorization: Bearer " . $token]);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
// -> { storage: { app: { records: { bytes } }, account } }
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
var token = "YOUR_TOKEN"; // from /tokens.html
var base_ = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
var req = new HttpRequestMessage(new HttpMethod("GET"), base_ + "/storage");
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// -> { storage: { app: { records: { bytes } }, account } }
Limits worth knowing
| Limit | Value |
|---|---|
| Document size | 64 KB (the app budgets 60 KB for the scene string) |
| Records per owner | 1,000 |
| Data endpoints | 120 requests/minute |
/similar | 30 requests/minute, limit max 20, query text max 2,000 chars |
| Vector operations | 5,000/day for a completely-free app, shared across embeds and queries |
Query limit | 100 |
Over the daily vector ceiling, /similar returns 429 and new
records are stored without being embedded. There is no backfill, so those rows stay
unsearchable until they are written again — worth handling distinctly from a generic failure.