Web Responder & Click-to-Call — Technical Reference

For day-to-day operations and the authoritative deploy process, see docs/webresponder-runbook.md. This doc is the technical reference for the two features and their NS integration.

What It Does

A Flask app serving two features, both driven by per-config entries in domains.json keyed by a free-form id (<ns-domain>-<suffix>):

  1. SMS Responder — NS calls the /webresponder webhook (via a dial rule); the app texts the caller hours/directions and returns confirmation XML.
  2. Click-to-Call/widget.js serves an embeddable button; /click-to-call originates a queued callback via the NS legacy API.

A Zoho form per feature posts to /update-sms or /update-domain to upsert config. /update-sms also auto-provisions the NS routing objects.


Server

Item Value
Host ns-scripts.pressone.net (hostname ponjsecdev1)
SSH root@ns-scripts.pressone.net (password in 1Password)
Deploy path /opt/ns-scripts/webresponder/
App Flask via gunicorn (-w 2 -b 127.0.0.1:5100), systemd unit ns-webresponder
Reverse proxy Caddy (server-config/Caddyfile → symlinked /etc/caddy/Caddyfile)
Static server nginx Docker container on :8081
Public URL https://ns-scripts.pressone.net

Files in This Repo

File Purpose
webresponder/app.py Flask app — all routes + NS provisioning helpers
webresponder/domains.json Per-config data (gitignored on server; template domains.example.json)
webresponder/tests/ pytest suite (~66 tests)
webresponder/ns-webresponder.service systemd unit
server-config/Caddyfile reverse-proxy routing

Routes

Route Method Looks up by Notes
/webresponder GET/POST ?id= SMS webhook; returns confirmation XML
/click-to-call POST id in JSON rate-limited; validates US phone
/widget.js GET ?id= branded button JS
/update-domain POST id form field Click-to-call Zoho form
/update-sms POST id form field SMS Zoho form + auto-provisioning

All app routes must be added to server-config/Caddyfile or they 404 (fall through to the static server).


domains.json Schema

{
  "pressone.net-sms": {
    "api_domain": "pressone.net",
    "user": "2000",
    "ns_system_user": "8001",
    "from_number": "19294873999",
    "message": "Our hours are Mon-Fri 9am-5pm. We are at 123 Main St, Tampa FL."
  },
  "pressone.net-sales": {
    "api_domain": "pressone.net",
    "queue": "9002@pressone.net",
    "branding": { "button_text": "Call Us Now", "button_color": "#1a73e8", "company_name": "PressONE", "logo_url": "" }
  }
}
Field Feature What it is
key (id) both <ns-domain>-<suffix>; used in webhook/widget URLs
api_domain both the real NS domain
user SMS extension that owns the SMS DID (sends the text)
ns_system_user SMS routing Simple User auto-created by provisioning (≠ user)
from_number SMS SMS DID, digits only
message SMS SMS body
queue C2C call queue, EXT@DOMAIN format
branding C2C button text/color/company/logo

No restart needed when domains.json changes — re-read on every request.


NS API Details

Item Value
API server ucportal.pressone.net
Auth Bearer NS_TOKEN in /opt/ns-scripts/webresponder/.env
SMS send (v2) POST .../v2/domains/{api_domain}/users/{user}/messagesessions/{uuid}/messages
Click-to-call (legacy) POST .../ns-api/?object=queuedcall&action=create (multipart; returns 202)
SMS inventory GET .../v2/domains/{domain}/smsnumbers (find user via dest)

SMS Auto-Provisioning (/update-sms)

After saving config, provision_sms_responder() runs three idempotent (409 / "already exists" → success), best-effort steps and returns a per-step status ({"user":..,"dial_rule":..,"answer_rule":..}). Config is saved regardless; partial failures still return 200.

  1. Create system userPOST .../v2/domains/{api_domain}/users — Simple User {ns_system_user}, service-code=system-webresponder, email-address=operations@pressone.net (hard-coded).
  2. Create dial rulePOST .../v2/domains/{api_domain}/dialplans/{api_domain}/dialrules (note the separate {dialplan} segment — plan named after the domain), application To Web, matches webresponder{ns_system_user}, dial-rule-parameter=https://ns-scripts.pressone.net/webresponder?id={id}.
  3. Set answer rulePUT .../v2/domains/{api_domain}/users/{ns_system_user}/answerrules/* forward-always → webresponder{ns_system_user}; falls back to POST .../answerrules if the * timeframe doesn't exist yet.

Routing chain: call → {ns_system_user}@{api_domain} → answer rule forward-always → webresponder{ns_system_user} → dial rule (To Web) → webhook GET ?id={id} → SMS sent.


What NS Sends to the SMS Webhook

Param Example Used?
NmsAni +19734321440 ✅ SMS destination (+ stripped)
id (query) pressone.net-sms ✅ config lookup key
Digits / OrigCallID / NmsDnis logged only

XML Responses

Success: <Response><Say>We've texted you our hours and directions…</Say><Hangup/></Response> Error: <Response><Say>Sorry, we could not send directions at this time.</Say><Hangup/></Response>


Environment Variables (.env on server, gitignored)

Var Purpose
NS_TOKEN NS Bearer token (SMS + queuedcall + provisioning). App won't start without it.
UPDATE_SECRET shared secret the Zoho forms send to /update-sms and /update-domain. Required.

Caddy Config

server-config/Caddyfile (symlinked to /etc/caddy/Caddyfile). The ns-scripts.pressone.net block forwards app routes to :5100, everything else to the static container on :8081:

ns-scripts.pressone.net {
    handle /webresponder*   { reverse_proxy localhost:5100 }
    handle /widget.js*      { reverse_proxy localhost:5100 }
    handle /click-to-call*  { reverse_proxy localhost:5100 }
    handle /update-domain*  { reverse_proxy localhost:5100 }
    handle /update-sms*     { reverse_proxy localhost:5100 }
    handle                  { reverse_proxy localhost:8081 }
}

After editing: caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy.


Service Management

systemctl status ns-webresponder
systemctl restart ns-webresponder        # after app.py or .env changes
journalctl -u ns-webresponder -f
journalctl -u ns-webresponder -n 50 --no-pager

Running Tests Locally

cd /Users/shripald/ClaudProjects/Netsapiens-Scripts/webresponder
venv/bin/pytest -v

Deploying a Code Change

Git discipline — edit local → commit → push → pull on server → restart. Never hand-edit files on the server.

# On the Mac
cd ~/ClaudProjects/Netsapiens-Scripts
cd webresponder && venv/bin/pytest -q && cd ..
git add -A && git commit -m "feat(webresponder): describe change" && git push

# On the server
ssh root@ns-scripts.pressone.net
cd /opt/ns-scripts && git pull
systemctl restart ns-webresponder            # for app.py / .env changes
# caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy   # for Caddyfile changes
journalctl -u ns-webresponder -n 20 --no-pager

domains.json is gitignored, so pulls don't clobber live config.