← Fart Sound Generator / Data API
Session & tokens

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.

The practical consequence. A row on the shelf is about 400 bytes. The same parp stored as audio would be roughly 350 KB, and would not fit the platform’s 64 KB per-document cap. Storing the recipe is not a space optimisation bolted on afterwards — it is the only shape that works.

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.

StatusCodeWhat it means
400VALIDATION_ERRORA 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.
401UNAUTHENTICATEDNo 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.
403FORBIDDENThe token is valid but the record belongs to another subject. Remember that every guest mint creates a new owner.
404NOT_FOUNDNo such record, or no such collection. A first read of a preferences key returns this and it is expected.
409CONFLICTA concurrent write lost. Re-read and retry.
413PAYLOAD_TOO_LARGEA document over the 64 KB cap. Fart Sound Generator stores a 27-character code rather than samples precisely so this never happens.
429RATE_LIMITEDToo 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.
500INTERNALOurs. 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
    }
  }
}

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.

#ParameterRangeUnitEffect
1duration0.15 – 3secondsTotal length, before the room tail is added.
2pitch40 – 320HzFundamental of the pulse train. Ignored by the flap engine, where pitch is an output.
3tightness0 – 1Pulse width and filter resonance together.
4wetness0 – 1Noise against tone, plus threshold jitter on the membrane model.
5burstiness0 – 1Number of flaps and, inversely, how much of each slot they fill.
6glide-1 – 1Pitch travel, up to about 1.15 octaves. Negative falls.
7room0 – 1Reverb mix and decay; also sets the tail length appended to the buffer.
8grit0 – 1Saturation drive. Exactly unity bypass at 0.
Decoding is deliberately forgiving. A code truncated by a chat client still yields whatever parameters survived, with the rest falling back to defaults and the page saying which. A code carrying an unknown version character is refused outright rather than guessed at, because handing somebody a different noise and calling it theirs is worse than saying no.

Limits worth knowing

Records per owner1,000
Document size64 KB — a Fart Sound Generator row is about 400 bytes
Query limit100 per page, cursor paginated
Data endpoints120 requests per minute
Similarity search30 requests per minute, and 5,000 vector operations a day for a free app
Embedded fieldsname, note, engine — set at declaration time and never backfilled