CALL URL RECEIPT LAB — 2026-11-06 This complete local exercise sends a fictional Dispo-shaped GET event into a durable SQLite receipt ledger. It does not call VICIdial, place a call, connect to a CRM, or install a production service. Python 3.8 or later on Linux/macOS with sqlite3 is required. The standard-library HTTP server is deliberately bound to 127.0.0.1 only. Save receiver.py, send-example.py, selftest.py and event-fictional.json together in a private working directory. Keep native-dispo-url.txt as the later mapping reference. Do not use real customer data. 1. Run the network-independent self-test: python3 -B selftest.py It uses real loopback HTTP with a temporary SQLite database. Seven cases cover store, identical replay, wrong token, missing identity, unexpanded variable, changed same-key payload, and replay after a receiver restart. Temporary test state is removed by the self-test after the assertions. 2. Create a new persistent exercise state directory: python3 -B receiver.py init Expected: a new mode-0700 call-url-state directory, private token.txt and events.sqlite3. Existing state is never overwritten. The secret is not printed. A setup failure requires inspecting your private directory/port; choose a new --state directory rather than discarding an existing ledger. 3. In terminal A start the local receiver: python3 -B receiver.py serve Expected: listening on 127.0.0.1:8765. Keep this terminal open. 4. In terminal B, from the same directory: python3 -B send-example.py Expected first run: HTTP 201 {"result": "stored"} Run the same command again: HTTP 200 {"result": "duplicate"} python3 -B receiver.py inspect Expected: exactly one row with source TRAINING_A, call_id 1700000000.42, event dispo, lead_id 900001 and dispo SALE. These are invented identifiers. 5. Stop only this receiver using Ctrl+C in terminal A, start serve again, then repeat send-example.py in terminal B. It must still return duplicate and inspect must still show one row. Do not run init again to simulate a restart: that would create a different ledger, not test persistence. 6. The self-test also proves that changing dispo for the same source/call/event produces 409 conflict without overwriting the original. A corrected status needs a separately designed revision policy; it is not silently ignored. The receipt key is (source, call_id, event), with a canonical payload hash. The SQLite transaction commits before an acknowledgement is sent. This makes receipt insertion durable across the tested process restart; it does not prove exactly-once delivery, arbitrary power-loss recovery, or exactly-once CRM writes. No worker or CRM side effect is hidden in this example. The ledger is the output. OPTIONAL VICIDIAL VALIDATION — separate, not executed by this package Only an approved single-server synthetic lab should configure the included native-dispo-url.txt in Campaigns -> the test campaign -> Dispo Call URL. Substitute the generated token privately. A loopback address refers to the VICIdial webserver because the Dispo request originates there; it does not refer to your laptop. The receiver must run on that same lab host for this exact URL. First confirm the installed build substitutes uniqueid, lead_id and dispo for your call mode. An empty or unexpanded value is rejected. source TRAINING_A is a single-server fixture namespace, not a collision-proof cluster identity. Save the old field value. Use a synthetic disposition and independently compare the resulting receipt with the matching call/disposition record. A local send-example result proves the receiver path only, not VICIdial triggering. Restore the old field after the test and stop only this receiver. Keep the private ledger for review; rotate/revoke the test token before repurposing it. BOUNDARIES The token is a replayable query credential. It appears in the saved native URL and can appear in upstream URL logs even though this receiver suppresses its own request logs. Never place CRM credentials, notes, names or phone numbers in that URL. This HTTP loopback server is not a public webhook endpoint. A real deployment needs a reviewed authenticated/TLS ingress, restricted access/logs, secret rotation, backup/recovery, bounded retry/reconciliation and a durable downstream job design. Do not expose this teaching server on 0.0.0.0. Primary references: https://www.vicidial.org/docs/CALL_URL_FEATURES.txt https://docs.python.org/3/library/http.server.html https://docs.python.org/3/library/sqlite3.html