Shop server API

Reference for running or integrating a shop backend compatible with CyberFoil. Works best with AeroFoil. Derived from shopInstall.cpp and save_sync.cpp.

See also: Network & download · Install pipeline

Connecting from CyberFoil

Set the shop URL in Settings or save a profile under sdmc:/switch/CyberFoil/shops/.

  • Default HTTP port: 8465 (HTTPS defaults to 443)
  • Example URL: http://192.168.1.2:8465
  • Auth: optional HTTP Basic (shopUser / shopPass in config, or per-profile in shops/*.json)
  • Trailing slashes are stripped; bare hostnames get http:// prepended

Shop profile file example:

{
  "shop": {
    "protocol": "http",
    "host": "192.168.1.2",
    "path": "",
    "port": 8465,
    "username": "user",
    "password": "pass",
    "title": "My LAN shop",
    "favourite": false
  }
}

How CyberFoil loads a catalog

  1. If shopLegacyMode is on → GET shop root only (Tinfoil-style).
  2. Otherwise → GET /api/shop/sections.
  3. On 404 or network failure → fallback to root GET (legacy JSON or TINFOIL binary).
  4. After load, optional MOTD from root JSON field success (skipped if body is TINFOIL).
  5. Search is client-side only - filters item names in the current section. No search HTTP API.

HTTP endpoints

All catalog requests are GET. File installs use GET on each item's url (with Basic auth forwarded).

MethodPathPurpose
GET/api/shop/sectionsModern catalog (preferred)
GET/Legacy catalog, MOTD, or TINFOIL payload
GET/api/shop/icon/{TITLEID}Icon - 16 hex digits, uppercase (e.g. 01007EF00011E000)
GET{item.url}Download NSP/NSZ/XCI/XCZ for install
GET/api/saves/listRemote save backups (non-legacy mode)
POST/api/saves/upload/{TITLEID}Upload save zip (multipart)
GET/api/saves/download/{TITLEID}.zipDownload latest save
GET/api/saves/download/{TITLEID}/{saveId}.zipDownload specific version
DELETE/api/saves/delete/{TITLEID}Delete save(s) for title
DELETE/api/saves/delete/{TITLEID}/{saveId}Delete one version

Shop fetch: 30s timeout, up to 4 retries on 408/429/5xx. SSL verify is disabled for shop requests.

Modern response: /api/shop/sections

Content-Type should be JSON. Error object:

{ "error": "Invalid credentials" }

Success shape:

{
  "sections": [
    {
      "id": "base",
      "title": "Games",
      "items": [
        {
          "name": "Example Game",
          "url": "/files/example.nsp",
          "size": 1234567890,
          "title_id": "01007EF00011E000",
          "app_version": 65536,
          "app_type": "base",
          "icon_url": "/api/shop/icon/01007EF00011E000",
          "release_date": 20240115
        }
      ]
    }
  ],
  "success": "Welcome - shown as message of the day"
}

Section id hints used client-side: all, updates/update, dlc, installed (local), saves/save.

Item fields

FieldAliasesNotes
url-Required. Relative paths resolved against shop base. URL fragment used as display name if name absent.
name-Display title
size-File size in bytes
title_id-16-char hex or decimal
app_version-Version number
app_type-base, upd/update/patch, dlc/addon, or 0/1/2
icon_urliconUrlIf omitted and title_id set → auto /api/shop/icon/{ID}
release_datereleaseDate, dateInteger YYYYMMDD

After parsing, CyberFoil may prompt to include related updates and DLC for selected bases.

Legacy / Tinfoil mode

Enable shopLegacyMode in Settings (Tinfoil Mode). Changes client behaviour:

  • Only GET shop root - no /api/shop/sections
  • User-Agent header is empty
  • Save sync API is disabled
  • Legacy headers report version 20.0.2

Legacy JSON at root

At least one of:

  • sections - same as modern API
  • files / paths - array of objects or map of "Display Name": "path_or_url"
  • directories - URLs fetched recursively for nested manifests

Per-entry URL keys: url, path, file, download_url, downloadUrl, or a plain string.

TINFOIL binary

Body starts with magic TINFOIL (optional BOM/whitespace prefix). May be AES-encrypted and/or zstd/zlib compressed. Decrypts to JSON. Encrypted shops without the required library blob are rejected with an error.

{
  "files": {
    "Game [01007EF00011E000][base]": "/nsp/game.nsp"
  }
}

Request headers

HTTP Basic

Standard Authorization: Basic … when username or password is configured.

User-Agent (non-legacy shop fetch)

httpUserAgentModeSent value
defaultcyberfoil
chrome / safari / firefoxBrowser preset string
tinfoil(empty)
customhttpUserAgent from config

Tinfoil legacy headers

When legacy auth support is available in the build, shop requests may also send:

  • Theme: 64 zero digits
  • UID: derived from console CID
  • Version: / Revision: from app version
  • Language: e.g. en, fr
  • HAUTH: HMAC-like token from shop URL
  • UAUTH: token from URL + credentials

401/403 or HTML login pages are treated as auth failures. Redirect to a URL containing /login fails login.

Save sync (/api/saves/*)

Requires non-legacy shop mode. Uses the same shop URL and Basic auth. Compatible with AeroFoil-style servers.

List - GET /api/saves/list

JSON array or { "saves": [ … ] }:

{
  "saves": [
    {
      "title_id": "01007EF00011E000",
      "name": "Example Game",
      "save_id": "abc123",
      "note": "Before final boss",
      "created_at": "2024-01-15T12:00:00Z",
      "created_ts": 1705312800,
      "size": 1048576,
      "download_url": "/api/saves/download/01007EF00011E000/abc123.zip"
    }
  ]
}

Field aliases: titleId, saveId, save_note, downloadUrl, etc.

Upload - POST /api/saves/upload/{TITLEID}

Multipart form:

  • file - zip archive
  • title_id / application_id - 16-char hex
  • note - required from UI for bulk backup

Expect HTTP 2xx on success.

Download / delete

Download via GET on constructed or provided download_url. DELETE endpoints remove by title or by save_id.

Related client settings

KeyDefaultEffect
shopHideInstalledtrueHide already-installed titles in shop UI
shopHideInstalledSectiontrueHide synthetic Installed section
shopAllBaseOnlytrueAll section shows bases only
shopLegacyModefalseTinfoil compatibility mode
shopStartGridModefalseOpen shop in grid view

Icon cache: sdmc:/switch/CyberFoil/shop_icons/

Minimal compatible server

  1. Listen on HTTP port 8465 (or HTTPS 443).
  2. Implement GET /api/shop/sections returning { "sections": [ … ] }.
  3. Serve files at URLs referenced in items; support relative paths.
  4. Optional: GET /api/shop/icon/{TITLEID}, root success MOTD string.
  5. For saves: implement /api/saves/list|upload|download|delete as above.
  6. For old Tinfoil shops: root JSON or TINFOIL binary + legacy headers if encrypted.