# Design: NS Web Responder — SMS Directions & Hours

**Date:** 2026-06-02  
**Status:** Approved

## Problem

When a caller presses a digit on a Netsapiens Auto Attendant, we want to SMS them the business's hours and directions. Each customer domain has different hours, directions, and outbound from-number.

## Architecture

```
NS Auto Attendant (caller presses digit)
  → POST https://ns-scripts.pressone.net/webresponder
      Flask app (gunicorn, localhost:5100)
        → looks up AccountDomain in domains.json
        → POST https://api.pressone.net/ns-api/v2/domains/{domain}/users/~/messagesessions/{uuid}/messages
            (Bearer token — super-user level, covers all domains)
        → returns <Response><Say>…</Say><Hangup/></Response> to NS
```

Caddy routes `/webresponder*` on `ns-scripts.pressone.net` to `localhost:5100`. All other paths on that domain continue to the existing nginx:8081 Docker container.

## Files

All files live in `/opt/ns-scripts/webresponder/` on `ns-scripts.pressone.net`.

| File | Purpose |
|---|---|
| `app.py` | Flask app — `POST /webresponder` endpoint |
| `domains.json` | Per-customer config: domain → from_number + message |
| `.env` | `NS_TOKEN` (super-user Bearer token) |
| `.env.example` | Committed template with placeholder values |
| `requirements.txt` | flask, gunicorn, requests, python-dotenv |
| `ns-webresponder.service` | systemd unit — auto-start, auto-restart |

## Request / Response Flow

1. NS POSTs call parameters to `/webresponder`:
   - `NmsAni` — caller's E.164 number (SMS destination)
   - `AccountDomain` — customer's NS domain (config lookup key)
   - `Digits`, `OrigCallID`, etc. (logged, not used for routing)

2. Flask looks up `AccountDomain` in `domains.json` → retrieves `from_number` and `message`.

3. Flask generates a unique `messagesession` using UUID4 (dashes stripped = 32 alphanumeric chars, meets NS minimum).

4. Flask POSTs to NS SMS API:
   ```
   POST https://api.pressone.net/ns-api/v2/domains/{domain}/users/~/messagesessions/{uuid}/messages
   Authorization: Bearer {NS_TOKEN}
   Content-Type: application/json

   {
     "type": "sms",
     "message": "{message from domains.json}",
     "destination": "{NmsAni}",
     "from-number": "{from_number from domains.json}"
   }
   ```

5. Flask returns XML to NS:
   - **Success:** `<Response><Say>We've texted you our hours and directions — goodbye!</Say><Hangup/></Response>`
   - **Domain not found or SMS error:** `<Response><Say>Sorry, we could not send directions at this time.</Say><Hangup/></Response>`

## Configuration

### `.env`
```
NS_TOKEN=<super-user bearer token>
```

### `domains.json`
```json
{
  "customer1.net": {
    "from_number": "+18135551234",
    "message": "Our hours are Mon–Fri 9am–5pm. We are at 123 Main St, Tampa FL 33601."
  },
  "customer2.net": {
    "from_number": "+14075559876",
    "message": "Open Mon–Sat 8am–6pm. Find us at 456 Oak Ave, Orlando FL 32801."
  }
}
```

Adding a new customer = add one JSON entry. No restart required (app reloads the file on each request).

## Error Handling

| Condition | Behavior |
|---|---|
| `NmsAni` missing from POST | Log warning, return "could not send" Say + Hangup |
| `AccountDomain` not in `domains.json` | Log warning, return "could not send" Say + Hangup |
| NS SMS API returns non-2xx | Log error + status code, return "could not send" Say + Hangup |
| Unhandled exception | Flask 500 — NS treats as failure, moves to next AA action |

## Deployment

### Server
- **Host:** `ns-scripts.pressone.net`
- **Path:** `/opt/ns-scripts/webresponder/`
- **OS:** Ubuntu 24.04 LTS
- **Python:** 3.12.3 (system); venv at `./venv`

### Caddy change
Add inside the existing `ns-scripts.pressone.net` block in `/etc/caddy/Caddyfile`:
```
handle /webresponder* {
    reverse_proxy localhost:5100
}
```
Move the existing catch-all `reverse_proxy localhost:8081` into a `handle` block so order is respected.

### systemd unit (`ns-webresponder.service`)
- Runs: `venv/bin/gunicorn -w 2 -b 127.0.0.1:5100 app:app`
- Working directory: `/opt/ns-scripts/webresponder`
- `Restart=always`, `RestartSec=5`

## NS Auto Attendant Configuration

In the customer's Auto Attendant digit action, set the action to **Web Responder** and the URL to:
```
https://ns-scripts.pressone.net/webresponder
```

## Future: n8n Admin UI

Phase 2 will add a `POST /admin/domains` endpoint to Flask (protected by a shared secret) and an n8n form workflow so customer service reps can add/edit customer entries without SSH access.
