Send Wix Forms to an external CRM via API
- WIXFULL
- Oct 18, 2025
- 3 min read
Velo web module + page code + secure secrets + field mapping (HubSpot / Pipedrive examples)
You’ll wire a Wix Form to your CRM using a backend web module ( .web.js ), safe secrets, and a thin frontend handler. We’ll keep it vendor-agnostic and ship two concrete adapters (HubSpot & Pipedrive).
What you’ll build
A backend crmBridge.web.js that exposes locked-down web methods to push submissions to a CRM. Powered by webMethod() + Permissions, wix-fetch on the backend, and API keys pulled from Secrets Manager.
A page hook on your Wix Form that collects the values and calls the backend safely. (I’ll also show the backend events alternative that catches all forms site-wide.)
Robust mapping, validation, retries, logging, and clear error surfaces.
Prereqs
Create API keys/tokens in your CRM (e.g., HubSpot Private App token; Pipedrive API token).
Store them in Wix Secrets Manager (e.g., HUBSPOT_TOKEN, PIPEDRIVE_TOKEN). dev.wix.com
Know your CRM endpoints:
HubSpot single-contact: POST /crm/v3/objects/contacts (OAuth/Private App token).
Pipedrive create person: POST /v1/persons or v2 /api/v2/persons (token in query/header). Check your account’s version & fields.
Step 1 — Backend web module (backend/crmBridge.web.js)

import { webMethod, Permissions } from 'wix-web-module';
import { getSecret } from 'wix-secrets-backend';
import { fetch } from 'wix-fetch';
const asEmail=v=>String(v||'').trim().toLowerCase();
const nonEmpty=v=>(typeof v==='string'?v.trim():v)||undefined;
function assertHas(obj,fields=[]){for(const f of fields){
if(!nonEmpty(obj[f]))
throw new Error(`Missing required field: ${f}`);
}
}
async function hubspotCreateContact({email,firstName,lastName,phone}){
const token=await getSecret('HUBSPOT_TOKEN');
const url='https://api.hubapi.com/crm/v3/objects/contacts';
const body={
properties:{
email:asEmail(email),
firstname:nonEmpty(firstName),
lastname:nonEmpty(lastName),
phone:nonEmpty(phone)
}
};
const res=await fetch(url,{
method:'POST',headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json'},
body:JSON.stringify(body)
});
if(!res.ok){
const err=await res.text();
throw new Error(`HubSpot error ${res.status}:
${err}`);
}
return res.json();
}
async function pipedriveCreatePerson({email,firstName,lastName,phone}){
const token=await getSecret('PIPEDRIVE_TOKEN');
const base='https://api.pipedrive.com/v1/persons';
const url=`${base}?api_token=${encodeURIComponent(token)}`;
const body={
name:[nonEmpty(firstName),
nonEmpty(lastName)].filter(Boolean).join(' ').trim()||nonEmpty(email),
email:asEmail(email),
phone:nonEmpty(phone)
};
const res=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
if(!res.ok){const err=await res.text();
throw new Error(`Pipedrive error ${res.status}: ${err}`);
}
return res.json();
}
async function dispatchToCRM(provider,payload){switch(provider){
case'hubspot':return hubspotCreateContact(payload);
case'pipedrive':return pipedriveCreatePerson(payload);
default:throw new Error(`Unsupported CRM: ${provider}`);
}
}
export const sendLead=webMethod(Permissions.SiteMemberReader,async({provider,fields})=>{
const data={
email:fields?.email,
firstName:fields?.firstName,
lastName:fields?.lastName,
phone:fields?.phone};
assertHas(data,['email']);
try{const resp=await dispatchToCRM(String(provider||'').toLowerCase(),data);
return{ok:true,provider,resp};
}
catch(e)
{
return {ok:false,provider,error:String(e.message||e)};
}
});Why this way
webMethod() + Permissions is the official pattern to expose backend functions safely to the frontend.
Secrets are retrieved server-side with getSecret(); never ship tokens to the browser.
Backend fetch is the recommended way to call external APIs from Velo.
Endpoints/behavior align with the vendors’ docs (HubSpot Contacts v3; Pipedrive Persons).
Step 2 — Page code: capture Wix Form submit and call the web method
Attach this to the page that hosts your form (Studio .web.js). If your site uses the new form components, prefer their instance event; for classic Wix Forms, you may need the v1 event or the migration notes (see links).
import { sendLead } from 'backend/crmBridge.web';
import { debounce } from 'utils/debounce';
$w.onReady(()=>{
const form=$w('#form1');
if(form?.onWixFormSubmit){form.onWixFormSubmit(async(event)=>{
const values=readFormValues(event);
await pushToCRM(values);
});
} else
{
$w('#submitBtn').onClick(async()=>{const values=readFormValues();
await pushToCRM(values);
});
}
}); function readFormValues(event){
return{email:String($w('#email').value||'').trim(),
firstName:String($w('#firstName').value||'').trim(),
lastName:String($w('#lastName').value||'').trim(),
phone:String($w('#phone').value||'').trim()
};
}
async function pushToCRM(fields){const provider='hubspot';
const res=await sendLead({provider,fields}); if(!res?.ok){console.warn('CRM push failed:',res?.error);}else{/* success UI */}}Heads-up (Studio forms): depending on the exact form element, you might need onWixFormSubmit (fires before server submit) or the newer APIs after migrating from wix-forms v1 to v2. Check the migration note & event references when wiring your specific form widget.
Alternative: Site-wide backend event (catch every form submission)
If you want to push all site forms without page code, use the backend events file. Create backend/events.js and add a handler that the platform invokes on successful submit.
Send Wix Forms to an external CRM via API
// backend/events.js
// Example for Wix Forms after it’s accepted server-side.
// Confirm the exact signature for your current forms package (v1/v2).
export function wixForms_onWixFormSubmitted(event){
const fields={
email:event?.fields?.find(f=>f.fieldName==='email')?.fieldValue,
firstName:event?.fields?.find(f=>f.fieldName==='firstName')?.fieldValue,
lastName:event?.fields?.find(f=>f.fieldName==='lastName')?.fieldValue,
phone:event?.fields?.find(f=>f.fieldName==='phone')?.fieldValue};
const { sendLead }=require('wix-backend-build/require')('backend/crmBridge.web');
return sendLead({provider:'hubspot',fields});}Use the events API variant that matches your site (classic WixForms vs WixFormsV2), per the official references.
Field mapping tips
HubSpot: everything goes under properties: { … }. Custom properties must exist in your HubSpot schema first.
Pipedrive: name, email, phone are common, but custom person fields vary per account; consult PersonFields to map IDs/keys.
Error handling & hardening
Do not expose tokens to the browser (keep them in backend + Secrets Manager).
Implement 429/5xx retry (exponential backoff).
Validate email, strip empty strings, and log failures (a tiny crm_logs collection).
Optional: queue + retry with a scheduled job if CRMs throttle during spikes.
Why this solves real problems
It follows the official Velo patterns for web modules, permissions, secrets, and backend fetch—no hacks, no keys in the client.
It’s CRM-agnostic: swap adapters, keep the rest.
Works with page-level hooks or global backend events, depending on how you manage forms across a site.
Useful references
Web modules, webMethod, Permissions (official). dev.wix.com+1
Secrets Manager (getSecret) and intro. dev.wix.com+1
wix-fetch (backend HTTP calls). dev.wix.com
Wix Forms events & migration to v2. dev.wix.com+1
HubSpot Contacts API (create). developers.hubspot.com
Pipedrive Persons API (IDs, fields).



