How to convert a customer csv to json for marketing apis
- Step 1Export your contact list as CSV — From your spreadsheet or current CRM, export a CSV with a header row. Name the columns to match the API's property names where you can (HubSpot's internal property names, Mailchimp merge tags), so less remapping is needed downstream.
- Step 2Clean the list first if needed — If the list has trailing-space emails, case-different duplicates, or embedded line breaks, run it through the CSV cleaner first — see the email subscriber cleanup guide. Then convert the cleaned file here.
- Step 3Drop the CSV onto the converter above — Accepts
.csv,.tsv,.txt. PapaParse parses in your browser and keeps commas inside quoted cells (acompany, inc.value stays one field). No PII leaves the page. - Step 4Choose Array (or NDJSON for big lists) — Array of objects for a list you wrap in one batch envelope. NDJSON when you will split a large list into per-call chunks — easier to slice 100-record batches for HubSpot.
- Step 5Set inference for scores vs phone/ZIP — Keep inference on so numeric scores and boolean flags are typed. If your list has phone numbers or ZIP codes, be aware inference will coerce numeric-looking ones — turn it off to keep them as exact strings.
- Step 6Wrap in the API envelope and POST — Download JSON, wrap the array in the endpoint's envelope (
{ inputs: array }for HubSpot,{ operations: [...] }for Mailchimp batch), and POST with your API key. Confirm the Records stat matches your contact count.
Array → marketing API envelope
This tool produces the contact array; you wrap it in the endpoint's envelope. Check each API's current docs for exact field names.
| Endpoint | Body envelope | Per-call limit (your batching) |
|---|---|---|
| HubSpot CRM batch create contacts | { "inputs": [ { "properties": {…} } ] } | 100 records per batch call |
| Mailchimp batch operations | { "operations": [ { method, path, body } ] } | Large; Mailchimp queues the batch |
| Generic marketing REST list import | bare array [ {…} ] | Per the endpoint's docs |
Inference and marketing fields
Inference is global. Off keeps everything (including scores) as strings.
| CSV cell | JSON (inference on) | Good or risky? |
|---|---|---|
87 (lead score) | 87 | Good — numeric property accepts it |
true (opted in) | true | Good — boolean property accepts it |
null | null | Good — clears a property explicitly |
+15550100 (phone) | "+15550100" | Good — stays a string (the + keeps it non-numeric) |
5550100 (bare phone) | 5550100 | Risky — becomes a number; turn inference off to keep as text |
01970 (ZIP) | 1970 | Risky — leading zero lost; keep as string |
Cookbook
Recipes for turning a customer CSV into a marketing-API-ready array.
Contact array wrapped for HubSpot batch create
ExampleConvert to an array, then nest each contact under properties and wrap in inputs.
Input (contacts.csv):
email,firstname,leadscore,optedin
a@x.com,Ada,87,true
Output (array, inference on):
[ { "email": "a@x.com", "firstname": "Ada", "leadscore": 87, "optedin": true } ]
// wrap for HubSpot:
{ "inputs": [ { "properties": { "email": "a@x.com", "firstname": "Ada", "leadscore": 87 } } ] }Keep phone and ZIP as exact strings
ExampleBare phone numbers and ZIPs must not be coerced. Turn inference off so they stay strings.
Input:
email,phone,zip
a@x.com,5550100,01970
Output (inference OFF):
[ { "email": "a@x.com", "phone": "5550100", "zip": "01970" } ]
→ with inference ON: phone 5550100, zip 1970 (broken)Omit blank optional fields on update
ExampleSkip empty cells so a blank field is absent — avoids overwriting an existing CRM value with an empty string.
Input:
email,company
a@x.com,
b@x.com,Globex
Output (Skip empty cells ON):
[ { "email": "a@x.com" }, { "email": "b@x.com", "company": "Globex" } ]NDJSON for 100-record HubSpot batches
ExampleConvert a big list to NDJSON, then slice it into 100-line chunks per batch call.
Output (NDJSON): one contact per line
// chunk for HubSpot's 100-per-batch limit
for (const chunk of chunksOf(lines, 100)) {
const inputs = chunk.map(l => ({ properties: JSON.parse(l) }));
await post('/crm/v3/objects/contacts/batch/create', { inputs });
}Explicit null to clear a property
ExampleA literal null cell becomes JSON null, which clears the property on update rather than leaving it unchanged.
Input:
email,company
a@x.com,null
Output:
[ { "email": "a@x.com", "company": null } ]
→ sends null to clear 'company' (vs omitting to leave it)Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Bare phone number coerced to a number
By designA phone like 5550100 (no + or punctuation) matches the integer pattern and becomes a number with inference on — and CRMs store phones as text. Turn off Infer numbers, booleans, null to keep phones as strings. Numbers with a +, spaces, or dashes already stay strings because they do not match the numeric pattern.
ZIP / postal code loses leading zero
By design01970 becomes 1970 with inference on, breaking the postal code. Turn inference off to keep ZIPs as strings. If your list mixes numeric scores (want typed) with ZIPs (want string), convert with inference off and let the CRM coerce the score, or pre-clean the ZIP column.
Empty field overwrites an existing CRM value
Update riskOn an update/upsert, sending "company": "" can blank out an existing CRM value. Turn on Skip empty cells so blank fields are omitted and the existing value is left untouched. Use a literal null cell only when you intentionally want to clear a property.
Boolean property rejects a string
Validation rejectIf you converted with inference off, optedin is the string "true", and a boolean CRM property may reject it. Re-convert with inference on so the value is a real boolean. This is the most common cause of a marketing-API validation error from a converted list.
Per-call batch limit is on your code, not the file
Your responsibilityHubSpot's batch create accepts up to 100 records per call; the converter will happily produce a 50,000-contact array. Chunk it in your code (NDJSON makes 100-line slices easy). The file size has no relation to the API's per-call cap.
Duplicate email column collapses
OverwriteTwo email columns map to one key (later wins), so a value is lost. Rename one before converting. Note this is different from duplicate contacts (same email in two rows) — for that, dedupe the list first via the cleaner.
Tags stored as a comma-joined string, not an array
ExpectedA tags cell like vip,beta is one string in the JSON, not a JSON array — the converter does not split delimited cells into arrays. If the API wants ["vip","beta"], split the column first with csv-column-splitter or post-process the array in your code.
Free tier cap on a large list
LimitFree tier caps at 2 MB / 500 rows. A real marketing list needs Pro (100 MB / 100,000 rows) or higher. For very large lists, split the CSV into chunks under the cap, convert each to NDJSON, and batch-POST per the API's per-call limit.
Frequently asked questions
Does the converter add the HubSpot/Mailchimp envelope automatically?
No — it produces a bare array of contact objects. Wrap it yourself: HubSpot wants { "inputs": [ { "properties": {…} } ] } and Mailchimp's batch endpoint wants { "operations": [...] }. The cookbook shows both. This keeps the tool API-agnostic.
Why is my boolean property being rejected?
Almost certainly you converted with inference off, so optedin is the string "true" and the boolean property rejects it. Re-convert with Infer numbers, booleans, null on so it becomes a real boolean true. This is the most common marketing-API validation failure on a converted list.
How do I keep phone numbers and ZIPs from being mangled?
Turn off type inference so they stay exact strings. Bare numeric phones (5550100) and ZIPs (01970) get coerced otherwise. Phones already containing +, spaces, or dashes stay strings even with inference on, because they do not match the numeric pattern.
Will subscriber PII be uploaded to JAD Apps?
No. Conversion runs entirely in your browser via PapaParse. Emails, names, tags, and any other PII never reach a JAD Apps server — only an anonymous run counter is stored when signed in (opt-out available). This is the right posture for GDPR/CCPA-sensitive marketing lists.
Should I clean the list before converting?
Yes, if it has trailing-space emails, case-different duplicates, or embedded line breaks that platforms reject. Run it through the CSV cleaner first — the email subscriber cleanup guide covers the per-platform quirks — then convert the cleaned file here.
How do I avoid overwriting existing CRM values with blanks?
Turn on Skip empty cells so blank fields are omitted from the JSON. On an upsert, an omitted field leaves the existing value alone, whereas "" would blank it. Use a literal null cell only when you deliberately want to clear a property.
My tags column should be a JSON array — does the tool split it?
No. A tags cell vip,beta becomes one string. To get ["vip","beta"], split the column with csv-column-splitter before converting, or build the array in your code after conversion.
How do I respect HubSpot's 100-records-per-batch limit?
Convert to NDJSON and slice it into 100-line chunks in your code, sending one batch-create call per chunk (see the cookbook). The converter does not enforce the limit — it is on your posting code.
How large a list can I convert?
Free: 2 MB / 500 rows. Pro: 100 MB / 100,000 rows. Pro+Media: 500 MB / 500,000 rows. Developer: 5 GB, no row cap. For larger lists, split the CSV into chunks under your cap and convert each to NDJSON for easy batching.
Can I send a null to clear a property?
Yes. Put the literal word null in the cell; with inference on it becomes JSON null, which most CRM update endpoints treat as 'clear this property'. Omitting the field (Skip empty cells) instead leaves the property unchanged — pick based on intent.
Can I automate list conversion before a scheduled sync?
Yes. Pair the @jadapps/runner once and POST the CSV to 127.0.0.1:9789/v1/tools/csv-to-json/run. A common pipeline: nightly list export → clean → runner converts to NDJSON → batch-POST to the CRM. Subscriber PII stays on your machine and never reaches JAD's servers.
Can I generate a schema or type for the contact records?
Yes — feed the converted array to json-schema-generator for a JSON Schema you can validate the list against before sending, or json-to-typescript for an interface to type the contacts in your sync script.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.