curl --request GET \
--url http://localhost:3000/api/apps \
--header 'Origin: <api-key>'import requests
url = "http://localhost:3000/api/apps"
headers = {"Origin": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Origin: '<api-key>'}};
fetch('http://localhost:3000/api/apps', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "3000",
CURLOPT_URL => "http://localhost:3000/api/apps",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Origin: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "http://localhost:3000/api/apps"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Origin", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("http://localhost:3000/api/apps")
.header("Origin", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("http://localhost:3000/api/apps")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Get.new(url)
request["Origin"] = '<api-key>'
response = http.request(request)
puts response.read_body[
{
"id": 324684580,
"name": "Spotify - Music and Podcasts",
"url": "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580",
"changeCount": 2,
"bundleId": "com.spotify.client",
"developer": "Spotify Ltd.",
"privacyPolicyUrl": "<string>",
"firstSeen": 123
}
]{
"error": "Admin token required"
}List tracked apps
Without limit, returns the complete fleet as a bare JSON array —
the legacy form, kept stable for existing consumers. Its response
size grows linearly with the fleet (~0.7 KB per app), so for
installs tracking more than ~1,000 apps prefer the paginated form:
the presence of limit switches the response to an
{ apps, total, limit, offset } envelope. Pages are ordered by app
name (then id), so offset paging is deterministic across requests.
Pass devices to restrict any list form to apps on particular
devices. Without it the response always covers the whole library,
whatever device is picked in the app’s device menu.
curl --request GET \
--url http://localhost:3000/api/apps \
--header 'Origin: <api-key>'import requests
url = "http://localhost:3000/api/apps"
headers = {"Origin": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Origin: '<api-key>'}};
fetch('http://localhost:3000/api/apps', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "3000",
CURLOPT_URL => "http://localhost:3000/api/apps",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Origin: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "http://localhost:3000/api/apps"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Origin", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("http://localhost:3000/api/apps")
.header("Origin", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("http://localhost:3000/api/apps")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Get.new(url)
request["Origin"] = '<api-key>'
response = http.request(request)
puts response.read_body[
{
"id": 324684580,
"name": "Spotify - Music and Podcasts",
"url": "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580",
"changeCount": 2,
"bundleId": "com.spotify.client",
"developer": "Spotify Ltd.",
"privacyPolicyUrl": "<string>",
"firstSeen": 123
}
]{
"error": "Admin token required"
}Authorizations
Same-origin CSRF check. The Origin header must match the request host;
enforced for every mutating verb (POST, PUT, PATCH, DELETE).
Query Parameters
Page size (1–500). Supplying this parameter opts into the paginated envelope response; values outside the range return 400.
1 <= x <= 500Row offset into the name-ordered fleet. Only meaningful together
with limit. An offset past the end returns an empty page, not
an error.
x >= 0With meta=grid (and limit), the envelope gains a meta
object carrying the per-app side-band maps the apps grid
renders — profile badges, user verdicts, pending-change
breakdown, and device links — scoped to the returned page.
grid Returns the grouped privacy view used by dashboards.
grouped Restricts every list form — the bare array, the paginated
envelope, and view=grouped — to apps on the given devices: a
comma-separated list of device IDs, optionally including
unattached for apps linked to no device, or all. In the
envelope, total counts the restricted set and pages are drawn
from it, so offset paging stays consistent. Opt-in: omitting it
always returns the whole library. IDs that don't match a device
are ignored; if nothing matches, the whole library is returned.
See Devices.
Response
Bare array of apps (no limit), or a pagination envelope
(with limit).
- object[]
- object
Apple track ID extracted from /id<digits>/ in the App Store URL.
324684580
"Spotify - Music and Podcasts"
"https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580"
Unacknowledged change events. Drives the bell badge.
2
"com.spotify.client"
"Spotify Ltd."
Unix timestamp (ms) of first scrape.