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.
A Flask app serving two features, both driven by per-config entries in
domains.json keyed by a free-form id (<ns-domain>-<suffix>):
/webresponder webhook (via a dial rule);
the app texts the caller hours/directions and returns confirmation XML./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.
| 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 |
| 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 |
| 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.
| 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) |
/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.
POST .../v2/domains/{api_domain}/users —
Simple User {ns_system_user}, service-code=system-webresponder,
email-address=operations@pressone.net (hard-coded).POST .../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}.PUT .../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.
| Param | Example | Used? |
|---|---|---|
NmsAni |
+19734321440 |
✅ SMS destination (+ stripped) |
id (query) |
pressone.net-sms |
✅ config lookup key |
Digits / OrigCallID / NmsDnis |
… | logged only |
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>
.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. |
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.
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
cd /Users/shripald/ClaudProjects/Netsapiens-Scripts/webresponder
venv/bin/pytest -v
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.