Files
simple/docs/api.md
T
catarrh e8190c65f7 v3.28.0 review: the batch failure classification
- A non-ValueError exception from the install run (a card removed
  mid-chain, a transport error) was recorded as a 'skipped' attempt with
  the error text - but the chain had started and died, which is a
  failure.  Only a ValueError (a request/eligibility error: the preset's
  ISD keyset, the plan) is a skip now; anything else is a 'failed' record
  carrying its error.  The PWA's failed row falls back to that error when
  there is no failing step / status word to show.
- The panel's Clear button is disabled while a batch is active (the
  server refuses the clear with 409 anyway) - no pointless error alert.
- api.md and the repo AGENTS document the two outcomes.  Tests: a
  transport error mid-chain is a failure (and the card stays seen), the
  plan's input validation (the block size, a missing cap_hex), the failed
  row without a step, the shared ram_format field, and the Clear state.
  915 frontend + 921 Python; sw cache simple-v438 (unchanged).
2026-10-14 15:09:33 +03:00

98 KiB
Raw Blame History

pysim-simple-server — HTTP API reference

The server exposes a JSON HTTP API under /api/*. All responses carry Access-Control-Allow-Origin: * (plus Access-Control-Allow-Private-Network: true on the preflight), so the API is reachable from a separately-hosted PWA.

Version compatibility

Server PWA (SIMple) Status
same major any ✅ Compatible
older major any ❌ Outdated — update server
newer major any ⚠️ Server newer — update PWA

The server reports its version via GET /api/version. The PWA checks this on connect and compares the major version (a 2.x server is flagged as outdated by a 3.x PWA).

Endpoints

Endpoint Method Purpose
/api/version GET Server version string
/api/status GET Card reader + card info + current selection
/api/command POST pySim command (equip, status, tree, etc.)
/api/commands GET List available pySim commands
/api/cardinfo GET pySim cardinfo output
/api/tree POST File tree browser for given FID/name
/api/select POST Select a file by name or FID
/api/read POST Read file content
/api/write POST Write raw hex data to a file
/api/apdu POST Raw APDU send
/api/verify-adm POST Verify the card's ADM PIN (from the matched card preset)
/api/presets GET Card preset store: all presets + the store path
/api/presets POST Create a card preset (server-side store)
/api/presets/update POST Update a preset ({id, fields}; the counter may be raised)
/api/presets/delete POST Delete a preset ({id})
/api/presets/import POST Import presets ({presets: [...], mode: merge|replace})
/api/presets/import-outfile POST Import a card vendor's outfile ({text, name?, algorithm16?, tars?, tar_keyset?, mode?}; without mode a dry-run preview)
/api/tar-probe POST Probe the card's registered OTA applications (one secured packet per TAR, TS 101 220 Annex D)
/api/counter-probe POST Bounded counter sync: probe increasing counter values (start+1, +2, +4, …) with a read-only command until the card accepts one (ceiling/attempt-capped), then persist accepted+1
/api/esim/chip GET eUICC chip details (EID, EUICCInfo1/2, configured addresses)
/api/esim/profiles GET Installed eSIM profiles with their metadata
/api/esim/notifications GET Pending eSIM notifications (read-only)
/api/esim/profile POST Enable/disable an eSIM profile (card re-initialized after the switch)
/api/help POST pySim help for a given command
/api/send-ota POST SCP80 OTA secured packet delivery
/api/ram-install POST Install a Java Card .cap file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install])
/api/ram-batch/start POST Start the batch CAP install (v3.28.0): the server installs the same .cap on every card that gets equipped, using each card's preset (matched by ICCID)
/api/ram-batch/status GET The batch state: phase, current card, counts and the attempts report (card-free)
/api/ram-batch/stop POST Stop the batch — a running install finishes first (card-free, so it works while the install holds the card lock)
/api/ram-batch/clear POST Drop the finished batch's report (409 while a batch is active)
/api/ram-install-app POST Single INSTALL [for install] / [for make selectable] for an already loaded package (no re-load)
/api/cap-info POST Validate a .cap archive and estimate its code/NVRAM/RAM requirements (read-only)
/api/cap-compat POST CAP compatibility test: LOAD only to the block completing the Import component (nothing committed)
/api/test/run POST Start a test script (actions + proactive expectations; script inline or script_id from the store) or a test suite (suite_id; ordered scripts, one card session)
/api/test/status GET Test script/suite run state, per-step results and the suite report
/api/test/stop POST Request a running test script/suite to stop
/api/test/clear POST Clear the finished run report
/api/test/scripts GET Test script store (server-side test_scripts.json): all scripts + the store path
/api/test/scripts POST Create a test script ({name, steps, suite_id} or {script}; the script joins the suite)
/api/test/scripts/update POST Update a test script ({id, script}; a changed suite_id is a move)
/api/test/scripts/delete POST Delete a test script ({id}; detaches it from its suite first)
/api/test/scripts/move POST Move a script to another suite ({id, suite_id})
/api/test/scripts/copy POST Copy a script into another suite ({id, suite_id, name?})
/api/test/scripts/import POST Import scripts ({scripts: [...], mode: merge|replace, suite_id?}; accepts the old localStorage export shape)
/api/test/suites GET Test suite store (server-side test_suites.json): all suites + the store path
/api/test/suites POST Create a suite ({name, require_adm?, start_reset?, scripts?} or {suite})
/api/test/suites/update POST Update a suite ({id, suite}; the member set changes through the script endpoints)
/api/test/suites/delete POST Delete a suite ({id}; refused while it holds scripts)
/api/test/suites/import POST Import a suite bundle ({suites, scripts, mode}; script references remapped)
/api/sp-verify POST Verify secured packet against pySim reference
/api/menu GET Current STK menu (title + items + active)
/api/menu-select POST ENVELOPE(Menu Selection) with item_id
/api/menu-respond POST TERMINAL RESPONSE for paused STK command
/api/stk-status GET STK session state (active/pending/type)
/api/events GET Event list from SET UP EVENT LIST
/api/event-send POST Send ENVELOPE(Event Download)
/api/net-sim POST Run a network-condition scenario (attach, service loss, roaming, churn, 2G, SMS, CB, AUTHENTICATE)
/api/net-state GET Cached network-state monitor (service, location, per-file state)
/api/net-state-refresh POST Re-read the monitored files (optional files list) and return the updated monitor state
/api/arr GET Cached EF.ARR copies (the FCP '8B' security attribute references): records per ARR file
/api/mcc-mnc GET Search the optional MCC/MNC operator list (?q=; ?random=1&exclude=)
/api/proactive-log GET Last 50 proactive commands
/api/status-poll POST Manual STATUS poll + FETCH if 91XX
/api/rescue POST Re-send TERMINAL PROFILE to recover CAT session
/api/terminal-profile GET Current TERMINAL PROFILE (hex) + CLI default
/api/terminal-profile POST Set and re-send the TERMINAL PROFILE at runtime (in-memory)
/api/poll-status GET Background STATUS polling state
/api/poll-toggle POST Enable/disable background polling
/api/pli-qualifiers GET List of qualifier codes with descriptions
/api/pli-dict GET Current dictionary (hex values per qualifier)
/api/pli-dict POST Update dictionary entries
/api/scp81/bip POST Start/stop the HTTP OTA listener (dump capture or PSK TLS server)
/api/scp81/status GET BIP terminal + listener state (channels, PSK identities, handshake identity, owner)
/api/scp81/log GET HTTP OTA event log (?after=<seq>)
/api/scp81/log-clear POST Clear the HTTP OTA event log
/api/bip/control POST Start/stop the generic BIP session (sink / passthru / redirect)
/api/bip/status GET BIP session state + owner (same shape as /api/scp81/status)
/api/bip/log GET BIP event log (?after=<seq>)
/api/bip/log-clear POST Clear the BIP event log
/api/scp81/queue POST Replace the SCP81 command script (optionally force-restart)
/api/scp81/script GET Active command script + execution state and R-APDUs
/api/scp81/psk-map POST Replace the PSK table of a running TLS listener
/api/scp81/gen-install POST Generate the RAM APDU list for a .cap (no queueing)

Endpoint details

GET /api/version

Returns server version for compatibility checking.

Example response:

{"version": "3.0.1"}

GET /api/status

Card reader, card type, current selection, and card state. Reported fields: reader, connected, card_present, card_session (increments on every equip/disconnect), proactive_seq, equipping, auto_equip, card, profile, eid, euicc, iccid, app_ready, adm_verified, atr, cla_byte, sel_ctrl, current_selection, channels.

eid is the eUICC identifier read from the ISD-R at equip (null for a non-eUICC card) and euicc says whether the equipped card is an eUICC (SGP.22/32); both are used by the PWA's eSIM view.

iccid is the E.118 digit string read from EF.ICCID (MF/2FE2) when the card is equipped, or null when it is not connected / the card does not let the terminal read it. The equip path reads it before the TERMINAL PROFILE, so no CAT session is active yet; the read is best-effort and never fails an equip (and a card removal clears it). The PWA uses it to auto-select the card preset with the same ICCID in both SCP80 views (Secured Packet and RAM).

GET /api/commands

List all available shell commands for the current card profile.

GET /api/cardinfo

Runs pySim's cardinfo command and returns its output as {"output": "..."} (card type, ATR, ICCID and other information pySim reports for the equipped card). A shortcut for POST /api/command with {"cmd": "cardinfo"}.

GET /api/mcc-mnc

Searches the optional worldwide operator list loaded from --mcc-mnc-list (default <workspace>/samples/mcc-mnc-list.json). ?q=<text> matches country, country code, MCC/MNC, brand and operator (compact results, max 50); ?random=1[&exclude=MCCMNC] returns one random entry (for roaming tests). When the list is not configured or missing the response is {"available": false}.

POST /api/command

Execute any pysim-shell command.

{"cmd": "select MF"}

Returns:

{"output": "..."}

POST /api/apdu

Send a raw APDU to the card.

{"apdu": "00A4040000..."}

Returns:

{"response": "...", "sw": "9000"}

POST /api/verify-adm

Verify the card's ADM PIN (TS 102 221 VERIFY, CHV number from the card model). The key comes from the PWA's matched card preset and is never stored; it is redacted from the request log. Short keys (4-16 hex digits) are padded to the 8 CHV bytes with f, like pySim's verify_adm. The response is structured so the UI can warn about the remaining attempts before retrying.

{"adm": "0011223344556677"}

Returns on success:

{"ok": true, "sw": "9000"}

On a wrong key (63Cx, x attempts left):

{"ok": false, "sw": "63C2", "attempts_left": 2}

A blocked ADM (6983/9804) reports {"ok": false, "sw": "9804", "blocked": true} and cannot be recovered without the card's unblock key.

Card presets (server-side store)

Card presets (keysets, a TAR table with per-TAR MSLs, PSK pair, ADM) live on the server in ~/.pysim-simple-server/card_presets.json (--card-presets PATH overrides it; Path.home() resolves the same way on Linux, macOS and Windows). The file is written atomically and every counter change is appended to card_presets.json.audit.jsonl.

Keysets. A preset carries several keysets — a keyset's number is the b8..b5 nibble of its KIc/KID (TS 102 225 §5.1.2/A.2); the number is implicit in the bytes, there is no separate field:

{"id": "…", "name": "Card A", "iccid": "…",
 "pin1": "", "puk1": "", "pin2": "", "puk2": "",
 "pskIdentity": "", "pskKey": "", "adm": "",
 "tars": [
   {"role": "isd",     "tar": "000000", "msl": "16", "desc": ""},
   {"role": "uiccRfm", "tar": "B00000", "msl": "16", "desc": ""},
   {"role": "usimRfm", "tar": "B00001", "msl": "16", "desc": ""},
   {"tar": "AF4D01",   "msl": "1A", "desc": "my applet"}
 ],
 "keysets": [
   {"kic": "15", "kid": "15", "kicKey": "…", "kidKey": "…", "cntr": "0000000001"},
   {"kic": "29", "kid": "29", "kicKey": "…", "kidKey": "…", "cntr": "0000000005"}
 ]}

Rules (all refused with 400): KIc and KID must carry the same keyset number (A.2 — the card rejects a mismatch with "Unidentified security error"), the number must be 01–0F (00 means "no security" and is a packet-level choice, not a preset keyset), no two keysets may share a number, both keys and a counter (1–10 hex digits) are required, and the counters are stored in the fixed-width 10-hex form. A v3.8.0 flat preset (kic/kid/kicKey/kidKey/ cntr at the top level) converts into a single keyset on load and on import, so old files and exports keep working.

TAR table. Every Remote Management application is addressed by its TAR and each entry carries its own MSL (Minimum SPI1, TS 102 226 §8.2.1.3.2.4): the card checks it before the security processing and answers response status 0A "Insufficient security level" when a packet's SPI1 is below it. The three role entries (isd, uiccRfm, usimRfm — GP ISD 000000, TS 31.116 UICC RFM B00000, TS 31.115 ADF RFM B00001) are mandatory with a non-empty TAR and MSL; further TARs can be added with an optional desc. A TAR is three hex bytes, an MSL one, and a TAR may appear once (a duplicate is refused). A v3.9.1 preset's card-wide spi1/tar/uiccTar/usimTar convert into the three role entries, the spi1 as their MSL. Operations derive a packet's SPI1 from the MSL of its TAR; a request naming a TAR outside the preset must pass spi1 itself, otherwise it is refused with 400 and a hint.

Card codes. The optional pin1/puk1/pin2/puk2/adm fields are stored for later use (nothing consumes them yet). PIN/PUK must be 4–8 decimal digits when set (empty is fine; anything else is refused with 400 naming the field); the ADM key is 4–16 hex digits (a decimal PIN is fine).

CAT (USAT) release. The optional release field (4–18) names the release the card implements; the release guard hides CAT items introduced after it (the Simulator's event parameters, see catrelease). Empty means unspecified and resolves to Rel-12 (catrelease.DEFAULT_RELEASE) — an unset release never filters anything away.

Counters. The store is the source of truth for the SCP80 counters, one per keyset ("a dedicated counter shall be associated to each key version", Annex A.1 — the spec's wording). The operations below accept a preset_id and persist the counter they consumed themselves (monotonic per keyset — a stale value never regresses it), so a closed tab, a lost response or a second browser window can no longer lose an increment. A request whose KIc/KID name a non-zero key number the preset does not define is refused (400 {"error": "keyset N is not defined in preset '…' - add it in the Cards tab"}), and so is a KIc/KID number mismatch. The Cards tab's export/import uses this API; the import also accepts presets exported by the older localStorage-based builds.

  • GET /api/presets — {"path": "…", "count": 2, "version": 5, "presets": [{…}]} (card-free: it answers while a long card operation runs).
  • POST /api/presets — body = the preset fields (keysets included); returns {"ok": true, "preset": {…}}. A duplicate ICCID (digits, spaced digits, digits with the odd-nibble F pad, or raw EF hex — all normalised to one form) is refused with 400 {"error": "card with this ICCID already exists: …"}.
  • POST /api/presets/update — {"id": "…", "fields": {…}} (partial update; a plain {id, name, …} body works too, keysets replaces the list and tars the TAR table — a non-list tars value is refused). A counter is written as given — this is the deliberate human edit; unknown ids answer 404.
  • POST /api/presets/delete — {"id": "…"} → {"ok": true, "removed": true}.
  • POST /api/presets/import — {"presets": […], "mode": "merge"} (or "replace") → {"ok": true, "added": N, "skipped": M, "errors": […]}; invalid entries and duplicates are skipped and reported, never fatal.

POST /api/presets/import-outfile

Import many cards at once from a card vendor's outfile (v3.27.0). An outfile is the vendor personalization tool's plain-text dump: a banner and a configuration header (customer, batch, profile, …) followed by the actual data. The data starts at a column-name line — whitespace-separated tokens that include ICCID (the first token may carry a var_out:-style prefix) — and continues with one whitespace-separated row per card; everything before the column line and every column the presets do not use is ignored.

Without a mode the call is a dry run: the parsed presets and the report come back for the preview (nothing is stored). With mode: "merge" / "replace" the same parse runs and the presets are imported through the store (replace wipes the store first, like /api/presets/import); any other mode value is refused with 400 — a typo must not silently turn an import into a preview.

{"text": "…", "name": "Batch 0002", "algorithm16": "3des2",
 "tars": [{"role": "isd", "tar": "000000", "msl": "16", "desc": ""}, …],
 "tar_keyset": 1, "mode": "merge"}
  • text (required) — the outfile contents. A file without a column line answers {"ok": true, "preview": true, "presets": [], "report": {"error": "no column-name line found - …"}} (a preview, not a 400: the panel shows the message).
  • name — one common name applied to every imported card; empty falls back to the card's ICCID.
  • algorithm16 — the algorithm nibble for a 16-byte key: "3des2" (default, KIc/KID nibble 5) or "aes" (nibble 2); any other value is refused with 400 (a misspelled algorithm must never silently pick one — the nibble decides whether the card accepts the packet). The key size alone decides the others: 8 bytes → DES (1), 24 bytes → 3DES3 (9); any other size skips that keyset with a report line.
  • tars — the TAR table every imported card gets (defaults to the three role entries with their spec TARs and MSL 16). A malformed table (a non-3-byte TAR, a non-1-byte MSL, a duplicate) is refused with 400.
  • tar_keyset — one global default keyset (1–15) written into every TAR entry of every imported card. Without it the card's first keyset is used (a multi-keyset card would otherwise fail the store's "a non-zero MSL needs a default keyset" rule).

Recognised columns (case-insensitive): ICCID (the digits + odd-nibble F pad, the plain digits, spaced digits or the raw nibble-swapped EF hex all normalise to the same value), PIN1/PIN2, PUK1/PUK2 (4–8 digits; an invalid value is dropped and reported, the card is kept), ADM (the ASCII string, hex-encoded — 3632333239463337 → 62329F37), PSK (the SCP81 key, exactly 32 hex chars) and KICn/KIDn — one keyset per numbered pair, up to 15. The KIc/KID bytes are never in the file: the keyset number is the column number (KIC2 → keyset 2) and the algorithm nibble follows the key size, both bytes carrying the same number (TS 102 225 §A.2). A PSKID / PSK_ID column names the PSK identity; without one the card's ICCID is used (the identity the card sends in the ClientKeyExchange is ICCID-like). IMSI*, EKI and KIKn (the key-management key, unused) are ignored.

The response carries the parsed presets (a dry run) or the import result (added/skipped/errors) plus the report:

{"ok": true, "preview": false, "added": 5, "skipped": 0, "errors": [],
 "report": {"columns": ["IMSI", "ICCID", "KIC1", …], "header_lines": 11,
            "rows": 5, "cards": 5, "keysets": ["KIC1/KID1", …], "tars": 3,
            "skipped": ["line 4: keyset 2 has an unsupported key size (10 bytes)"],
            "skipped_more": 0}}

POST /api/counter-probe

Sync a preset's SCP80 counter with the card. The card accepts only a counter above its own (TS 102 225 §5.1.1, SPI1 b5b4 = 10) and updates it only when the packet is accepted, so rejected attempts change nothing; the probe walks a doubling ladder (start+1, +2, +4, …) with a fixed read-only command until one is accepted, then persists accepted+1 (monotonic) into the keyset. The search stops at a ceiling and after a bounded number of packets, so it can never run towards the 40-bit maximum, where the card blocks the counter. The card's own counter is never reported by the card (the PoR's CNTR is a copy of the packet's, TS 102 225 §5.2 Table 3), which is why this probe exists.

{"preset_id": "…", "kvn": 2, "tar": "000000", "spi1": "16", "spi2": "21",
 "cntr": "0001000000", "ceiling": "FFFFFFFF", "max_attempts": 40}

All fields except preset_id are optional: kvn picks a keyset (the first otherwise), tar defaults to the preset's ISD entry and spi1 to that TAR's MSL, spi2 to the RAM listing transport 21, cntr to the keyset's counter, ceiling to FFFFFFFF (must stay below FFFFFFFFFF) and max_attempts to 40 (clamped to 1–64). The probe needs the PoR verdict, so bit b1 of spi2 is always set (a caller's 00 becomes 01; the transport bits are kept) - without a PoR the probe is blind. The probe always sends the read-only SELECT MF - the counter advances on the packet's security acceptance whatever the applet answers (6A86/6D00 are normal there). A SPI1 without a counter check is refused (400).

The ladder only advances on a cntr_low verdict (the card's "your counter is not above mine"); any other verdict stops the search. A stop on a non-por_ok verdict (rc_cc_ds_failed, tar_unknown, …) after at least one cntr_low keeps the counter value anyway - the earlier rejection proves the value is above the card's counter - but the response reports success: false, counter_saved: true, the verdict and the saved counter, stating that something is likely wrong with the security settings. Without a prior cntr_low nothing is saved (the value is not established), and a missing PoR is an error too.

Response: {"success": true, "accepted_cntr": "0001000000", "stored_cntr": "0001000001", "packets": 3, "stopped": "accepted", "attempts": [{"cntr": "…", "por_status": "cntr_low"}, …], "start": "…", "ceiling": "FFFFFFFF", "kvn": 2}

stopped is accepted, ceiling, attempts or error; on a non-cntr_low failure (refused ENVELOPE, wrong TAR, security error) the probe stops at the first attempt instead of hammering the card.

POST /api/tar-probe

Probe which OTA applications the card has registered: one secured packet per TAR of the named preset (v3.26.0 — the probe checks the preset's TAR list only; a tars list may select a subset, and a TAR outside the preset is refused with 400), each carrying a harmless C-APDU (SELECT MF by default). Each row uses its own MSL (the request's spi1 is only a fallback for a TAR the preset does not carry) and its own default keyset — the keyset resolved from that TAR's entry, so the request does not carry KIc/KID/keys and the probe's keyset selector is gone. A TAR whose MSL requires security (non-zero) without a default keyset, or one naming an undefined keyset, is refused with a hint before any packet is sent. The resolved SPI1 must carry a counter check (b5b4 ≠ 00) and ask for a PoR. Every accepted packet consumes a counter, which is persisted into that row's keyset like any other operation (each keyset has its own counter, TS 102 225 A.1); an explicit cntr seeds the first row only. The delivered SMS carries the OTA CPI IE by default (includeCpi, as in /api/send-ota).

{"preset_id": "…", "spi1": "16", "apdu": "00A40000023F00",
 "tars": ["000000", "B00000"]}

Response: {"success": true, "registered": N, "total": M, "final_cntr": "…", "kvn": 2, "spi1": "16", "spi2": "01", "results": [{"tar": "000000", "label": "ISD", "spi1": "16", "kvn": 2, "verdict": "registered", "sw": "9000", "por_status": "por_ok", "por_sw": "6D00", "por_data": ""}], "steps": [...]}

Verdicts: registered (ENVELOPE 9000 + a PoR carrying the application's own status word/data), no_answer (accepted, PoR without an R-APDU), no_por (accepted, no PoR at all — the packet was silently dropped), refused (a non-9000 ENVELOPE, typically 6200 for a TAR without a registered application). Observed on a live card: 000000, B00000 and B00001 registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused.

eSIM / LPA (local ES10 operations)

These endpoints work only when the equipped card is an eUICC (SGP.22/32); otherwise they answer 400 with {"error": "The equipped card is not an eUICC"}. They use pySim's ES10 static API — no lpac, no SM-DP+ contact, no profile downloads and no notification processing. All run under the card lock; GET /api/status reports euicc and eid for the PWA.

  • GET /api/esim/chip — {"eid": "8908…", "info1": {…}, "info2": {…}, "addresses": {"default_dp_address": …, "root_ds_address": …}, "rat": [{…}], "errors": {"<part>": "<reason>"}} (a part the card does not support is reported in errors instead of failing the whole request). The EUICCInfo TLVs are requested raw and decoded per SGP.22 v2.6 §5.7.8 (pySim's classes are incomplete), rat comes from the ES10b GetRat command (§5.7.13) and undecoded TLVs are preserved in a raw_tlvs map:

    {"eid": "89086030202200000026000024920451",
     "info1": {"svn": "2.2.2",
               "euicc_ci_pki_list_for_verification": ["8137…FB"],
               "euicc_ci_pki_list_for_signing": ["8137…FB"]},
     "info2": {"profile_version": "2.3.1", "svn": "2.2.2",
               "euicc_firmware_ver": "4.2.0",
               "ext_card_resource": {"installed_application": 0,
                                     "free_non_volatile_memory": 439084,
                                     "free_volatile_memory": 9798},
               "uicc_capability": ["usimSupport", "isimSupport", "…"],
               "ts102241_version": "9.2.0",
               "globalplatform_version": "2.3.0",
               "rsp_capability": ["additionalProfile", "testProfileSupport"],
               "euicc_category": "other",
               "forbidden_profile_policy_rules": ["ppr1"],
               "pp_version": "1.0.0",
               "ss_acreditation_number": "ED-ZI-UP-0826"},
     "addresses": {"default_dp_address": null,
                   "root_ds_address": "testrootsmds.gsma.com"},
     "rat": [{"ppr_ids": ["ppr1", "ppr2"],
              "allowed_operators": [{"plmn": "EEEEEE", "gid1": null, "gid2": null}],
              "ppr_flags": ["consentRequired"]}],
     "errors": {}}
    

    uicc_capability, rsp_capability, forbidden_profile_policy_rules and ppr_flags are ASN.1 BIT STRINGs decoded to the names of the set bits (the first content octet is the unused-bit count, bits are MSB-first); euicc_category accepts both the implicit (0x8B) and explicit (0xAB) tag encodings; pp_version (0x04) and ss_acreditation_number (0x0C) are the bare, untagged SGP.22 types; tre_properties, tre_product_reference, additional_euicc_profile_package_versions and certification_data_object are decoded when a card sends them.

  • GET /api/esim/profiles — {"profiles": [{"iccid": "8970…", "isdp_aid": "A000…", "state": "enabled"|"disabled", "nickname": …, "provider": …, "name": …, "class": "test"|"provisioning"|"operational", "owner": "250-99", "icon_type": "png"|"jpg", "icon": "89504E47…", "icon_size": 1234}], "error": null}. icon is the SGP.22 icon data (tag 0x94) as hex and icon_size its byte count, so a client can show the profile image (data:image/<type>;base64,…); both are null when the card sent no icon.

  • GET /api/esim/notifications — {"notifications": [{"seq_number": 3, "operations": ["enable"], "address": "smdp.example.org", "iccid": "8970…"}], "error": null}. operations are the decoded ProfileMgmtOperation flags (install, enable, disable, delete, in that order) — one or more per notification.

  • POST /api/esim/profile — {"action": "enable"|"disable", "iccid"?: …, "isdp_aid"?: …, "refresh"?: true} (one identifier required). With the refresh flag set the ISD-R returns OK before the REFRESH (SGP.22 v2.6 §5.7.16/§5.7.17 step 6) and the switch completes upon the TERMINAL RESPONSE or the following RESET (step 8): a 91xx answer is that OK, so the server answers the REFRESH proactive command (logged in /api/proactive-log), never retries the STORE DATA (the mid-switch card answers 6985) and re-initializes the card like an equip (physical reset, re-read ICCID/network state, new card_session). It then re-reads the profile list and reports the verified result:

    {"ok": true, "result": "ok", "refresh_seen": true, "reinitialized": true,
     "verified": true, "state_after": "disabled", "iccid": "8970…",
     "card_session": 7}
    

    verified is false when the target profile is not in the requested state after the re-init (null when the state could not be read). Failures carry the ES10c result code (iccidOrAidNotFound, profileNotInDisabledState, profileNotInEnabledState, disallowedByPolicy, wrongProfileReenabling, catBusy, undefinedError) and a short message; an unexpected status word is returned as {"ok": false, "result": "undefinedError", "sw": "6985", "message": "SW 6985"}.

POST /api/help

Get structured help for a shell command.

{"cmd": "apdu"}

Returns:

{"usage": "apdu [-h] [--expect-sw EXPECT_SW] [--raw] APDU", "description": "...", "args": [{"name": "APDU", "type": "positional", "help": "..."}]}

POST /api/send-ota

Send an OTA command (SCP80) to the card via SMS-PP-DOWNLOAD ENVELOPE (one ENVELOPE per SMS). With apdu the secured packet is built server-side (no single-SMS limit); a packet that does not fit one SMS is sent as a concatenated download per TS 31.115 §4.3: the packet is split into SMS user-data parts (first SM 132 octets, following ones 134 — the first one additionally carries the concatenation and CPI IEs) and the segments are sent in order. A packet that would need more than 5 segments is refused (the card's concatenation buffer is the limit). With sp a pre-built packet is delivered the same way.

SMS framing — includeCpi. By default (true) the delivered SMS TPDUs carry the OTA CPI information element (IEI 70, null IED — TS 31.115 §4.2/§4.3), the marker of a command packet; it shrinks the SMS user data — a single SM takes up to 137 packet octets instead of 140, and the first concatenated segment 132 instead of 134. The RAM/ISD operations send with includeCpi: true — a concatenated command carries the CPI IE in its first SM (TS 31.115 §4.3), and those are the packets that span several SMs. With false no CPI IE is added (a single SM then carries no UDH at all and the full 140-octet budget is available) — for a send whose receiver does not expect the marker.

SPI1 and the TAR's MSL. With a preset_id the packet's SPI1 is derived from the MSL of its tar (TS 102 226 §8.2.1.3.2.4) unless the request passes spi1 — an explicit value wins (a hand send may deliberately ask for more security than the minimum), and a value below the TAR's MSL is reported as "warning" in the response (numeric compare). A TAR the preset does not carry, without an explicit spi1, is refused with 400 and a hint.

The card may answer with PoR status actual_response_sms_submit (0x0B): the real response (a big GET STATUS listing, for example) then arrives as one or more proactive SEND SHORT MESSAGE commands. The server captures those SMS-SUBMIT TPDUs, reassembles the concatenated segments and returns the decoded response in por (as if it had arrived in the ENVELOPE), so callers see a normal por.response_status == "por_ok" with the remote status word and response data. Responses wrapped in the TS 102 226 5.2.2 Response Scripting template (AB/AF: executed-count TLV 80 + R-APDU TLV 23) are decoded the same way (response_type: "scripting"), with the R-APDU's own status word and data.

With ram_format the server handles the two RAM command formats of TS 102 226 §5.2.1: "auto" (the default when the key is present) sends a read-only GET STATUS [ISD] probe first — compact, then wrapped as AA (Command TLV 22) — and sends the command in the format the card answered; "compact" / "expanded" pin it. The response reports the detected ram_format and final_cntr (the counter to use next — accepted probe packets consume counters). Without the key the command data is sent byte-exact: applet-directed payloads (HTTP OTA triggers, §9 push, expanded scripts) must not be wrapped.

final_cntr is reported for the plain sp path too (a pre-built packet): the counter that was sent, advanced by one when the card accepted the packet.

cntr is optional (v3.20.0) — the preset store is the source of truth:

  • with apdu (the server builds the packet) a missing cntr is seeded from the named preset's keyset (the one the packet's KIc/KID resolve to); the effective value is echoed back as cntr, so a caller that omits it learns what was used. The seed uses the packet's keyset whatever SPI1.b5b4 says — with no counter check the CNTR field is spec-ignored, nothing is advanced or persisted, and the OTA SEND: counter not tracked line names it;
  • an explicit cntr always wins — the packet uses it verbatim (a hand send, the counter probe or a pre-built sp may deliberately pick one);
  • the store only moves forward: an explicit value (or its next counter) below the stored one is used for the packet but never lowers the store; a higher accepted value is persisted;
  • the sp (pre-built packet) path never seeds — the packet carries its own counter — so pass cntr with it to get final_cntr / persistence. A request with neither simply reports no final_cntr — it never fails the send.

With preset_id the server persists the counter into the keyset the packet used (see Card presets), so a lost response or a closed tab cannot lose the increment; the request is refused when KIc/KID carry different keyset numbers (TS 102 225 A.2) or a non-zero keyset number the preset does not define.

Counter tracking follows SPI1.b5b4 (TS 102 225 §5.1.1): with 00 the counter field is "present, ignored, never updated", so a packet whose SPI1 has no counter check (the keyless SPI1 00 form) reports no final_cntr and persists nothing — a keyless send must leave every preset counter untouched. The spec's no-security KIc/KID values 00/00 are accepted when the SPI says no ciphering / no RC-CC-DS (A.2), so {apdu, spi1: "00", kic: "00", kid: "00"} builds a keyless packet (the card accepted exactly that form; a fake 15/15 was refused with 6200). When a PoR was requested and the card did not answer, the response carries por_missing: true.

Request body:

{
  "sp": "00201516011515b00000...",
  "spi1": "16",
  "spi2": "01",
  "kic": "15",
  "kid": "15",
  "tar": "b00000",
  "cntr": "0000000001",
  "kicKey": "D6FCC023...",
  "kidKey": "1B07E7E0...",
  "preset_id": "7ee0367f3a444b28988fea5fbc50de9f",
  "ram_format": "auto",
  "includeCpi": true
}

Response (delivery PoR):

{"success": true, "sw": "9000", "response_data": "027100000e0a...",
 "bytes": 36, "segments": 1,
 "ram_format": "compact", "final_cntr": "0000000002",
 "por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
         "decoded": {"number_of_commands": 1, "last_status_word": "6e00",
                      "last_response_data": ""}}}

bytes is the secured packet size and segments the number of SMS segments sent (1 = single SMS, >1 = concatenated download; the failure response carries them too, plus an error).

Response (submit PoR): PoR is extracted from the SMS-SUBMIT TPDU fetched via a proactive command (FETCH). The response contains the same por structure if decoding succeeds.

The response form follows the command's TAR (v3.21.0): a remote-management TAR (the preset's role entries and the standard ISD/RFM allocations of TS 101 220 Annex D) is decoded as an RM response; any other TAR's secured data is the receiving application's own (TS 102 226 4) and is reported with por.response_type raw - the whole secured data in decoded.last_response_data and no status word (secured_data carries the response bytes in every case). The same raw classification covers a secured data that cannot be the compact structure (its command count exceeds the command script that was sent, or it is too short for Table 5.1), while a genuinely malformed packet or a failed RC/CC/DS check still yields no por.

The SPI2 por_in_submit bit (0x20) selects submit-mode PoR.

POST /api/cap-info

Validate a Java Card .cap archive and estimate its memory requirements. Read-only: it runs the same structural parse as the install paths (so a corrupt or wrong-format file fails here first) plus the bundled CAP analyzer (pysim_simple_server/capmem.py — component parsers and a method-bytecode allocation scan). It never touches the card, the SCP81 listener or the scripts; the install endpoints stay self-sufficient and the estimate is not used for installation.

{"cap_hex": "504B0304..."}

Response (ok):

{
  "ok": true,
  "load_file_aid": "AA1902BC226001", "module_aid": "AA1902BC226001",
  "load_file_bytes": 1234,
  "memory": {
    "package_aid": "AA1902BC226001",
    "applet_count": 1, "applets": ["AA1902BC226001"],
    "imports": [{"aid": "A0000000620101", "minor": 0, "major": 1, "refs": 6},
                {"aid": "A0000000090005FFFFFFFF8912000000", "minor": 11, "major": 1, "refs": 7}],
    "flags": {"raw": 4, "int": false, "export": false, "applet": true},
    "package_name": null,
    "components": [{"name": "Header", "size": 20}, {"name": "Method", "size": 433}],
    "class_count": 3, "method_count": 12,
    "code": {"method_component": 850, "load_file": 1234},
    "nvram": {"static_image": 12, "array_init": 4, "install_objects": 100,
              "header_overhead": 24, "ref_storage": 8, "total": 148, "runtime": 0,
              "requirement": 1382},
    "ram": {"transient_arrays": 16, "runtime_transient": 0, "peak_frame": 8, "total": 24},
    "suggested": {"c6": 850, "c7": 272, "c8": 148},
    "warnings": []
  }
}

imports are the libraries the CAP is linked against (JC VM spec §6.6, the Import component), each with the export-file version recorded in the CAP and the number of distinct constant-pool references to it (§6.7; a linked but unreferenced package is 0). A card resolves an import only when the resident package has the same major version and a minor ≥ the recorded one (§4.5.2), hence the name >= version display. flags decodes the Header package flags (Table 6-4: 0x01 uses int, 0x02 exports an API, 0x04 applet package); package_name is only present in JC 2.2+ headers (CAP 2.1 files have none); components lists the archive entries in load-file order (each size includes the component's tag and size header, so the sizes sum to load_file_bytes).

code.load_file is the concatenated load file (all components) - the package image the card stores, our proxy for the GlobalPlatform Card Spec v2.3.1 Table 11-48 "non-volatile code" minimum memory requirement; the tool's bytecode-only figure stays in code.method_component. The suggested C6 follows the load file, and nvram.requirement = code.load_file + nvram.total (the C6+C8-style total per §11.5.2.3.7: with no code/data split in the card's memory the required minimum is the sum of both). The estimate excludes card-specific memory management, allocation rounding and the GP registry entry.

Errors (HTTP 200 with ok: false, like /api/scp81/gen-install): {"ok": false, "error": "cap parse failed: File is not a zip file"} for a corrupt/wrong archive, or cap analysis failed: … when a component passes the structural parse but not the analyzer. The numbers are an estimate: the model assumes 2-byte references, a 6-byte object header and NVM cell rounding, and does not include applet-created runtime objects/arrays.

POST /api/test/run

Runs a test script: an ordered list of actions and proactive-command expectations, executed server-side on the equipped card. While a run is active the card is owned by the script - other card endpoints answer 409 {"error": "test script running ..."} and background STATUS polling is suspended; only /api/test/*, /api/status, /api/poll-status, /api/version and static files stay available.

{"script": {"name": "STK menu browsing", "steps": [
   {"type": "action", "kind": "menu-select", "params": {"item_id": 128},
    "check": {"sw": "91??"}},
   {"type": "expect", "command": "SELECT ITEM",
    "checks": [{"kind": "item", "id": 1, "text": "test"}],
    "respond": {"result": "ok", "item_id": 1}},
   {"type": "expect", "command": "DISPLAY TEXT",
    "checks": [{"kind": "text", "value": "hello"}],
    "respond": {"result": "ok"}}
 ]},
 "preset": {"name": "lab card",
            "keysets": [{"kic": "15", "kid": "15", "kicKey": "...", "kidKey": "...",
                         "cntr": "0000000A"}],
            "tars": [{"role": "isd", "tar": "000000", "msl": "16"},
                     {"role": "uiccRfm", "tar": "B00000", "msl": "16"},
                     {"role": "usimRfm", "tar": "B00001", "msl": "16"}]}}

The PWA sends the current script inline (a run always uses what the editor shows) plus script_id when the script is stored; sending script_id alone resolves the script from the store. The PWA sends preset_id (the stored preset's id) instead of preset; the server resolves it from the preset store, so a run always uses the stored keysets, TARs and counters. The inline preset form stays for external callers.

Action steps (type: "action"): kind is envelope (event, data, optional src - the device-identities source override), event (event name or hex + fields for the four server-built events 0x03/0x0B/0x12/0x1D, or the PWA form's built data hex for any other event - validated as hex and against the one-ENVELOPE budget at load; optional src, the device-identities override), menu-select (item_id 1-255, or text + mode exact/contains (case-insensitive by default; case_sensitive: true forces an exact-case match) - the text is resolved against the card's cached SET UP MENU, which is refreshed whenever the card sends the menu), file-write (path, data, mode auto/binary/record, record), file-read (same, verifies check.data), apdu (raw transport, no auto-handler), scp80 (apdu or sp, selected by the optional source field when both are present; optional kvn (keyset number 1-15), tar/spi1/spi2 overrides and format (compact default / expanded / expanded-ae - the TS 102 226 5.2.1 Command Scripting template around the C-APDU) - KIc/KID and the counter always come from the resolved preset, which must match the equipped card and be complete), status (attempts, interval_ms - when attempts > 1 the default SW check is the mask 91??, i.e. poll until the card announces a command) or proactive-drain (the proactive drain: poll STATUS and consume every announced command - respond/first TERMINAL RESPONSE results, attempts/interval_ms bound, optional require {command, qualifier?} asserting at least one drained command; the final SW is the step's check.sw, so a still-pending card fails and hands the command to the next step - an empty drain passes unless require was given). It is exempt from the runner's unexpected-pending-command guard, so it also consumes a command a previous step left pending (91XX).

check is {"sw": ..., "data": ...} (exact or {"mode": "mask", "value": "91??"}, ? = per-nibble wildcard) plus "por" for SCP80 - "none"/"ok"/"any" or an object {"status"?, "sw"?, "data"?} asserting the decoded PoR of this exchange (inline or SEND SHORT MESSAGE; at least one field). on_fail is error (terminates the script) or warning (continues) and applies to every step failure - including an expectation that finds no pending command (v3.21.0; warning is the tolerant consume-if-pending form) and a card/transport error raised during the step (v3.22.7 - the error is reported as a failed Error check at the step's level).

Expectation steps (type: "expect") require a command pending from the previous step (91XX); they never poll - a 9000 response means no command and fails the step at its on_fail level (TS 102 221 7.4.2.1 / TS 102 223 6.3; a status action with attempts > 1 polls when the card delivers on poll). command is a name or type code; checks may be text (contains/exact), alpha (the command's Alpha identifier 05/85, contains/exact - distinct from the 8D text string), item (id/text for SELECT ITEM / SET UP MENU), raw (mask), por (a SEND SHORT MESSAGE carrying the PoR of the preceding scp80 step - PoR-in-submit, SPI2 bit 20: status (the response status name, ok = por_ok), sw (the R-APDU status word, exact/mask) and data (the R-APDU response data, exact/mask)), sms (the SEND SHORT MESSAGE TPDU: da digits with whitespace skipped and a leading + ignored, pid/dcs/ud hex exact/mask, udl decimal; SMS-SUBMIT only) or files (the File List of a REFRESH, TS 102 223 8.18; files is a list of path hex strings, matched order-insensitively); respond is the TERMINAL RESPONSE in the TS 102 223 6.8.0 object order (result - a script alias or a two-digit hex value from the full TS 102 223 8.12.0 list; additional_info - the general result's additional-information hex byte, sent as 83 02 <result> <additional_info>, default 00; text+dcs for GET INKEY/GET INPUT, dcs 00 packed / 04 8-bit / 08 UCS2; item_id; extra raw TLVs appended after the standard objects). The aliases (ok=00, partial=01, missing=02, modified=07, cancel=10, back=11, timeout=12, refused=22, not_understood=32, plus the compatibility-only no_response=12) follow the spec; v3.23.5 corrected refused, not_understood, modified and no_response (they used to carry legacy values). The terminal's own objects ride along unless raw overrides them: a successful PROVIDE LOCAL INFORMATION answer carries the TR Config PLI dictionary entry (or the host clock for the date/time qualifier) and a POLL INTERVAL answer the current-interval echo.

The response is the initial state (running: true), the final counter (scp80_counter) and the step list; poll /api/test/status. The server persists every counter an SCP80 step consumed into the preset store as the run proceeds; the snapshot's scp80_counter/scp80_counters carry the latest values (the PWA only syncs its local copy - nothing is written back).

A run that ends (an error, a stop, an unexpected pending command) with a command still announced drains it with ok (0x00) - fetching while the TERMINAL RESPONSE answers 91XX (bounded); a cancel is never sent automatically (it tells the card the user aborted the session and can push assertive applications onto an error path). The cleanup is reported as a cleanup step; a script that wants the refusal path answers explicitly (an expectation's respond, or the proactive-drain action's first).

GET /api/test/scripts and the script store

Test scripts live in a server-side store (~/.pysim-simple-server/test_scripts.json, --test-scripts PATH) - the runner is server-side, so the store is the source of truth and every browser manages the same set. The store validates with the same engine the runner uses, so a stored script can never fail at run start for a validation reason.

{"path": "/home/user/.pysim-simple-server/test_scripts.json", "count": 1, "version": 2,
 "scripts": [{"id": "…", "name": "applet RFM update", "suite_id": "…",
              "require_adm": false, "steps": [...],
              "created": 1690000000.0, "updated": 1690000000.0}]}

Schema v2 (v3.22.0): every script belongs to exactly one test suite (suite_id; the suite is the root object) and may declare require_adm (the run verifies the ADM before starting). A v1 entry loads with suite_id: null and the server attaches it to an auto-created "Imported scripts" suite on startup.

POST /api/test/scripts creates one ({name, steps, suite_id, require_adm?} or {script: {...}}; a valid suite_id is required - the script joins the suite's member list) and POST /api/test/scripts/update replaces name/steps/require_adm ({id, script}). A suite_id that differs from the current one is a move (both suites are updated in the request). POST /api/test/scripts/delete ({id}) detaches the script from its suite first - the suite's member list is the only reference, so no dangling id is left. POST /api/test/scripts/move ({id, suite_id}) moves a script to another suite (the target appends it as a member, keeping the on_fail policy) and POST /api/test/scripts/copy ({id, suite_id, name?}) clones it into the target. POST /api/test/scripts/import takes {scripts: [...], mode: "merge"|"replace", suite_id?} (the old localStorage export shape; invalid entries are reported in errors instead of failing the whole import; suite_id attaches the imported scripts to that suite). A replace is refused while any suite still lists scripts — the wipe would leave dangling references; delete those scripts first, or import with merge.

GET /api/test/suites and the suite store

Test suites live next to the scripts (~/.pysim-simple-server/test_suites.json, --test-suites PATH): a suite is the ordered list of its scripts - the optional setup first, the member scripts, the optional teardown last - plus the per-member on_fail policy, the suite-level require_adm and the optional start_reset (reset + fully re-initialize the card before the run - see /api/test/run).

{"path": "/home/user/.pysim-simple-server/test_suites.json", "count": 1, "version": 2,
 "suites": [{"id": "…", "name": "alfa regression", "require_adm": true,
             "start_reset": false,
             "scripts": [{"script_id": "…", "role": "setup", "on_fail": "stop"},
                         {"script_id": "…", "role": "member", "on_fail": "stop"},
                         {"script_id": "…", "role": "teardown", "on_fail": "stop"}],
             "created": 1690000000.0, "updated": 1690000000.0}]}

Schema v2 (v3.23.0) adds start_reset (default false); a v1 file loads with it false.

The store normalises the order (setup first, teardown last, at most one of each; a script appears once per suite) and validates on_fail as stop (default) or continue - "which failures cascade" is a visible per-suite decision.

POST /api/test/suites creates one ({name, require_adm?, start_reset?, scripts?} or {suite: {...}}), POST /api/test/suites/update replaces name/require_adm/start_reset/scripts ({id, suite}; the member set changes through the script endpoints, not here), POST /api/test/suites/delete ({id}) removes it - refused while the suite still holds scripts - and POST /api/test/suites/import takes {suites: [...], scripts: [...], mode: "merge"|"replace"}: the scripts are imported first always with merge (a script replace would wipe the other suites' scripts; the mode applies to the suite store), ids preserved, a collision gets a fresh uuid and the suites' references are remapped; then the suites are imported (replace wipes the suite store first); an unresolvable reference is reported in errors and the suite is skipped. After the import the ownership invariant is reconciled: a script the imported suites list adopts its listing suite, a script listed by several suites keeps its explicit owner and the other listings are pruned, and a script no suite lists any more — the replace mode wipes the suites that owned them — is adopted into "Imported scripts" instead of becoming invisible (the same reconciliation runs at startup for a v3.21.0 store).

The import always merges: an id already in the store gets a fresh uuid (scripts) / a fresh suite id, so importing a bundle into the store it came from duplicates the suite and its scripts — the round-trip is not idempotent. Restore a bundle into a fresh store, or wipe the target first (mode: "replace" for the suites).

POST /api/test/run (single script or suite)

{script, script_id?, preset_id?, require_adm?} runs one script (the PWA sends the edited copy; the server validates and runs it). {suite_id, preset_id?} runs a suite: its scripts in order, one card session, no resume. With start_reset the card is physically reset and fully re-initialized first (ICCID, network state, TERMINAL PROFILE, the equip drain); the run's preset must still match the card's ICCID afterwards (a mismatch refuses the start) and a required ADM is verified against the new card session. Then the ADM prerequisite (suite- or member-level require_adm: the matched preset's key is verified once; a failure refuses the start, is not retried automatically, and requires a manual verify), setup, members (per-member on_fail), teardown. The initialization consumes the card's initial proactive commands (they stay in the proactive log), so a script cannot catch the first command after the reset. The teardown runs on every stop as long as the card is in the reader; a card reset (session change) stops the suite, skips the remaining members and is flagged in the report.

The suite run's snapshot adds suite: the member records (status, steps, note, log - the semantic run-log excerpt), the summary (ok/warning/ error/skipped counts, wall_ms, stopped, session_changed, the SCP80 counters_before/counters_after per keyset), the start_reset flag and the reset result (ICCID + ms) and the run's adm state. The report is exported from the PWA as JSON or Markdown; the run state lives in the server until POST /api/test/clear (no history).

GET /api/test/status

The current (or last) run:

{"running": true, "name": "STK menu browsing", "status": null,
 "index": 1, "total": 3, "scp80_counter": null,
 "steps": [{"index": 0, "type": "action", "label": "ENVELOPE(Menu Selection)",
            "status": "ok", "sw": "9103", "sent": "80C2000009...",
            "checks": [{"label": "SW", "ok": true, "expected": "91??",
                        "actual": "9103", "level": "error"}], "ms": 4}]}

status becomes ok/warning/error/stopped when the run finishes; expected/actual pairs are reported per check.

POST /api/test/stop

Requests a stop ({"stop": true} is set on the run); the runner finishes the current step, drains any pending proactive command with an ok (0x00) TERMINAL RESPONSE (the same bounded cleanup as a run that ends - a cancel is never sent automatically, v3.21.0) and reports the run as stopped.

POST /api/test/clear

Clears a finished run report (409 while a run is active).

POST /api/ram-install

Install a Java Card .cap file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-por_ok PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR por_ok); final_cntr is returned on success and on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). A missing cntr is seeded from the preset's keyset (v3.20.0); an explicit value wins but never lowers the stored counter (only forward moves are persisted). The RAM command format is detected per operation: a read-only GET STATUS [ISD] probe (FORMAT CHECK (compact), then FORMAT CHECK (expanded)) decides between the compact C-APDU and the expanded AA/22 form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries ram_format. Pass ram_format (compact/expanded) to pin it. The NV footprint is measured around the chain: a best-effort GET DATA FF21 (Extended Card Resources, TS 102 226 §8.2.1.7.2) read before the first step and again after the last one (also on failure) reports the free non-volatile memory and its delta — the read is silent (no step record, no failure) and a card without FF21 simply yields no fields. The .cap archive (a ZIP of nested components) is parsed server-side in _cap_parse; no external tooling is required.

Request body:

{
  "cap_hex": "DECAFFED...",
  "sd_aid": "A000000003000000",
  "install_params": "C900",
  "stk_params": "",
  "nv_quota": 0,
  "volatile_quota": 0,
  "make_selectable": true,
  "spi1": "0E", "spi2": "01",
  "kic": "15", "kid": "15",
  "tar": "000000",
  "cntr": "0000000001",
  "kicKey": "D6FCC023...",
  "kidKey": "1B07E7E0..."
}
Field Req Description
cap_hex yes Even-length hex of the .cap file (zipped Java Card CAP), max 48 kB (98304 hex chars)
sd_aid no Security Domain AID for INSTALL[for load]; empty → default ISD A000000003000000
install_params no Hex of the install-parameter field — the mandatory C9 TLV (application-specific parameters; C900 when empty), the System Specific Parameters EF, extra raw TLVs. When empty the server composes it from volatile_quota/nv_quota and stk_params (see below)
stk_params no The STK part appended to the install parameters: the EF-wrapped CA (EF <len> CA <len> …, TS 102 226 §8.2.1.3.2.1 — the SIM access domain leads the CA value as 01 <ad> / 00) or the UICC template (EA <len> 80 … [81 …]). A bare CA is refused (it belongs inside the EF)
nv_quota / volatile_quota no Integer memory quotas (bytes) coded as C8 / C7 in the install EF; only composed when install_params is empty. 0 is a valid quota (the vendor reference form carried C7 02 0000 C8 02 0000); omit the field to leave the TLV out
make_selectable no If true (default), final INSTALL uses P1=0C (install + make selectable)
load_block_size no Bytes of load-file payload per LOAD APDU, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS.
ram_format no RAM command format: auto (default — the read-only probe decides, recorded as FORMAT CHECK (...) steps), compact or expanded to pin it
includeCpi no As in /api/send-ota: true (default) marks the delivered SMS with the OTA CPI IE; false drops it (the full 140-octet single-SM budget then applies)

Install-parameter composition (TS 102 226 §8.2.1.3.2.1; GPD_SPE_013 v1.1 Table 6-5). The memory quotas (C7/C8) and an EF-form STK part (the CA) must share one System Specific Parameters EF; an EA-form STK part is a sibling of EF (the reference TCA loader sends C9 00 EF 00 EA …). A second EF makes the card read the quotas and ignore the CA (live 2026-10-05: por_ok but no toolkit registration and no menu), so the server refuses the combination with a 400 and names the fix:

  • caller-composed install_params that already carry an EF plus an EF-form stk_params → refused;
  • nv_quota/volatile_quota next to non-empty install_params → refused;
  • with an empty install_params the server composes C900 EF{C7?,C8?,CA} (or C900 EF{quotas} EA… for the EA form).

The PWA composes the same single EF itself, so its RAM installs never hit the refusal.

Response (success):

{"success": true, "failed_step": null,
 "steps": [{"name": "INSTALL [for load]", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "por_cntr": "000000011B", "sw": "9000", "bytes": 58, "segments": 1},
           {"name": "LOAD (1/9)", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "sw": "9000", "bytes": 274, "segments": 3},
           {"name": "INSTALL [for install]", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "sw": "9000", "bytes": 66, "segments": 1}],
 "final_cntr": "0000000004",
 "load_file_aid": "A000000003000000",
 "module_aid": "A000000003000000",
 "application_aid": "A000000003000000",
 "load_block_size": 240,
 "load_block_size_requested": null,
 "load_block_size_auto": true,
 "nv_before": 50646,
 "nv_after": 23400,
 "nv_delta": 27246}

load_block_size is the effective size used for the LOAD blocks (240 by default), load_block_size_requested echoes an explicit load_block_size (null = the default was used) and load_block_size_auto marks that default. Each step reports the secured packet size bytes and the number of SMS segments it took. sw is the transport (ENVELOPE/GET RESPONSE) status word; the RAM results are in por_status (the PoR verdict: por_ok, rc_cc_ds_failed, cntr_low, ... or no_por when the card sent none) and por_sw (the remote command's own status word from the compact/expanded response, with por_type/por_cntr/por_data/por_raw for context).

nv_before/nv_after are the free non-volatile memory (bytes) read via GET DATA FF21 before and after the chain, nv_delta their difference (what the operation consumed; negative = memory was released, e.g. a discarded partial load). The three fields are omitted when the card has no FF21 readout; on a failure the values still show what the failed attempt consumed. The two FF21 reads consume counters like any accepted packet, so final_cntr already includes them.

A step fails when the PoR is not por_ok, when por_sw is outside the success set (9000, 61xx more data, 62xx/63xx warnings, CAFE GP "more data"), or when response data arrived but could not be decoded. The failing step carries por_error (e.g. remote SW 6700) and the response sets success: false, failed_step and a detailed error such as LOAD (1/9): remote SW 6700.

Response (failure):

{"success": false, "failed_step": "load_1",
 "steps": [{"name": "install_for_load", "por_status": "por_ok", "sw": "9000"},
           {"name": "load_1", "por_status": "rc_error", "sw": null}],
 "error": "..."}

The steps array contains one entry per GP command. final_cntr is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned.

POST /api/ram-batch/start, GET /api/ram-batch/status, POST /api/ram-batch/stop, POST /api/ram-batch/clear

The batch CAP install (v3.28.0): one .cap and one set of install parameters applied to card after card, for bulk provisioning. The loop runs in the server (a worker thread), so a page reload or a closed tab does not interrupt it; the state lives in memory and a server restart ends the run.

  • POST /api/ram-batch/start — the body is the /api/ram-install payload without the per-card fields (preset_id, tar, spi1, cntr, kic/kid/kicKey/kidKey): the plan is card independent and is built once, so a bad .cap or an invalid parameter combination is a 400 at Start, not a per-card failure. No card is needed — the worker waits for one. 400 for a bad request, 409 when another batch or a test run is active. The response is the status payload below plus ok: true.
  • GET /api/ram-batch/status — card-free (the panel polls it while an install holds the card lock):
{"active": true, "phase": "waiting_card",
 "attempts": [{"ts": 1760000000.0, "iccid": "8970119000004600098",
               "preset": "Batch 0002", "result": "installed",
               "duration_ms": 41230, "final_cntr": "0000000005",
               "load_file_aid": "F0414C46416101",
               "module_aid": "F0414C4641610101", "nv_delta": 3120}],
 "attempts_total": 3,
 "current": null, "notice": "Card 8970119000004600098 installed.",
 "counts": {"installed": 2, "failed": 1, "skipped": 0},
 "started": 1760000000.0, "stopped": null, "progress": null}

phase is idle (no run), waiting_card, installing, stopping, stopped or finished. A failed attempt carries failed_step (e.g. LOAD (3/7)), por_sw (the card's own status word), por_status, por_error and sw — or, when the chain died before a verdict (a lost card, a transport error), just error; a skipped one carries reason (a request/eligibility error: the install never started). current names the card being installed ({iccid, preset, started}) and progress mirrors the RAM progress payload while a chain runs. The report keeps the last 500 attempts (attempts_total counts them all).

  • POST /api/ram-batch/stop — sets the stop flag: a running install finishes first (an interrupted LOAD chain can leave a half-loaded package), then the run ends. It touches no card state, so it answers while the card lock is held (a _CARD_FREE_POST exception); a repeated stop is idempotent, 409 when no run exists.
  • POST /api/ram-batch/clear — drops the finished run's report (409 while a batch is active).

Per-card resolution. Every equipped card's preset is matched by ICCID; the preset's ISD role TAR supplies the keyset (v3.26.0), the keys and the counter seed (v3.20.0) — the same rules as /api/ram-install. A card with no readable ICCID, no matching preset or no usable ISD keyset is recorded as skipped with its reason and the batch keeps waiting. A card is attempted once per run (any outcome): re-equipping it is recorded as already processed in this batch (<outcome>), never installed again — restart the batch to retry it. Failures never stop the run and are never retried automatically.

/api/status carries a small summary in ram_batch — the detailed state minus the attempts list ({active, phase, current, notice, attempts_total, counts, started, stopped}), so the panel's headline (the phase, the current card, the counts) rides the existing 2 s status poll; the attempts report is fetched from /api/ram-batch/status only when attempts_total (or the active flag) changed.

POST /api/ram-install-app

Run the single INSTALL [for install] (or INSTALL [for make selectable]) APDU for an already loaded package — one APDU, so the install parameters can be iterated without deleting and re-loading the .cap. The APDU builders are shared with /api/ram-install (_cap_install_apdu / _cap_make_selectable_apdu; both case 3 — no trailing Le, which the card's SCP80 layer counts as a phantom command) and the step verdict/counter logic is the same (final_cntr is returned on success and on failure).

Request body:

{
  "mode": "install",
  "loadfile_aid": "F0414C46416101",
  "module_aid": "F0414C4641610101",
  "instance_aid": "",
  "privileges": "00",
  "install_params": "C900",
  "stk_params": "",
  "make_selectable": true,
  "spi1": "0E", "spi2": "01",
  "kic": "15", "kid": "15",
  "tar": "000000",
  "cntr": "0000000001",
  "kicKey": "D6FCC023...",
  "kidKey": "1B07E7E0..."
}
Field Req Description
mode no install (default) or make_selectable (GP Table 11-44; needs instance_aid)
loadfile_aid cond Package AID (required for mode: "install")
module_aid cond Executable module / applet class AID (required for mode: "install")
instance_aid no Application AID; empty → the module AID
privileges no Hex privileges value (1 or 3 bytes), default 00
install_params / stk_params no As in /api/ram-install: the C9 field (with any extra TLVs) and the STK part; an EF-form STK part must not follow a caller-composed EF (the quotas and the CA share one EF, v3.20.0)
nv_quota / volatile_quota no As in /api/ram-install: composed into the install EF only when install_params is empty
ram_format no As in /api/ram-install: auto (default — a read-only probe step decides), compact or expanded
includeCpi no As in /api/ram-install: true (default) marks the delivered SMS with the OTA CPI IE

Response: {"success": bool, "steps": [...], "final_cntr": "...", "ram_format": "...", "error": "...", "failed_step": N} — the same step records as /api/ram-install. A missing cntr is seeded from the preset's keyset (v3.20.0; an explicit value wins but never lowers the stored counter).

POST /api/cap-compat

Run the CAP compatibility test: the format check, INSTALL [for load] and the LOAD blocks only up to the block that completes the Import component (the point where the JCRE verifies the import list, JC VM spec 4.5.2), then stop — no last-block flag and no INSTALL [for install], so nothing is committed and nothing has to be deleted afterwards. The verdict (imports_ok) covers the LOAD/import gate only; the link gate is what a real install checks at INSTALL [for install].

Request body: as /api/ram-install (cap_hex, the SCP80 fields, optional load_block_size, ram_format and includeCpi) — a missing cntr is seeded from the preset's keyset (v3.20.0) — plus:

Field Req Description
probe_imports no {"all": "x.y"} or {aid_hex: "x.y"}: rewrite the Import component's requested versions (diagnostic)
probe_additions no [{"aid": hex, "version": "x.y"}]: append synthetic imports (card-capability queries; an AID already present becomes a version override)

Response: {"success": bool, "imports_ok": bool, "steps": [...], "boundary_block": N, "total_blocks": M, "load_file_aid": "...", "module_aid": "...", "ram_format": "...", "probe_imports": [...], "probe_unmatched": [...], "final_cntr": "...", "load_block_size": N, "error": "...", "failed_step": N}.

probe_imports lists the entries changed or appended ({"aid": "...", "from": "1.3", "to": "0.0"}; from: null = an appended synthetic entry). probe_unmatched lists override AIDs that matched no Import entry — the CAP does not import them, so the card is never asked about them — as [{"aid": "...", "version": "0.0"}] with the version the probe requested. The PWA renders both as a per-AID list in the result popup, together with the failing block's CAP section and the remote status word's meaning.

POST /api/sp-verify

Cross-check a secured packet against pySim's OtaDialectSms.encode_cmd reference. Returns the JS-generated packet, pySim reference, a match flag, and the decoded SPI fields. A missing cntr is seeded from the named preset's keyset (v3.20.0) — the same rule the PWA's builder uses, so a reference for a packet built from an untouched counter field matches.

{"spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000",
 "cntr": "0000000001", "apdu": "00a40000023f00",
 "kicKey": "D6FCC023...", "kidKey": "1B07E7E0..."}

Response:

{"js_sp": "...", "py_sp": "...", "match": true,
 "diffs": [], "spi": {"counter": "counter_must_be_higher", ...}}

GET /api/menu

Returns the SIM Toolkit SETUP MENU captured from the card's TERMINAL PROFILE response at startup. Empty {"items": []} if the card didn't send a menu.

Response:

{"command_number": 1, "items": [{"id": 128, "text": "Настройки/Settings"}],
 "title": "Alfa Mobile", "active": false}

POST /api/menu-select

Sends an ENVELOPE(MENU SELECTION) with the selected item ID, then handles the card's proactive response (DISPLAY TEXT or SELECT ITEM).

{"item_id": 128}

Response:

{"type": "display_text", "text": "Hello", "sw": "9122"}

or

{"type": "select_item", "items": [{"id": 1, "text": "Sub-menu"}], "sw": "9122"}

POST /api/menu-respond

Sends TERMINAL RESPONSE to the current proactive command with the given result code. Continues the proactive chain if the card responds with 91XX. If no response arrives within --menu-timeout seconds (default 60, 0 disables), the server watchdog sends the timeout result itself.

{"result": "ok", "item_id": 1}
result TERMINAL RESPONSE code Meaning
ok 0x00 Command performed successfully
cancel 0x10 Proactive session terminated by the user
back 0x11 Backward move in the proactive session requested by the user
timeout 0x12 No response from the user

GET /api/stk-status

Returns the current STK session state.

{"active": true, "pending": true, "pending_type": "select_item"}

POST /api/read

Read file content. Auto-detects transparent vs record files.

{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"], "mode": "raw"}

Returns transparent data:

{"success": true, "sw": "9000", "file_type": "transparent", "data": "...",
 "apdu_times": [{"type": "select", "ms": 12}, {"type": "read_binary", "ms": 9}]}

Returns records:

{"success": true, "sw": "9000", "file_type": "linear_fixed",
 "records": [{"num": 1, "data": "..."}, {"num": 2, "data": "..."}],
 "apdu_times": [{"type": "select", "ms": 12},
                {"type": "read_record", "ms": 11}, {"type": "read_record", "ms": 13}]}

apdu_times reports each command's duration (command sent to response received) classified as select, read_binary or read_record; the PWA uses it for snapshot timing statistics. Other commands are not reported.

POST /api/write

Write raw hex data to a file.

{"name": "EF.ICCID", "fid": "2FE2", "data": "A0A1A2...", "parent_path": ["MF"]}

A full path may be used instead of name/fid/parent, like /api/select and /api/read:

{"path": "ADF.USIM/6F7E", "data": "4FE69DE7..."}

For record files:

{"name": "EF.ADN", "fid": "6F3A", "data": "A0A1...", "record_nr": 1, "parent_path": ["MF", "7F10"]}

Returns:

{"success": true, "sw": "9000"}

POST /api/select

Select a file by name or FID, with optional parent selection.

{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"]}

parent_path lists the path segments from MF to the parent (ADF names or FIDs); the legacy single-segment parent_sel is still accepted but is only unambiguous for ADFs. Resolution is strictly parent-scoped: model-known files are selected through the requested parent only (pySim select_file()), never via pySim's global selectables or its probe_file() model injection, so a same-FID file under another parent is never picked and the filesystem model is not modified. allow_probe: true (PWA custom files) additionally allows a model-unknown 4-hex FID to be selected directly; any temporary model object created for it is detached again before the response is sent.

Returns:

{"name": "EF.ICCID", "fid": "2FE2", "file_type": "transparent",
 "file_size": 10, "record_len": null, "num_of_rec": null,
 "fci_hex": "621082024021...",
 "apdu_times": [{"type": "select", "ms": 12}], "exists": true}

fci_hex is the raw FCP template ('62') from the SELECT response, used by the PWA's Exact FCI checks; file_size / record_len / num_of_rec drive the profiler's size and record checks. When the file does not exist the endpoint responds 404 with {"error": "...", "exists": false}.

POST /api/tree

Get directory listing with typed children.

{"name": "MF", "fid": "3F00"}

Use parent_path (or the legacy parent_sel) to list a subdirectory, e.g. {"name": "DF.GSM-ACCESS", "fid": "5F3B", "parent_path": ["MF", "ADF.USIM"]}.

Returns:

{"exists": true, "name": "MF", "fid": "3F00", "file_type": "df", "children": [{"name": "EF.ICCID", "fid": "2fe2", "isDir": false}]}

GET /api/events

Returns the event list captured from the card's SET UP EVENT LIST (an array of event byte values, or [] when none was received).

POST /api/event-send

Sends an ENVELOPE(Event Download) for a subscribed event.

{"event_type": 4, "event_data": "01A0"}

event_type is required (the SET UP EVENT LIST event byte); event_data is optional hex for events that carry data. Returns the SW and any response data:

{"sw": "9000", "data": "..."}

For Poll Interval Negotiation (event 0x1C, TS 102 223 §7.5.22) the proposal rides in event_data as the Duration TLV 04 02 <unit> <interval> (unit 00 minutes / 01 seconds / 02 tenths of seconds) and the UICC's response is decoded into negotiation:

{"sw": "9000", "data": "1101020402011E",
 "negotiation": {"result": 2, "result_name": "modified",
                 "unit": 1, "interval": 30, "seconds": 30}}

result is 00 accepted / 01 rejected / 02 modified (§8.97); no response data means accepted. A modified answer sets the background poll interval, and a rejected answer may carry the closest acceptable duration.

Channel status (event 0x0A, TS 102 223 §8.56) carries the Channel status TLV B8 02 <status> <info>, where the status byte is the channel id (1–7) OR-ed with the state bits (0x00 link not established / 0x40 TCP LISTEN / 0x80 link established) and the info byte is 00 (no further info) or 05 (link dropped). The server also sends this event automatically when a BIP link drops outside a proactive command and the card subscribed to 0x0A.

POST /api/net-sim

Runs one network-condition scenario from projects/UICC_NAA.md section 13 against the equipped card and returns the step log:

{"scenario": "service_lost", "mcc": "262", "mnc": "01", "lac": "6CD7",
 "send_event": true, "dummy_locations": true, "keep_kasme": true}

Scenarios: cold_boot, attach_eps, attach_2g, service_lost, limited_service, roaming_denied, churn, sms_received, cb_reconfig, authenticate. Optional parameters: mcc/mnc (or plmn), lac, cell_id, tac, rac, tmsi, ptmsi, ptmsi_sig, guti, ksi, kasme, ul, dl, algo, kc, rand, autn, churn_count, churn_delay_ms, and the toggles send_event, dummy_locations, invalidate_epsnsc, keep_kasme, write_kc, sms_location, cb_clear (empty identity values are randomized). The event step is skipped when the card did not subscribe to Location status; only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent — EF.FPLMN is appended only by roaming_denied (shift-list semantics of TS 31.102 §4.2.16, duplicates skipped) and an attach to a listed PLMN clears its entry first (successful manual selection, TS 23.122), while the 5GS location files are never written. The response is {success, error, steps:[{action, file, path, data, sw, ok}], net_state} where net_state is the updated monitor state (see GET /api/net-state).

GET /api/net-state

Returns the cached network-state monitor — the panel the PWA's Phone tab shows next to the simulation buttons. The state is created when a card is equipped (only when EF.ICCID was readable) and is updated in place from the bytes the network simulation wrote, so this endpoint performs no card I/O:

{"available": true, "state": {
   "files": {"imsi": {"name": "EF.IMSI", "fid": "6F07",
                      "path": "ADF.USIM/6F07", "present": true,
                      "source": "init", "updated": 1758396000.0,
                      "kind": "transparent", "data": "0829051032547698"},
             "epsnsc": {"kind": "record",
                        "records": [{"num": 1, "data": "..."}]}},
   "service": {"state": "normal", "source": "service_lost",
               "time": 1758396123.0},
   "network": {"service": {"state": "normal"},
               "location": {"plmn": "26201", "mcc": "262", "mnc": "01",
                            "area": "6CD7", "lac": "6CD7",
                            "country": "Germany", "operator": "Telekom",
                            "roaming": "home", "rejected": false,
                            "source": "write"},
               "home": "26201"},
   "read_at": 1758396000.0}}

available: false with state: null means no monitor state exists yet (no card equipped or EF.ICCID not readable). service.state is the simulated service state (normal / limited / none, null until a scenario or a Location status event sets it); network.location.roaming is home / equivalent / guest against EF.HPLMNwAcT and EF.EHPLMN, and rejected is set when the location files or EF.FPLMN show a permanent rejection. Monitored file keys: imsi, ehplmn, spdi, hplmnwact, loci, psloci, epsloci, epsnsc, cbmi, cbmir, smsstatus, fplmn; each entry's source is init (equip read), read, refresh or write (patched from the simulator's own writes). The PWA decodes the entries client-side.

POST /api/net-state-refresh

Re-reads the monitored files from the card, merges them into the cached state (source: refresh) and returns the same shape as GET /api/net-state. The optional body filters the read:

{"files": ["imsi", "loci", "epsnsc"]}

Without a body all monitored files are re-read. Answers 503 when the reader is not initialized.

GET /api/arr

Returns the card's cached EF.ARR copies — the access-rule files the FCP security attribute DO '8B' references (ISO 7816-4 Table 25). The copies are read once per card session when a card is equipped (MF/2F06, MF/7F10/6F06, MF/7F20/6F06 and ADF.USIM/6F06, best effort) and served from memory, so this endpoint performs no card I/O; a disconnect clears the cache, and an EF.ARR write through /api/write refreshes it.

{"ok": true, "session": 12, "arrs": [
   {"fid": "2F06", "path": "MF/2F06", "record_len": 10, "num_records": 23,
    "records": ["FFFF...", "..."]}]}

ok is false (with an empty arrs) when the card has no readable ARR. The PWA's file manager resolves the '8B' reference of the selected file's FCI against this cache and shows the referenced record's access rules as indented lines; snapshots resolve the same references from their own captured EF.ARR copy.

GET /api/proactive-log

Returns the last 50 proactive commands fetched during CAT sessions, newest first:

[{"type_hex": "25", "type_name": "SET UP MENU", "elapsed": 3.2, "bytes": 97}]

POST /api/status-poll

Manually sends STATUS (F2) and, if the card answers 91XX, runs the proactive chain (FETCH → TERMINAL RESPONSE) until it settles. Returns:

{"sw": "9000", "proactive": true}

POST /api/rescue

Recovers a stuck CAT session by clearing the pending state and re-sending the TERMINAL PROFILE. Returns whether a menu and event list were captured again (plus the profile used):

{"ok": true, "profile": "FFFF...", "menu": true, "events": [4, 5]}

GET /api/terminal-profile

The TERMINAL PROFILE currently in effect and the CLI default (for reference; runtime changes are in-memory only):

{"profile": "FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003",
 "bytes": 33,
 "cli_default": "FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003"}

POST /api/terminal-profile

Sets the TERMINAL PROFILE at runtime (in-memory) and re-sends it to the card, resetting the STK session state exactly like /api/rescue. Body with a new profile, or {} to re-send the current one:

{"profile": "FFFFFFFF7F1F007FFF00001F230811060700"}

Hex, even number of digits, 1–255 bytes. Response is the same shape as /api/rescue (with ok: true); invalid hex is a 400, no reader a 503.

GET /api/poll-status

Background STATUS polling state. Polling is enabled by default (a CAT terminal polls during idle, TS 102 221 §14.6.2; --poll-interval 0 disables it at startup) and enabled is the operator's switch. card_disabled is true after the card sent POLLING OFF (TS 102 223 §6.4.14): proactive polling stays suspended until a POLL INTERVAL arrives, independent of enabled - the PWA shows the effective state (OFF while suspended).

{"enabled": true, "interval": 30, "card_disabled": false}

POST /api/poll-toggle

Turns background STATUS polling on or off.

{"enabled": true}

Returns the new state ({"enabled": ..., "interval": ..., "card_disabled": ...}); while the card has disabled polling, enabling adds a warning explaining that the timer stays suspended until the card sends a new POLL INTERVAL.

GET /api/pli-qualifiers

Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names.

[{"code": "00", "name": "Location Information"}, {"code": "0A", "name": "Battery Charge Level"}]

GET /api/pli-dict

Returns the effective PLI data dictionary as a qualifier-code map: the operator's override, else the built-in default (the qualifiers whose objects the pinned ETSI specs define - location as the test network 001/01, the test IMEI/IMEISV, language, access technology, search mode, battery; v3.23.1). The date/time qualifier stays empty - the host clock answers it while no override is set.

{"00": "930500F1100000", "0A": "630104", "03": ""}

POST /api/pli-dict

Updates dictionary entries. Body is a map of qualifier code to hex value. A non-empty value becomes an override; an empty value clears the override, so the built-in default (if any) applies again. Returns the updated effective dictionary plus, when any field was not saved, the lists that name them: invalid (a malformed key or value — not hex, an odd number of digits, a non-string value) and unknown (a well-formed code outside the qualifier list); both keys are omitted when everything was applied (v3.25.6).

{"00": "130500F1100000", "0A": "630102", "invalid": ["0G"], "unknown": ["18"]}

POST /api/scp81/bip

Starts or stops the local target the card's BIP channel is redirected to.

Dump mode captures whatever the card sends (e.g. its TLS ClientHello) without answering:

{"action": "start", "mode": "dump", "host": "127.0.0.1", "port": 8443}

Redirect mode (mode: "redirect") starts no local listener: every BIP channel the card opens is connected to the configured target (host/port are required — no defaults), which terminates TLS and runs the administration dialog; the address the card requests is only logged. The status API reports mode: "redirect" with the target while it runs. (This is the behavior that was called passthru before 2.2.13 — the name is now taken by the mode below.)

{"action": "start", "mode": "redirect", "host": "203.0.113.10", "port": 10174}

Pass-through mode (mode: "passthru") starts no listener and has no target: every BIP channel dials the destination the card requests in OPEN CHANNEL — the Other address (3E/BE) plus the Transport level (3C/BC) port, TCP client remote (02) only. The specs define no default port, so an incomplete or non-TCP request fails the channel with result 3A and an open-fail log reason; host/port in the request are ignored. The status API reports {"mode": "passthru"} and the per-channel targets appear in bip.channels.

{"action": "start", "mode": "passthru"}

TLS mode runs the Phase B PSK TLS server (GPC v2.2 Amendment B): the PSK table is applied to the TLS handshake, and the GP HTTP administration dialog (X-Admin-* headers, 200 with a command string or 204 No Content) is served. psk_map is the lookup table for the identity the card presents in the TLS handshake — the PWA sends it from the card presets ({identity, psk_hex} objects or an {identity: psk_hex} map); a handshake whose identity is not listed fails with the log entry tls-psk-unknown. The legacy single-key form psk_hex (with optional psk_identity, empty = accept any identity) is still accepted; when both are omitted the table of the previous start is reused. Keys are never stored or logged.

{"action": "start", "mode": "tls", "host": "127.0.0.1", "port": 8443,
 "psk_map": [{"identity": "89012345678901234567",
              "psk_hex": "00112233445566778899aabbccddeeff"}],
 "script": ["80CAFF2100", "80F28002024F0000"], "script_kind": "Explore ISD"}

script is the APDU list served to the card (an explicit list, or none); the server is agnostic to what the APDUs do. script_kind is an optional label for the logs/results. Omitting script keeps the configured script and its run progress.

Stop either mode with {"action": "stop"} (also disables the BIP terminal).

POST /api/bip/control

Generic BIP terminal control for the Simulator's BIP pill - the terminal side of the Bearer Independent Protocol for cards that use TCP without HTTP OTA. The session is shared with the SCP81 listener: only one BIP session runs at a time, starting either control stops the other, and the response reports what was stopped as replaced: {"owner": "scp81"|"bip", "mode": ...} (the PWA shows a notice).

{"action": "start", "mode": "sink", "host": "127.0.0.1", "port": 0}

Modes:

  • sink (default) - starts a local TCP listener that accepts the card's BIP channels and only logs what arrives (conn, sink-rx, conn-close); it never sends anything back. port may be 0/omitted for an ephemeral port; the bound address is reported in the status. Use it for cards that open a TCP connection just to upload data.
  • passthru - no listener and no target: each channel dials the destination the card requests in OPEN CHANNEL (TCP client only, no default port; an incomplete or non-TCP request fails with result 3A).
  • redirect - every channel connects to the required host/port; the address the card requests is only logged.

link_events (default true) controls the terminal-side Channel status events (TS 102 223 7.5.11). An invalid request never disturbs a running session. Stop with {"action": "stop"}; the response carries the same body as the status endpoints.

GET /api/bip/status

Same shape as GET /api/scp81/status: {"owner": "bip"|"scp81"|null, "bip": {...}, "listener": {...}}. A running sink reports {"mode": "sink", "host": ..., "port": ..., "connections": N}.

GET /api/bip/log / POST /api/bip/log-clear

The shared BIP event log (?after=<seq>) - identical to /api/scp81/log.

GET /api/scp81/status

{"bip": {"enabled": true, "mode": "redirect", "target": "127.0.0.1:8443", "channels": [], "seq": 12},
 "listener": {"mode": "tls", "host": "127.0.0.1", "port": 8443,
              "psk_identities": ["89012345678901234567"], "psk_wildcard": false,
              "identity_seen": "89012345678901234567", "identity_matched": true,
              "version_seen": "TLSv1.2", "cipher_seen": "PSK-AES128-CBC-SHA256"}}

owner is scp81 or bip (whichever control started the session), null when idle; it is returned by both this endpoint and /api/bip/status.

Listener modes: tls (local PSK TLS server), dump (capture-only TCP listener), redirect (no local listener; the BIP channels go straight to the configured host:port, e.g. an external HTTP OTA platform — reported as {"mode": "redirect", "host": ..., "port": ..., "target": "host:port"}) and passthru (no listener and no target; each channel dials the destination the card requests in OPEN CHANNEL — reported as {"mode": "passthru"}, with the actual peer in bip.channels[].target).

psk_identities lists the identities the listener accepts (keys are never exposed); psk_wildcard marks the legacy single-key mode. identity_seen / identity_matched reflect the last handshake: an unknown identity is logged as tls-psk-unknown and the handshake fails.

POST /api/scp81/psk-map

Replaces the PSK table of the running TLS listener (the PWA pushes card-preset edits without a listener restart):

{"psk_map": [{"identity": "89012345678901234567",
              "psk_hex": "00112233445566778899aabbccddeeff"}]}

Returns {"ok": true, "identities": [...], "listener": {...}}; entries without an identity or a valid key are skipped, and an empty table is rejected.

GET /api/scp81/log

Returns the BIP/TLS event log (open/close, SEND/RECEIVE DATA hex, TLS handshake and HTTP request/response records). ?after=<seq> returns only newer entries; seq echoes the latest sequence number.

POST /api/scp81/queue

Replace the SCP81 command script (used by the Remote APDU tab's RAM chain "Queue in SCP81" and the PWA's "Restart script"). Body {"apdus": ["80E60C002E...", ...]} (or a single apdu), optional kind and force. Entries that already are Command Scripting templates (AA.../AE80..., the expanded format) are sent verbatim instead of being wrapped again. Refused while a script is mid-run unless forced; queuing resets the execution progress.

POST /api/scp81/gen-install

Generate the RAM (GP) APDU sequence for a .cap without touching the listener or the running script; the PWA's "Install from .cap" script template stores the returned list. The .cap is parsed server-side (same parser as /api/ram-install) and expanded to INSTALL [for load] -> LOAD blocks (240-byte payloads) -> INSTALL [for install]; the file itself is never stored.

{"cap_hex": "504B0304...", "sd_aid": "A000000003000000", "privileges": "00",
 "install_params": "", "stk_params": "", "nv_quota": 0, "volatile_quota": 0,
 "make_selectable": true}

sd_aid empty = the ISD. install_params, stk_params and the nv_quota/volatile_quota primitives follow the composition rule of /api/ram-install (the quotas and an EF-form STK part share one System Specific Parameters EF); an invalid combination answers {"ok": false, "error": "..."}. Responds with {"ok": true, "apdus": [...], "load_file_aid": ..., "module_aid": ...}.

GET /api/scp81/script

Returns the configured command script and the execution state:

{"script": ["80CAFF2100", "80F28002024F0000"], "next": 2, "total": 2,
 "done": [0, 1], "kind": "Explore ISD",
 "pending": {"index": 17, "pos": null, "page": true, "apdu": "80F28003024F0000"},
 "pages": 11, "pages_queued": 0, "complete": false,
 "results": [{"index": 1, "pos": 0, "page": false, "sw": "9000",
              "apdu": "80CAFF2100", "rapdu": "FF210C810102..."}]}

next is the index of the next script APDU to send; done lists the script indices the card reported. pending describes the C-APDU awaiting the card's X-Admin-Script-Status report as {index, pos, page, apdu} (pos = script index, null for an auto continuation page) or null; pages counts the continuation pages queued so far and pages_queued those not yet sent. complete is true when every configured APDU was reported and nothing is in flight — a script can therefore be complete while a listing page is still being fetched (pending.page = true), which is tracked separately from the script's own progress. results entries carry the send order (index), the script position (pos, null for continuation pages) and the page flag.

Execution tracking and resume: an APDU counts as executed only when the card reports it in the next POST's Response Scripting template. A POST with X-Admin-Resume continues with the unexecuted tail (the pending APDU is resent if its report never arrived), a POST without it is a fresh dialog where the script runs from the start, and a completed script closes the session with 204.

Each APDU is delivered in an AE 80 22 <len> <apdu> 00 00 Command Scripting template (TS 102 226 §5.2.1) with X-Admin-Next-URI; the card returns its R-APDUs in the next POST's Response Scripting template, which is parsed and logged (script-rapdu, script-memory). Long GET STATUS listings that answer 63 10 / CA FE ("more data available") are auto-continued with the same command carrying P2.b1=1.

TLS mode also accepts chunked (default true — the reference server's chunked framing; the card rejects a chunked response that also carries a Content-Length) and chunk_size (default 0 — the whole response in one TLS record, as in the decrypted reference session; a positive value writes the head and each body piece as its own record). Both are echoed by GET /api/scp81/status.

The connection stays open between POSTs for the whole dialog (the card is the HTTP client and may reuse it or dial a new one at will — GP Am. B 4.3.1 leaves connection management to the SD); only the 204 ends the session, and the server then shuts the TLS session down cleanly (close_notify while the response is still buffered, then FIN). There is no option to close after every response.

compact_headers (default false) omits the optional space after each header colon — legal per RFC 7230 3.2 (OWS), though a single SP is preferred, and it saves one byte per header (useful to keep a response within one card-sized TLS record); conn_header (default 'none' = omit the header; 'keep-alive' adds it) declares the connection fate, tls_version (default 'auto' — accept TLS 1.0-1.2 and let OpenSSL pick the highest the card offers; '1.0'/'1.1'/'1.2' pin a version for debugging) selects the protocol window, cipher pins one suite, next_uri overrides the per-command X-Admin-Next-URI (%d = command id; empty string omits the header), link_events (default true, all modes) controls the automatic Channel status events, answer_delay waits before answering a request. keylog writes the TLS traffic secrets to the given file (SSLKEYLOGFILE format) for debugging captures — it contains key material, use a temporary path.

The response headers are minimal: X-Admin-Protocol, the optional X-Admin-Next-URI/X-Admin-Targeted-Application, Content-Type on 200s and Transfer-Encoding/Content-Length per chunked. The Date/Server/X-Powered-By mimicry (apache_headers) was removed in 2.2.14 — the reference server's extra headers were an unnecessary copy (the real blocker was the BIP TERMINAL RESPONSE BER length) and the card accepts the minimal set.

GET /api/scp81/status echoes the negotiated handshake as version_seen / cipher_seen (plus identity_seen); a handshake that fails for TLS/cipher reasons (no shared cipher, unsupported protocol version, card alert) is logged as tls-handshake-failed with the OpenSSL reason, separately from post-handshake record errors (tls-error).

The PWA's Listener Options block (collapsed by default; a custom marker appears when anything differs from the reference defaults) exposes chunked, chunk_size, conn_header, compact_headers, next_uri (checkbox + template), script_template, cr_tag, targeted_app (checkbox + //aid/... field) and link_events (applied at Start, persisted in localStorage); tls_version, cipher, keylog and answer_delay stay API-only.