← back to Wallpapercontractor
Notify NPH ops on each lead: fire-and-forget POST to /api/partner-lead (token-gated, 4s timeout, non-blocking); add dotenv + env docs
d0fa59a4d279fad8abb5bc9b225c71d2be583b28 · 2026-05-31 17:21:22 -0700 · steve
Files touched
M DEPLOY.mdM package-lock.jsonM package.jsonM server.js
Diff
commit d0fa59a4d279fad8abb5bc9b225c71d2be583b28
Author: steve <steve@designerwallcoverings.com>
Date: Sun May 31 17:21:22 2026 -0700
Notify NPH ops on each lead: fire-and-forget POST to /api/partner-lead (token-gated, 4s timeout, non-blocking); add dotenv + env docs
---
DEPLOY.md | 24 ++++++++++++++++++------
package-lock.json | 13 +++++++++++++
package.json | 1 +
server.js | 27 +++++++++++++++++++++++++++
4 files changed, 59 insertions(+), 6 deletions(-)
diff --git a/DEPLOY.md b/DEPLOY.md
index 7412d2b..485de52 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -27,7 +27,7 @@ GoDaddy → unlock → set nameservers to **GoDaddy default** → add **A `@`
## STEP 3 — Deploy to Kamatera *(Claude can run once DNS resolves to 45.61.58.125)*
```sh
-rsync -az --exclude node_modules --exclude .git \
+rsync -az --exclude node_modules --exclude .git --exclude .env \
~/Projects/wallpapercontractor/ my-server:/root/Projects/wallpapercontractor/
ssh my-server 'cd /root/Projects/wallpapercontractor && npm install --omit=dev'
ssh my-server 'cd /root/Projects/wallpapercontractor && pm2 start server.js --name wallpapercontractor && pm2 save'
@@ -43,8 +43,20 @@ project segment mapped (`residential|multifamily→luxury_residential`,
`hospitality→hospitality`, `retail→retail`; corporate/healthcare omit segment → full
breadth) plus UTM attribution + `lead_id`. Verified: residential lead → `/find?...&segment=luxury_residential` → 200 live.
-- NPH has **no generic lead-intake API** (its funnel is `/find` → `/installer/:slug` →
- `POST /api/installers/:slug/book`), so the integration is funnel-redirect + local
- capture, not a server-to-server lead POST.
-- **Optional follow-up:** if you want NPH *ops* to be emailed each contractor lead, that
- needs a small new endpoint on NPH (none exists today) — say the word and I'll add it.
+### NPH ops email — WIRED ✅ (2026-05-31)
+Each lead also fires a **server-to-server, fire-and-forget** POST to NPH's
+`POST /api/partner-lead` (token-gated, CSRF-exempt, rate-limited; added to NPH this
+session, commit e3c2ed5). NPH emails ops (`NPH_LEADS_EMAIL`, default
+info@nationalpaperhangers.com) with the lead. The call has a 4s timeout and never blocks
+the visitor's redirect; the local `leads.jsonl` capture is the durable record. Verified
+end-to-end: contractor lead → NPH `/api/partner-lead 200` (UA `node`) → ops emailed.
+
+### Required env on prod *(create `/root/Projects/wallpapercontractor/.env` — gitignored, NOT rsynced)*
+```
+PARTNER_LEAD_TOKEN=<must MATCH the value in NPH's .env> # internal shared secret
+NPH_LEAD_ENDPOINT=http://127.0.0.1:9765/api/partner-lead # same-box internal (faster than public)
+```
+> The deploy rsync in STEP 3 must add `--exclude .env` so the local test `.env`
+> (which points NPH_LEAD_ENDPOINT at the public URL) doesn't overwrite the prod one.
+> If `PARTNER_LEAD_TOKEN` is unset, the ops-notify is silently skipped (lead still
+> captured + redirected) — so a missing token degrades gracefully, never errors.
diff --git a/package-lock.json b/package-lock.json
index 0ee438a..928b801 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"compression": "^1.7.4",
+ "dotenv": "^17.4.2",
"ejs": "^3.1.10",
"express": "^4.21.0",
"helmet": "^8.1.0",
@@ -324,6 +325,18 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
diff --git a/package.json b/package.json
index f71cc8e..104caa8 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"compression": "^1.7.4",
+ "dotenv": "^17.4.2",
"ejs": "^3.1.10",
"express": "^4.21.0",
"helmet": "^8.1.0",
diff --git a/server.js b/server.js
index 4e70e73..116aa32 100644
--- a/server.js
+++ b/server.js
@@ -1,3 +1,4 @@
+try { require('dotenv').config(); } catch (_) { /* dotenv optional */ }
const express = require('express');
const path = require('path');
const fs = require('fs');
@@ -11,6 +12,12 @@ const GA_ID = process.env.GA_ID || 'G-LC83F70YHV';
const contractors = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'contractors.json'), 'utf8'));
+// NPH ops notification (server-to-server, fire-and-forget). On Kamatera, set
+// NPH_LEAD_ENDPOINT=http://127.0.0.1:9765/api/partner-lead (same box, internal).
+// PARTNER_LEAD_TOKEN must match the value in NPH's .env.
+const NPH_LEAD_ENDPOINT = process.env.NPH_LEAD_ENDPOINT || 'https://nationalpaperhangers.com/api/partner-lead';
+const PARTNER_LEAD_TOKEN = process.env.PARTNER_LEAD_TOKEN || '';
+
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
@@ -200,6 +207,26 @@ app.post('/api/lead', (req, res) => {
return res.status(500).json({ error: 'Could not save your request — please email info@wallpapercontractor.com.' });
}
+ // Notify NPH ops, server-to-server. Fire-and-forget with a hard timeout so it
+ // NEVER blocks the visitor's response/redirect; the local capture above is the
+ // durable record, this is just the ops heads-up.
+ if (PARTNER_LEAD_TOKEN && typeof fetch === 'function') {
+ const ac = new AbortController();
+ const t = setTimeout(() => ac.abort(), 4000);
+ fetch(NPH_LEAD_ENDPOINT, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'x-partner-token': PARTNER_LEAD_TOKEN },
+ body: JSON.stringify({
+ source: lead.source, lead_id: lead.id,
+ name, email, phone, zip, project_type, square_footage, timeline, material_status, role, notes,
+ }),
+ signal: ac.signal,
+ })
+ .then(r => { if (!r.ok) console.warn('[lead] NPH ops notify non-200:', r.status); })
+ .catch(e => console.warn('[lead] NPH ops notify failed:', e && e.message))
+ .finally(() => clearTimeout(t));
+ }
+
// Wire into the nationalpaperhangers funnel: land the visitor on /find (the
// installer-search results — the real funnel entry), pre-filtered to their
// project segment when it maps cleanly. NPH /find matches ?segment= against
← 66d30ec Update runbook: /api/lead → NPH funnel wire is done + verifi
·
back to Wallpapercontractor
·
hero: unique og:image hero-bg.jpg (dedupe fleet viewer + soc 52f7ff5 →