SCP81: - New listener mode "passthru": no local listener - the card's BIP channels connect straight to a configured external platform (host/port required), which terminates TLS and runs the dialog. The status API reports mode/target (_SCP81_MODE/_SCP81_TARGET) and clears them on stop. PWA mode select, hint, Start validation; TLS PSK stays the default. Proactive command decoding (Phone tab log): - SEND SHORT MESSAGE (0x13) is now decoded: alpha, address (TON/NPI + number), 3GPP-SMS TPDU (type, TP-MR, TP-DA, TP-PID 0x7F flagged as SIM data download, TP-DCS, TP-VP, TP-UDL), UDH concatenation IEs, and the TP-UD as text (GSM-7 with a septet unpacker, UCS2, 8-bit) or as a TS 31.115 secured packet for PID 0x7F; malformed TPDUs fall back to the raw hex line. - PROVIDE LOCAL INFORMATION qualifier names completed per TS 102 223 V18.3.0: ESN (07), MEID (0B), Supported RATs (1A); 05 relabelled "Reserved for GSM (Timing Advance)". Fixed in the server dict and both frontend tables. Header: - Compact ADM badge next to the card indicator: "ADM ✓" green when pySim's adm_verified is set, "ADM ✗" red otherwise, hidden without a card session or when the server is down; updated ahead of the card state-key early return so it never disturbs the connect/reset flow. File manager: - Sort pills (FID/Name), Probe all files button and progress line are pinned above the tree instead of scrolling with it; the tree box cap grows from 420px to 65vh. SW cache otaman-v165; help EN/RU updated; tests 234 Python + 361 frontend.
24 KiB
pysim-otaman-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 (OTAMan) | Status |
|---|---|---|
| 1.x.x | 1.x.x | ✅ Compatible |
| 0.x.x | 1.x.x | ❌ Outdated — update server |
| 2.x.x+ | 1.x.x | ⚠️ Server newer — update PWA |
The server reports its version via GET /api/version. The PWA checks this on
connect and warns if versions are incompatible.
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/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/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/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/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/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) |
/api/scp81/log |
GET | HTTP OTA event log (?after=<seq>) |
/api/scp81/log-clear |
POST | Clear the HTTP OTA 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": "2.1.2"}
GET /api/status
Card reader, card type, current selection, and card state.
GET /api/commands
List all available shell commands for the current card profile.
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/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. The secured packet is delivered in an SMS-DELIVER TPDU wrapped in an ENVELOPE command.
Request body:
{
"sp": "00201516011515b00000...",
"spi1": "16",
"spi2": "01",
"kic": "15",
"kid": "15",
"tar": "b00000",
"cntr": "0000000001",
"kicKey": "D6FCC023...",
"kidKey": "1B07E7E0..."
}
Response (delivery PoR):
{"success": true, "sw": "9000", "response_data": "027100000e0a...",
"por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
"decoded": {"number_of_commands": 1, "last_status_word": "6e00",
"last_response_data": ""}}}
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 SPI2 por_in_submit bit (0x20) selects submit-mode PoR.
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 and its PoR is checked; the sequence aborts on the first PoR error. 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": "C90000",
"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 C9 TLV install parameters; if empty, gen_install_parameters() is used with the quota/stk params |
stk_params |
no | Hex CA TLV (TS 102 226 §8.2.1.3.2.1) for SIM toolkit app-specific params |
nv_quota / volatile_quota |
no | Integer memory quotas (bytes) for gen_install_parameters() |
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. When empty/omitted the server auto-fits: the largest size whose SCP80 secured packet still encodes into one SMS (140 octets; e.g. 107 for the 3DES spi1=16/spi2=01 configuration). An explicit value larger than the fitting size is clamped; over SCP80 the default 240 does not fit and used to fail with pySim's "Cannot encode command in a single SMS". |
Response (success):
{"success": true, "failed_step": null,
"steps": [{"name": "install_for_load", "apdu": "80E60200...", "por_status": "por_ok", "sw": "9000"},
{"name": "load_0", "apdu": "80E80000...", "por_status": "por_ok", "sw": "9000"},
{"name": "install_for_install", "apdu": "80E60C00...", "por_status": "por_ok", "sw": "9000"}],
"final_cntr": "0000000004",
"load_file_aid": "A000000003000000",
"module_aid": "A000000003000000",
"application_aid": "A000000003000000",
"load_block_size": 107,
"load_block_size_requested": null,
"load_block_size_clamped": false}
load_block_size is the effective size used for the LOAD blocks,
load_block_size_requested echoes an explicit load_block_size (null =
auto-fit) and load_block_size_clamped is true when the requested size was
reduced to fit one SMS.
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/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.
{"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"]}
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": "..."}
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.
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:
{"menu": true, "events": [4, 5]}
GET /api/poll-status
Background STATUS polling state.
{"enabled": true, "interval": 300}
POST /api/poll-toggle
Turns background STATUS polling on or off.
{"enabled": true}
Returns the new state ({"enabled": ..., "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 current PLI data dictionary as a qualifier-code map.
{"00": "0291...", "0A": "64"}
POST /api/pli-dict
Updates dictionary entries. Body is a map of qualifier code to hex value; keys must be known qualifiers and values valid hex, otherwise they are ignored. Returns the updated dictionary.
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}
Pass-through mode (mode: "passthru") starts no local listener: every BIP
channel the card opens is connected to the configured external platform
(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: "passthru" with the target while it runs.
{"action": "start", "mode": "passthru", "host": "203.0.113.10", "port": 10174}
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"}
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).
GET /api/scp81/status
{"bip": {"enabled": true, "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}}
Listener modes: tls (local PSK TLS server), dump (capture-only TCP
listener) and passthru (no local listener; the BIP channels go straight to
host:port, e.g. an external HTTP OTA platform — reported as
{"mode": "passthru", "host": ..., "port": ..., "target": "host:port"}).
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": "", "make_selectable": true}
sd_aid empty = the ISD. 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",
"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.
keep_alive (default true, matching the reference session: the card sends
all its POSTs on one connection until the 204) ends the TLS connection after
each response
(after the card drained the BIP buffer, with close_notify, so the card
processes the script and opens a new connection for its next POST);
compact_headers (default false) drops the space after each header colon,
apache_headers (default true) adds Date/Server/X-Powered-By like the
reference servers and puts Transfer-Encoding before Content-Type,
conn_header (default 'none' = omit the header, like the reference)
declares the connection fate, tls_version pins 1.1/1.0 for cards that
only speak the older record layer, 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) 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.