Data API
Fart Sound Generator synthesises its audio entirely in the browser, so there is nothing server-side to call for a noise — the API here is about the shelf: the parps you chose to keep. Save one, list them, search them by meaning, delete one. Everything below is free.
What this API is, and is not
Worth being explicit, because most SkillSafe apps work the other way round.
This app declares no AI model. There is no server-side generation step, no job to poll and nothing that is billed, so this app exposes no metered execution surface at all — the endpoints other SkillSafe apps use for that simply are not part of Fart Sound Generator. What a client stores here is a recipe: an engine name, eight numbers and a seed, packed into a 27-character code. Feed that code back to the page and it reconstructs the identical waveform, sample for sample, because none of the synthesis draws on the browser’s own randomness.
Base URL and envelope
https://api.skillsafe.ai/v1/app-api
Every response is {"ok": true, "data": {...}} or
{"ok": false, "error": {"code": "...", "message": "..."}}.
Authenticate with Authorization: Bearer <token>. There is
no app-slug header — the token is scoped to this app already.
| Status | Code | What it means |
|---|---|---|
400 | VALIDATION_ERROR | A field is the wrong shape. The two that catch people here are a document posted without its doc wrapper, and a where entry given a bare value instead of an operator object. |
401 | UNAUTHENTICATED | No token, or a token the platform no longer accepts. On a first-ever visit this is the correct answer, not a fault — mint a guest identity and retry. |
403 | FORBIDDEN | The token is valid but the record belongs to another subject. Remember that every guest mint creates a new owner. |
404 | NOT_FOUND | No such record, or no such collection. A first read of a preferences key returns this and it is expected. |
409 | CONFLICT | A concurrent write lost. Re-read and retry. |
413 | PAYLOAD_TOO_LARGE | A document over the 64 KB cap. Fart Sound Generator stores a 27-character code rather than samples precisely so this never happens. |
429 | RATE_LIMITED | Too many requests, or the daily vector-operations budget is spent. Similarity search is the tight one at 30 a minute; plain queries share a 120 a minute budget. |
500 | INTERNAL | Ours. Retry with backoff and tell us if it persists. |
Steps
1. A client, once
Everything is one JSON envelope over HTTPS with a bearer token. Get a token from the session page, keep it out of your shell history, and reuse this helper for every call below.
# Every call below assumes this. The token comes from the session page:
# https://fart-sound-generator.skillsafe.ai/tokens.html
export FART_SOUND_GENERATOR_TOKEN="YOUR_TOKEN"
# A quick check that the token works.
curl -s "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN"
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://fart-sound-generator.skillsafe.ai/tokens.html
def call(method, path, json_body=None):
data = None if json_body is None else json.dumps(json_body).encode()
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
if data:
req.add_header("Content-Type", "application/json")
# Cloudflare answers urllib's default user agent with a 1010, which reads
# like an auth failure and is not one. Send a real one.
req.add_header("User-Agent", "fart-sound-generator-example/1.0")
with urllib.request.urlopen(req) as r:
payload = json.loads(r.read().decode())
if not payload.get("ok"):
raise RuntimeError(payload.get("error"))
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from https://fart-sound-generator.skillsafe.ai/tokens.html
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": "Bearer " + TOKEN,
...(body ? { "Content-Type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : undefined
});
const payload = await res.json();
if (!payload.ok) throw new Error(JSON.stringify(payload.error));
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN" // from https://fart-sound-generator.skillsafe.ai/tokens.html
func call(method, path string, body any) (map[string]any, error) {
var buf *bytes.Buffer = bytes.NewBuffer(nil)
if body != nil {
b, _ := json.Marshal(body)
buf = bytes.NewBuffer(b)
}
req, err := http.NewRequest(method, base+path, buf)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var payload struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error any `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
return nil, err
}
if !payload.OK {
return nil, errors.New(fmt.Sprint(payload.Error))
}
return payload.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class FartSoundGenerator {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN"; // from https://fart-sound-generator.skillsafe.ai/tokens.html
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.method(method, pub);
if (body != null) b.header("Content-Type", "application/json");
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://fart-sound-generator.skillsafe.ai/tokens.html
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post,
"PUT" => Net::HTTP::Put, "DELETE" => Net::HTTP::Delete }[method]
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.dump(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise payload["error"].to_s unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from https://fart-sound-generator.skillsafe.ai/tokens.html
function call($method, $path, $body = null) {
$headers = ["Authorization: Bearer " . TOKEN];
if ($body !== null) { $headers[] = "Content-Type: application/json"; }
$ch = curl_init(BASE . $path);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) { throw new Exception(json_encode($payload["error"])); }
return $payload["data"];
}
using System.Net.Http;
using System.Text;
using System.Text.Json;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN"; // from https://fart-sound-generator.skillsafe.ai/tokens.html
static readonly HttpClient Http = new HttpClient();
static async Task<string> CallAsync(string method, string path, string? body) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
req.Headers.Add("Authorization", "Bearer " + Token);
if (body != null) {
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
var res = await Http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
2. Get an identity
If you have no token at all, mint a guest one. A guest identity is a real subject that owns its own records — and every call to this endpoint mints a NEW one, so a fresh guest sees an empty shelf even when records exist. Reuse one token across writes and reads or you will be talking to a different owner each time.
POST https://api.skillsafe.ai/v1/app-api/guest
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
r = call("POST", "/guest", json={})
print(r)
const r = await call("POST", "/guest", {});
console.log(r);
body := map[string]any{
}
r, err := call("POST", "/guest", body)
if err != nil { log.Fatal(err) }
fmt.Println(r)
String r = call("POST", "/guest", """
{}
""");
System.out.println(r);
r = call("POST", "/guest", {})
puts r
$r = call("POST", "/guest", []);
print_r($r);
var r = await CallAsync("POST", "/guest", @"{}");
Console.WriteLine(r);
Response
{
"ok": true,
"data": {
"token": "aut_\u2026",
"subject_type": "guest",
"subject_id": "gst_\u2026"
}
}
3. Check who you are
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"; anything else is a guest. Fart Sound Generator has no model, so the credits figure is never spent by this app; it is your wallet balance across the platform.
GET https://api.skillsafe.ai/v1/app-api/me
curl -s -X GET "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN"
r = call("GET", "/me")
print(r)
const r = await call("GET", "/me");
console.log(r);
r, err := call("GET", "/me", nil)
if err != nil { log.Fatal(err) }
fmt.Println(r)
String r = call("GET", "/me", null);
System.out.println(r);
r = call("GET", "/me", nil)
puts r
$r = call("GET", "/me", null);
print_r($r);
var r = await CallAsync("GET", "/me", null);
Console.WriteLine(r);
Response
{
"ok": true,
"data": {
"subject_type": "user",
"subject_id": "usr_\u2026",
"credits": 12500
}
}
4. Save a parp
The document is WRAPPED in a `doc` key — posting the fields at the top level is the most common mistake here. `saved_at` is a declared timestamp field, and it accepts ISO-8601 with a Z and nothing else: epoch milliseconds, epoch seconds and naive ISO all come back as `Field type mismatch`. Undeclared keys are stored and returned intact; they are simply not filterable.
POST https://api.skillsafe.ai/v1/app-api/collections/parps/records
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/collections/parps/records" \
-H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"doc": {"name": "The one that woke the dog", "note": "For the end of a long meeting", "engine": "flap", "code": "1ff54w7xs2c8836hf4-cushion1", "seed": "cushion1", "seconds": 1.74, "saved_at": "2026-08-26T18:40:00Z"}}'
r = call("POST", "/collections/parps/records", json={
"doc": {
"name": "The one that woke the dog",
"note": "For the end of a long meeting",
"engine": "flap",
"code": "1ff54w7xs2c8836hf4-cushion1",
"seed": "cushion1",
"seconds": 1.74,
"saved_at": "2026-08-26T18:40:00Z"
}
})
print(r)
const r = await call("POST", "/collections/parps/records", {
"doc": {
"name": "The one that woke the dog",
"note": "For the end of a long meeting",
"engine": "flap",
"code": "1ff54w7xs2c8836hf4-cushion1",
"seed": "cushion1",
"seconds": 1.74,
"saved_at": "2026-08-26T18:40:00Z"
}
});
console.log(r);
body := map[string]any{
"doc": map[string]any{
"name": "The one that woke the dog",
"note": "For the end of a long meeting",
"engine": "flap",
"code": "1ff54w7xs2c8836hf4-cushion1",
"seed": "cushion1",
"seconds": 1.74,
"saved_at": "2026-08-26T18:40:00Z",
},
}
r, err := call("POST", "/collections/parps/records", body)
if err != nil { log.Fatal(err) }
fmt.Println(r)
String r = call("POST", "/collections/parps/records", """
{
"doc": {
"name": "The one that woke the dog",
"note": "For the end of a long meeting",
"engine": "flap",
"code": "1ff54w7xs2c8836hf4-cushion1",
"seed": "cushion1",
"seconds": 1.74,
"saved_at": "2026-08-26T18:40:00Z"
}
}
""");
System.out.println(r);
r = call("POST", "/collections/parps/records", {
"doc" => {
"name" => "The one that woke the dog",
"note" => "For the end of a long meeting",
"engine" => "flap",
"code" => "1ff54w7xs2c8836hf4-cushion1",
"seed" => "cushion1",
"seconds" => 1.74,
"saved_at" => "2026-08-26T18:40:00Z"
}
})
puts r
$r = call("POST", "/collections/parps/records", ["doc" => ["name" => "The one that woke the dog", "note" => "For the end of a long meeting", "engine" => "flap", "code" => "1ff54w7xs2c8836hf4-cushion1", "seed" => "cushion1", "seconds" => 1.74, "saved_at" => "2026-08-26T18:40:00Z"]]);
print_r($r);
var r = await CallAsync("POST", "/collections/parps/records", @"{""doc"": {""name"": ""The one that woke the dog"", ""note"": ""For the end of a long meeting"", ""engine"": ""flap"", ""code"": ""1ff54w7xs2c8836hf4-cushion1"", ""seed"": ""cushion1"", ""seconds"": 1.74, ""saved_at"": ""2026-08-26T18:40:00Z""}}");
Console.WriteLine(r);
Response
{
"ok": true,
"data": {
"record": {
"record_id": "rec_\u2026",
"doc": {
"name": "The one that woke the dog",
"engine": "flap"
}
}
}
}
5. List what you saved
Note the shapes, because they differ and it matters. `query` returns `{records, next_cursor}` and every record nests its fields under `doc` — read `rec.doc.name`, never `rec.name`. Every `where` entry must be an operator object; the bare-value shorthand is rejected. The sort key is `sort`, an object — `order_by` is silently ignored and you get created_at descending without being told.
POST https://api.skillsafe.ai/v1/app-api/collections/parps/query
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/collections/parps/query" \
-H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"where": {"engine": {"eq": "flap"}}, "sort": {"saved_at": "desc"}, "limit": 20}'
r = call("POST", "/collections/parps/query", json={
"where": {
"engine": {
"eq": "flap"
}
},
"sort": {
"saved_at": "desc"
},
"limit": 20
})
print(r)
const r = await call("POST", "/collections/parps/query", {
"where": {
"engine": {
"eq": "flap"
}
},
"sort": {
"saved_at": "desc"
},
"limit": 20
});
console.log(r);
body := map[string]any{
"where": map[string]any{
"engine": map[string]any{
"eq": "flap",
},
},
"sort": map[string]any{
"saved_at": "desc",
},
"limit": 20,
}
r, err := call("POST", "/collections/parps/query", body)
if err != nil { log.Fatal(err) }
fmt.Println(r)
String r = call("POST", "/collections/parps/query", """
{
"where": {
"engine": {
"eq": "flap"
}
},
"sort": {
"saved_at": "desc"
},
"limit": 20
}
""");
System.out.println(r);
r = call("POST", "/collections/parps/query", {
"where" => {
"engine" => {
"eq" => "flap"
}
},
"sort" => {
"saved_at" => "desc"
},
"limit" => 20
})
puts r
$r = call("POST", "/collections/parps/query", ["where" => ["engine" => ["eq" => "flap"]], "sort" => ["saved_at" => "desc"], "limit" => 20]);
print_r($r);
var r = await CallAsync("POST", "/collections/parps/query", @"{""where"": {""engine"": {""eq"": ""flap""}}, ""sort"": {""saved_at"": ""desc""}, ""limit"": 20}");
Console.WriteLine(r);
Response
{
"ok": true,
"data": {
"records": [
{
"record_id": "rec_\u2026",
"doc": {
"name": "The one that woke the dog",
"code": "1ff54w7xs2c8836hf4-cushion1"
}
}
]
},
"meta": {
"pagination": {
"has_more": false,
"next_cursor": null
}
}
}
6. Search by meaning
Vector similarity over the declared embed fields — name, note and engine. This endpoint resolves to the RECORDS ARRAY ITSELF, not to an object with a `records` key like `query` does; reading `.records` off it yields undefined and every search silently returns nothing. Rate limited to 30 requests a minute per IP, and free apps share a daily ceiling of 5,000 vector operations, over which this answers 429 while plain queries keep working.
POST https://api.skillsafe.ai/v1/app-api/collections/parps/similar
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/collections/parps/similar" \
-H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "something for the end of a long meeting", "limit": 10}'
r = call("POST", "/collections/parps/similar", json={
"text": "something for the end of a long meeting",
"limit": 10
})
print(r)
const r = await call("POST", "/collections/parps/similar", {
"text": "something for the end of a long meeting",
"limit": 10
});
console.log(r);
body := map[string]any{
"text": "something for the end of a long meeting",
"limit": 10,
}
r, err := call("POST", "/collections/parps/similar", body)
if err != nil { log.Fatal(err) }
fmt.Println(r)
String r = call("POST", "/collections/parps/similar", """
{
"text": "something for the end of a long meeting",
"limit": 10
}
""");
System.out.println(r);
r = call("POST", "/collections/parps/similar", {
"text" => "something for the end of a long meeting",
"limit" => 10
})
puts r
$r = call("POST", "/collections/parps/similar", ["text" => "something for the end of a long meeting", "limit" => 10]);
print_r($r);
var r = await CallAsync("POST", "/collections/parps/similar", @"{""text"": ""something for the end of a long meeting"", ""limit"": 10}");
Console.WriteLine(r);
Response
{
"ok": true,
"data": {
"records": [
{
"record_id": "rec_\u2026",
"doc": {
"name": "The one that woke the dog"
},
"score": 0.81
}
]
}
}
7. Delete one
Hard delete, scoped to the calling subject. There is no undo and no bin.
DELETE https://api.skillsafe.ai/v1/app-api/collections/parps/records/rec_example
curl -s -X DELETE "https://api.skillsafe.ai/v1/app-api/collections/parps/records/rec_example" \
-H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN"
r = call("DELETE", "/collections/parps/records/rec_example")
print(r)
const r = await call("DELETE", "/collections/parps/records/rec_example");
console.log(r);
r, err := call("DELETE", "/collections/parps/records/rec_example", nil)
if err != nil { log.Fatal(err) }
fmt.Println(r)
String r = call("DELETE", "/collections/parps/records/rec_example", null);
System.out.println(r);
r = call("DELETE", "/collections/parps/records/rec_example", nil)
puts r
$r = call("DELETE", "/collections/parps/records/rec_example", null);
print_r($r);
var r = await CallAsync("DELETE", "/collections/parps/records/rec_example", null);
Console.WriteLine(r);
Response
{
"ok": true,
"data": {
"deleted": true
}
}
8. Check your quotas
Usage against quota for this app, including the vector-operations budget that governs step 6. For a completely-free app, `vector_ops.daily_ops_remaining` is a number rather than null.
GET https://api.skillsafe.ai/v1/app-api/storage
curl -s -X GET "https://api.skillsafe.ai/v1/app-api/storage" \
-H "Authorization: Bearer $FART_SOUND_GENERATOR_TOKEN"
r = call("GET", "/storage")
print(r)
const r = await call("GET", "/storage");
console.log(r);
r, err := call("GET", "/storage", nil)
if err != nil { log.Fatal(err) }
fmt.Println(r)
String r = call("GET", "/storage", null);
System.out.println(r);
r = call("GET", "/storage", nil)
puts r
$r = call("GET", "/storage", null);
print_r($r);
var r = await CallAsync("GET", "/storage", null);
Console.WriteLine(r);
Response
{
"ok": true,
"data": {
"records": {
"used": 12,
"limit": 1000
},
"vector_ops": {
"daily_ops_remaining": 4988
}
}
}
The share code
Twenty-seven characters that reproduce a noise exactly. Documented so a script can build one without running the page.
1 b 8f3k00zz... -cushion1
^ ^ \____________/ \__ seed, verbatim, lowercase alphanumerics
| \__ engine: b = buzz, f = flap
\____ format version, currently 1
The eight parameters follow the engine letter, two lowercase base-36
characters each, in the fixed order below. Each pair is the parameter’s
position in its own range scaled to 0–1295, so
round(1295 * (value - min) / (max - min)) encodes and the
inverse decodes. That is 1,296 steps per control — finer than the
sliders and far finer than anyone can hear.
| # | Parameter | Range | Unit | Effect |
|---|---|---|---|---|
| 1 | duration | 0.15 – 3 | seconds | Total length, before the room tail is added. |
| 2 | pitch | 40 – 320 | Hz | Fundamental of the pulse train. Ignored by the flap engine, where pitch is an output. |
| 3 | tightness | 0 – 1 | — | Pulse width and filter resonance together. |
| 4 | wetness | 0 – 1 | — | Noise against tone, plus threshold jitter on the membrane model. |
| 5 | burstiness | 0 – 1 | — | Number of flaps and, inversely, how much of each slot they fill. |
| 6 | glide | -1 – 1 | — | Pitch travel, up to about 1.15 octaves. Negative falls. |
| 7 | room | 0 – 1 | — | Reverb mix and decay; also sets the tail length appended to the buffer. |
| 8 | grit | 0 – 1 | — | Saturation drive. Exactly unity bypass at 0. |
Limits worth knowing
| Records per owner | 1,000 |
| Document size | 64 KB — a Fart Sound Generator row is about 400 bytes |
| Query limit | 100 per page, cursor paginated |
| Data endpoints | 120 requests per minute |
| Similarity search | 30 requests per minute, and 5,000 vector operations a day for a free app |
| Embedded fields | name, note, engine — set at declaration time and never backfilled |