Website form to CRM
The browser posts the form to your server. Your server adds the key and calls NexaCloser. The key never reaches the browser.
Browser form ──POST──▶ your server route ──POST /v1/contacts──▶ NexaCloser (holds the key)1. Make a key
Section titled “1. Make a key”In Settings → API → New key: an Organization key with access Contacts: create & update only. Make a test key too, for development. See Access levels.
2. Store it as an environment variable
Section titled “2. Store it as an environment variable”| Variable | Value |
|---|---|
NEXACLOSER_API_KEY |
The key (nxa_… live, nxa_test_… while testing) |
NEXACLOSER_API_BASE_URL |
https://api.nexacloser.com/v1 |
Set them in your host’s environment settings. Never commit them, never print them in logs.
3. The server route
Section titled “3. The server route”A Node.js example (Express style; the same logic works in any framework or language):
// POST /api/contact — called by the website form. Runs on the server only.const BASE = process.env.NEXACLOSER_API_BASE_URL || 'https://api.nexacloser.com/v1'const KEY = process.env.NEXACLOSER_API_KEY
// The consent wording shown next to the checkbox, and its version.const CONSENT_TEXT = 'I agree to receive emails and text messages from Example Clinic. Reply STOP to opt out.'const CONSENT_VERSION = '2026-11-01'
export async function contactRoute(req, res) { const f = req.body || {} if (!f.email && !f.phone) return res.status(400).json({ ok: false })
const body = { email: f.email || undefined, phone: f.phone || undefined, full_name: f.name || undefined, source: 'Website contact form', tags: ['website'], custom_fields: { visit_type: f.visit_type, contact_time: f.contact_time }, attribution: { utm_source: f.utm_source, utm_medium: f.utm_medium, utm_campaign: f.utm_campaign, landing_url: f.page_url, referrer: f.referrer, }, consent: { granted: f.marketing_opt_in === true, text: CONSENT_TEXT, version: CONSENT_VERSION, at: new Date().toISOString(), ip: req.ip, // the visitor's IP (behind a proxy: app.set('trust proxy', 1)) user_agent: req.get('user-agent'), page_url: f.page_url, }, }
const r = await fetch(`${BASE}/contacts`, { method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000), })
if (r.status === 201 || r.status === 200) return res.json({ ok: true }) if (r.status === 422) { const err = await r.json() console.warn('NexaCloser rejected the form', err.details) // field names and codes only return res.status(422).json({ ok: false }) } // 429 / 5xx: keep the lead and retry later (a queue or a cron), never drop it. console.error('NexaCloser unavailable', r.status) return res.status(502).json({ ok: false })}Notes:
JSON.stringifydropsundefinedvalues, so empty form fields are simply not sent.consent.grantedistrueonly when the visitor actually ticked the box.falserecords nothing, which is right: an unticked box is not an opt-out. Leaveconsentout if the form has no opt-in at all.- Do not log the request body: it holds personal data.
- Use a test key until the form works end to end, then switch the variable to the live key.
4. Check it
Section titled “4. Check it”- With the test key, submit the form. Your route gets
200with"test": true; fix any422it reports. - With the live key, submit it once with your own details. The contact appears under
Contacts with tag
website, source channelWebsite contact form, its custom fields and the consent record. - Submit again with the same email: the same contact is updated, not duplicated.
