Pic Lite API
Read this first: there is no compression API. Pic Lite does all of its image work in the browser — decoding, resizing, quantising and re-encoding all happen in the tab, and no image is ever sent anywhere. There is no endpoint you can post a JPEG to, and adding one would break the only promise the app makes.
What is reachable over HTTP is the account side: the token that identifies you,
your saved settings presets, and the record of past batches. That is what this page
documents. If you want to compress images from a script, the honest answer is that this
app is the wrong tool — use mozjpeg, oxipng,
cwebp or gifsicle locally, which is what those tools are for.
Base URL and envelope
Base URL: https://api.skillsafe.ai/v1/app-api. Every request carries
Authorization: Bearer <token> and X-App-Slug: pic-lite.
Every response is one of two shapes:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
Error codes
| Code | HTTP | What it means here |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed or expired token. Mint a new one at /tokens.html. |
FORBIDDEN | 403 | The token is valid but not for this app, or you asked for another owner's records. |
VALIDATION_ERROR | 400 | A malformed body. The most common causes are a bare value in where instead of an operator object, and omitting slug from the /guest body. |
NOT_FOUND | 404 | Unknown collection or record id. |
RATE_LIMITED | 429 | 120 requests per minute per IP on the data endpoints. Back off; do not tight-loop. |
PAYLOAD_TOO_LARGE | 413 | A record document over the 64 KB cap. The app trims per-file rows out of a batch record before this happens. |
A tiny client
Everything below is this, with a different path.
curl -s https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "X-App-Slug: pic-lite"
import requests
TOKEN = "YOUR_TOKEN" # from https://pic-lite.skillsafe.ai/tokens.html
H = {"Authorization": f"Bearer {TOKEN}", "X-App-Slug": "pic-lite"}
r = requests.get("https://api.skillsafe.ai/v1/app-api/me", headers=H, timeout=30)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN"; // from https://pic-lite.skillsafe.ai/tokens.html
const H = { "Authorization": `Bearer ${TOKEN}`, "X-App-Slug": "pic-lite" };
const res = await fetch("https://api.skillsafe.ai/v1/app-api/me", { headers: H });
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", "pic-lite")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] a) throws Exception {
String token = System.getenv("SKILLSAFE_TOKEN");
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token)
.header("X-App-Slug", "pic-lite")
.GET().build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
token = ENV.fetch("SKILLSAFE_TOKEN")
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
req["X-App-Slug"] = "pic-lite"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = getenv("SKILLSAFE_TOKEN");
$headers = ["Authorization: Bearer $token", "X-App-Slug: pic-lite"];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Add("X-App-Slug", "pic-lite");
var res = await http.GetStringAsync("https://api.skillsafe.ai/v1/app-api/me");
Console.WriteLine(res);
}
}
Steps
1. Get a token
Every call needs a bearer token scoped to this app. The easiest place to get one is this
app's own token page — it reads the token this browser
already holds, shows whether it is a personal or a guest token, and gives you a
Copy shell export button. Nothing below needs the developer console.
Two kinds exist. A guest token is minted automatically by the app and is
enough for /me and /storage. A personal token,
from signing in, is what lets you read and write the saved presets and batch history that
belong to your account. Both are passed the same way.
The samples read the token from an environment variable named SKILLSAFE_TOKEN
rather than embedding it. Do not commit a token; it carries your account's authority for
this app.
export SKILLSAFE_TOKEN="aut_..."
2. Mint a guest token
If you have no token at all, mint a guest one. The slug goes in the request
body as well as the X-App-Slug header — a bare
{} is rejected with 400 slug is required.
The response is { "token", "guest_id", "expires_at" }. There is no
subject_type and no credits field on this response; if you want
those, call /me afterwards.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "X-App-Slug: pic-lite" \
-H "Content-Type: application/json" \
-d '{"slug": "pic-lite"}'
import requests
TOKEN = "YOUR_TOKEN" # from https://pic-lite.skillsafe.ai/tokens.html
H = {"Authorization": f"Bearer {TOKEN}", "X-App-Slug": "pic-lite"}
body = {
"slug": "pic-lite"
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/guest", headers=H, json=body, timeout=30)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN"; // from https://pic-lite.skillsafe.ai/tokens.html
const H = { "Authorization": `Bearer ${TOKEN}`, "X-App-Slug": "pic-lite" };
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({"slug": "pic-lite"})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
body := []byte(`{"slug": "pic-lite"}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", "pic-lite")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] a) throws Exception {
String token = System.getenv("SKILLSAFE_TOKEN");
String body = """
{"slug": "pic-lite"}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Authorization", "Bearer " + token)
.header("X-App-Slug", "pic-lite")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
token = ENV.fetch("SKILLSAFE_TOKEN")
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["X-App-Slug"] = "pic-lite"
req["Content-Type"] = "application/json"
req.body = {"slug": "pic-lite"}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = getenv("SKILLSAFE_TOKEN");
$headers = ["Authorization: Bearer $token", "X-App-Slug: pic-lite"];
$headers[] = "Content-Type: application/json";
$body = '{"slug": "pic-lite"}';
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Add("X-App-Slug", "pic-lite");
var body = new StringContent(@"{""slug"": ""pic-lite""}", System.Text.Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/guest", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
3. Check who you are
GET /me tells you whether the token is a user or a
guest, and reports a credit balance. Pic Lite never spends
credits — there is no model in this app and no metered call in its run
path — so the balance is informational only. Compression happens in the browser.
curl -s https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "X-App-Slug: pic-lite"
import requests
TOKEN = "YOUR_TOKEN" # from https://pic-lite.skillsafe.ai/tokens.html
H = {"Authorization": f"Bearer {TOKEN}", "X-App-Slug": "pic-lite"}
r = requests.get("https://api.skillsafe.ai/v1/app-api/me", headers=H, timeout=30)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN"; // from https://pic-lite.skillsafe.ai/tokens.html
const H = { "Authorization": `Bearer ${TOKEN}`, "X-App-Slug": "pic-lite" };
const res = await fetch("https://api.skillsafe.ai/v1/app-api/me", { headers: H });
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", "pic-lite")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] a) throws Exception {
String token = System.getenv("SKILLSAFE_TOKEN");
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token)
.header("X-App-Slug", "pic-lite")
.GET().build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
token = ENV.fetch("SKILLSAFE_TOKEN")
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
req["X-App-Slug"] = "pic-lite"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = getenv("SKILLSAFE_TOKEN");
$headers = ["Authorization: Bearer $token", "X-App-Slug: pic-lite"];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Add("X-App-Slug", "pic-lite");
var res = await http.GetStringAsync("https://api.skillsafe.ai/v1/app-api/me");
Console.WriteLine(res);
}
}
4. Read and write your presets
Saved settings presets live in the per-user key-value store under the key
presets. The value is an array of
{ "name", "settings", "ts" } objects, where settings is exactly
the object documented in The settings object below. Writing this key from a
script is how you push one house preset onto every machine you use.
Reads of a /data key are cached for roughly 90 seconds, so a read straight
after a write can return the previous value. That is why batch history uses a collection
instead.
curl -s -X PUT https://api.skillsafe.ai/v1/app-api/data/presets \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "X-App-Slug: pic-lite" \
-H "Content-Type: application/json" \
-d '{"value": [{"name": "Web upload", "settings": {"format": "webp", "quality": 78, "maxDim": 2000, "stripMeta": true}, "ts": 1786600000000}]}'
import requests
TOKEN = "YOUR_TOKEN" # from https://pic-lite.skillsafe.ai/tokens.html
H = {"Authorization": f"Bearer {TOKEN}", "X-App-Slug": "pic-lite"}
body = {
"value": [
{
"name": "Web upload",
"settings": {
"format": "webp",
"quality": 78,
"maxDim": 2000,
"stripMeta": true
},
"ts": 1786600000000
}
]
}
r = requests.put("https://api.skillsafe.ai/v1/app-api/data/presets", headers=H, json=body, timeout=30)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN"; // from https://pic-lite.skillsafe.ai/tokens.html
const H = { "Authorization": `Bearer ${TOKEN}`, "X-App-Slug": "pic-lite" };
const res = await fetch("https://api.skillsafe.ai/v1/app-api/data/presets", {
method: "PUT",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({"value": [{"name": "Web upload", "settings": {"format": "webp", "quality": 78, "maxDim": 2000, "stripMeta": true}, "ts": 1786600000000}]})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
body := []byte(`{"value": [{"name": "Web upload", "settings": {"format": "webp", "quality": 78, "maxDim": 2000, "stripMeta": true}, "ts": 1786600000000}]}`)
req, _ := http.NewRequest("PUT", "https://api.skillsafe.ai/v1/app-api/data/presets", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", "pic-lite")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] a) throws Exception {
String token = System.getenv("SKILLSAFE_TOKEN");
String body = """
{"value": [{"name": "Web upload", "settings": {"format": "webp", "quality": 78, "maxDim": 2000, "stripMeta": true}, "ts": 1786600000000}]}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/data/presets"))
.header("Authorization", "Bearer " + token)
.header("X-App-Slug", "pic-lite")
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
token = ENV.fetch("SKILLSAFE_TOKEN")
uri = URI("https://api.skillsafe.ai/v1/app-api/data/presets")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer #{token}"
req["X-App-Slug"] = "pic-lite"
req["Content-Type"] = "application/json"
req.body = {"value": [{"name": "Web upload", "settings": {"format": "webp", "quality": 78, "maxDim": 2000, "stripMeta": true}, "ts": 1786600000000}]}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = getenv("SKILLSAFE_TOKEN");
$headers = ["Authorization: Bearer $token", "X-App-Slug: pic-lite"];
$headers[] = "Content-Type: application/json";
$body = '{"value": [{"name": "Web upload", "settings": {"format": "webp", "quality": 78, "maxDim": 2000, "stripMeta": true}, "ts": 1786600000000}]}';
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/data/presets");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Add("X-App-Slug", "pic-lite");
var body = new StringContent(@"{""value"": [{""name"": ""Web upload"", ""settings"": {""format"": ""webp"", ""quality"": 78, ""maxDim"": 2000, ""stripMeta"": true}, ""ts"": 1786600000000}]}", System.Text.Encoding.UTF8, "application/json");
var res = await http.PutAsync("https://api.skillsafe.ai/v1/app-api/data/presets", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
5. Query your batch history
Each completed batch is written to the declared batches collection. Records
hold names, byte counts and outcomes only — never image data, not
even a thumbnail. The whole point of the app is that pixels do not leave the machine, and
the history feature does not make an exception.
Note two things that bite. Every where entry must be an
operator object: {"saved_pct": {"gte": 40}}, never
{"saved_pct": 40}. And the sort key is sort, an object —
order_by is silently ignored and you get created_at desc.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/batches/query \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "X-App-Slug: pic-lite" \
-H "Content-Type: application/json" \
-d '{"where": {"saved_pct": {"gte": 40}}, "sort": {"field": "ran_at", "dir": "desc"}, "limit": 20}'
import requests
TOKEN = "YOUR_TOKEN" # from https://pic-lite.skillsafe.ai/tokens.html
H = {"Authorization": f"Bearer {TOKEN}", "X-App-Slug": "pic-lite"}
body = {
"where": {
"saved_pct": {
"gte": 40
}
},
"sort": {
"field": "ran_at",
"dir": "desc"
},
"limit": 20
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/collections/batches/query", headers=H, json=body, timeout=30)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN"; // from https://pic-lite.skillsafe.ai/tokens.html
const H = { "Authorization": `Bearer ${TOKEN}`, "X-App-Slug": "pic-lite" };
const res = await fetch("https://api.skillsafe.ai/v1/app-api/collections/batches/query", {
method: "POST",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({"where": {"saved_pct": {"gte": 40}}, "sort": {"field": "ran_at", "dir": "desc"}, "limit": 20})
});
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
body := []byte(`{"where": {"saved_pct": {"gte": 40}}, "sort": {"field": "ran_at", "dir": "desc"}, "limit": 20}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/collections/batches/query", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", "pic-lite")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] a) throws Exception {
String token = System.getenv("SKILLSAFE_TOKEN");
String body = """
{"where": {"saved_pct": {"gte": 40}}, "sort": {"field": "ran_at", "dir": "desc"}, "limit": 20}""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/collections/batches/query"))
.header("Authorization", "Bearer " + token)
.header("X-App-Slug", "pic-lite")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
token = ENV.fetch("SKILLSAFE_TOKEN")
uri = URI("https://api.skillsafe.ai/v1/app-api/collections/batches/query")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["X-App-Slug"] = "pic-lite"
req["Content-Type"] = "application/json"
req.body = {"where": {"saved_pct": {"gte": 40}}, "sort": {"field": "ran_at", "dir": "desc"}, "limit": 20}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = getenv("SKILLSAFE_TOKEN");
$headers = ["Authorization: Bearer $token", "X-App-Slug: pic-lite"];
$headers[] = "Content-Type: application/json";
$body = '{"where": {"saved_pct": {"gte": 40}}, "sort": {"field": "ran_at", "dir": "desc"}, "limit": 20}';
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/collections/batches/query");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Add("X-App-Slug", "pic-lite");
var body = new StringContent(@"{""where"": {""saved_pct"": {""gte"": 40}}, ""sort"": {""field"": ""ran_at"", ""dir"": ""desc""}, ""limit"": 20}", System.Text.Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/collections/batches/query", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
}
}
6. Check your storage use
GET /storage reports what this app is holding against the per-app and
per-user quotas — useful if you have been writing batch records from a script.
curl -s https://api.skillsafe.ai/v1/app-api/storage \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "X-App-Slug: pic-lite"
import requests
TOKEN = "YOUR_TOKEN" # from https://pic-lite.skillsafe.ai/tokens.html
H = {"Authorization": f"Bearer {TOKEN}", "X-App-Slug": "pic-lite"}
r = requests.get("https://api.skillsafe.ai/v1/app-api/storage", headers=H, timeout=30)
r.raise_for_status()
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN"; // from https://pic-lite.skillsafe.ai/tokens.html
const H = { "Authorization": `Bearer ${TOKEN}`, "X-App-Slug": "pic-lite" };
const res = await fetch("https://api.skillsafe.ai/v1/app-api/storage", { headers: H });
const { data, error } = await res.json();
if (error) throw new Error(error.message);
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/storage", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", "pic-lite")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] a) throws Exception {
String token = System.getenv("SKILLSAFE_TOKEN");
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/storage"))
.header("Authorization", "Bearer " + token)
.header("X-App-Slug", "pic-lite")
.GET().build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
require "net/http"
require "json"
require "uri"
token = ENV.fetch("SKILLSAFE_TOKEN")
uri = URI("https://api.skillsafe.ai/v1/app-api/storage")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
req["X-App-Slug"] = "pic-lite"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = getenv("SKILLSAFE_TOKEN");
$headers = ["Authorization: Bearer $token", "X-App-Slug: pic-lite"];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/storage");
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$out = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($out["data"]);
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Add("X-App-Slug", "pic-lite");
var res = await http.GetStringAsync("https://api.skillsafe.ai/v1/app-api/storage");
Console.WriteLine(res);
}
}
The settings object
This is the exact shape the app stores in a preset and in a batch record, taken from compress.js. Every field is clamped on read, so an out-of-range value written by a script is corrected rather than honoured.
| Field | Type | Default | Meaning |
|---|---|---|---|
format | string | "keep" | keep, jpeg, png or webp. keep round-trips each file's own container. |
quality | number 1–100 | 78 | JPEG and WebP only. Ignored for PNG and GIF, which have no quality dial. |
scale | number 0.1–1000 | 100 | Percentage. The output is never smaller than 1×1 pixel. |
maxDim | number | 0 | Cap on the long side in pixels. 0 means no cap. Applied after scale. |
noUpscale | boolean | true | Clamps the result to the source dimensions whatever scale asked for. |
shrinkOnly | boolean | true | The anti-enlargement rule. Walks the quality or palette down, and returns the original file if nothing is smaller. |
stripMeta | boolean | true | Removes EXIF, GPS, XMP, ICC, IPTC and comments. Lossless on a file that is kept rather than re-encoded. |
pngColors | number 2–256 | 256 | Palette size for PNG output. |
pngDither | boolean | true | Floyd–Steinberg diffusion for PNG. Skipped automatically when quantisation is exact. |
gifColors | number 2–256 | 128 | Palette size per GIF frame. |
gifDither | boolean | true | Dithering can make a GIF larger — it breaks up the flat runs LZW depends on. |
matte | string #rrggbb | "#ffffff" | What transparency is flattened onto when writing JPEG. |
suffix | string | "-min" | Appended before the extension. Empty keeps the original name. |
watermark | object | off | { on, text, position, size, opacity, rotation, shadow, color }. position is one of top-left, top-right, bottom-left, bottom-right, center, tile. |
The batch record
What a batches query returns. Records nest their document under
doc — read record.doc.title, never record.title.
{
"record_id": "rec_...",
"doc": {
"uid": "1786600000000-a1b2c3",
"title": "14 files",
"file_count": 14,
"failed_count": 0,
"kept_count": 2,
"before_bytes": 18442110,
"after_bytes": 4120887,
"saved_pct": 77.66,
"format": "keep",
"ran_at": "2026-08-14T09:12:44.101Z",
"entry": {
"settings": { "...": "the settings object above" },
"files": [
{ "n": "harbour.jpg", "o": "harbour-min.jpg", "b": 415720, "a": 88214, "k": 0, "e": "" },
{ "n": "favicon-tile.png", "o": "favicon-tile.png", "b": 146, "a": 146, "k": 1, "e": "" }
]
}
}
}
In the per-file rows: n is the input name, o the output name,
b and a the byte counts before and after, k is
1 when the original was kept because nothing re-encoded smaller, and
e carries the failure reason when a file did not finish. If
entry.files_trimmed is present, the row list was shortened to fit the 64 KB
document cap and is not the complete batch.
Rate limits
- 120 requests per minute per IP on the data endpoints.
- Quotas that matter here: 10,000 records per collection, 1,000 records per owner, 64 KB per document.
- There is no run endpoint and no per-run cost, because there is no model behind this app.