Let's Talk Run the Diagnostic Diagnostic

Zoho CRM Bulk Write API: Move 25,000 Records at Once

The Zoho CRM Bulk Write API inserts, updates, or upserts up to 25,000 records in one job from a zipped CSV file. You upload the file, create a job that says which module and columns it maps to, and Zoho processes it in the background, then calls your callback URL or lets you check the status. Its partner, the Bulk Read API, exports up to 200,000 records per job the same way.

This guide covers both, with the Deluge we use to run them and the limits that shape a design. If you'd rather have us move the data, that's our API integration work.

The four steps of a Zoho CRM bulk write job: upload a zipped CSV to get a file_id, create the job with the module and field mappings, Zoho processes it in the background and calls your callback URL, then you download the result file with each row's ID, status, and error

When should you use the bulk APIs?

Not for every job. Here's how we decide:

Records Use Why
A few at a time, as things happen zoho.crm.createRecord or updateRecord in a function Simple, and workflow rules run as normal
Up to a few thousand, now and then The standard insert or update API, 100 records per call Zoho's update endpoint takes up to 100 records per request
Tens of thousands, or a regular large sync Bulk Write One job per 25,000 rows, processed in the background
Exporting a module or a filtered slice of it Bulk Read Up to 200,000 rows per job as CSV

The tradeoff is timing. A normal API call answers right away. A bulk job answers later, so the code that starts it and the code that handles the result are two separate pieces.

How the Bulk Write API works

Zoho's Bulk Write overview lays out four steps:

  1. Prepare the file. One CSV, field API names in the first row, one record per row, all for the same module. Zip it.
  2. Upload it. POST the zip to https://upload.zoho.com/crm/v8/upload with a feature: bulk-write header and your org ID in X-CRM-ORG. The response gives you a file_id.
  3. Create the job. POST to /crm/bulk/v8/write with the operation (insert, update, or upsert), the module, the file_id, which CSV column maps to which field, and an optional callback URL.
  4. Get the result. When the job finishes, Zoho posts to your callback, or you poll /crm/bulk/v8/write/{job_id}. The job details include counts (added, updated, skipped) and a download_url for a result CSV with each row's ID, status, and error.

These numbers come from Zoho's Bulk Write documentation, including the upload and create job pages:

Limit Value
Records per CSV 25,000
Columns per CSV 200
Zip file size 25 MB
Zipped files per upload 1
API credits per job 500, whatever the outcome
Result file available for 7 days
Workflows Not supported on bulk write
Not supported Attachments, notes, file upload fields, record images

Two of those change how you plan. Every job costs 500 credits even if it fails, so validate the CSV before you upload it. And workflow rules don't fire, so anything that normally happens on create, like assignment or a welcome email, needs its own step after the job completes.

Running a bulk write from Deluge

Deluge can do the whole thing: build the CSV, zip it, upload it, and start the job. This function upserts contacts matched on email:

//
// BULK UPSERT CONTACTS FROM A LIST OF MAPS, MATCHED ON EMAIL
//
// 1. BUILD THE CSV: HEADER ROW OF API NAMES, THEN ONE ROW PER RECORD
csvStr = "Last_Name,First_Name,Email,Lead_Source";
for each contactMp in contactsLst
{
	rowLst = List();
	rowLst.add("\"" + ifnull(contactMp.getJSON("last_name"),"").replaceAll("\"","\"\"") + "\"");
	rowLst.add("\"" + ifnull(contactMp.getJSON("first_name"),"").replaceAll("\"","\"\"") + "\"");
	rowLst.add(ifnull(contactMp.getJSON("email"),""));
	rowLst.add("Import");
	csvStr = csvStr + "\n" + rowLst.toString(",");
}
// 2. MAKE IT A FILE AND ZIP IT (COMPRESS RETURNS AT MOST 2 MB)
csvFile = csvStr.toFile("contacts.csv");
zipFile = csvFile.compress("contacts");
zipFile.setParamName("file");
// 3. UPLOAD THE ZIP
uploadHeaderMp = Map();
uploadHeaderMp.put("feature","bulk-write");
uploadHeaderMp.put("X-CRM-ORG",crmOrgIdStr);
uploadResp = invokeurl
[
	url :"https://upload.zoho.com/crm/v8/upload"
	type :POST
	headers:uploadHeaderMp
	files:zipFile
	connection:"zcrm_bulk"
];
fileIdStr = uploadResp.getJSON("details").getJSON("file_id");
// 4. CREATE THE JOB
mappingLst = List();
mappingLst.add({"api_name":"Last_Name","index":0});
mappingLst.add({"api_name":"First_Name","index":1});
mappingLst.add({"api_name":"Email","index":2});
mappingLst.add({"api_name":"Lead_Source","index":3});
resourceMp = Map();
resourceMp.put("type","data");
resourceMp.put("module",{"api_name":"Contacts"});
resourceMp.put("file_id",fileIdStr);
resourceMp.put("find_by","Email");
resourceMp.put("field_mappings",mappingLst);
jobMp = Map();
jobMp.put("operation","upsert");
jobMp.put("ignore_empty",true);
jobMp.put("callback",{"url":callbackUrlStr + "&batch=" + batchRefStr,"method":"post"});
jobMp.put("resource",{resourceMp});
jobResp = invokeurl
[
	url :"https://www.zohoapis.com/crm/bulk/v8/write"
	type :POST
	body:jobMp.toString()
	headers:{"Content-Type":"application/json"}
	connection:"zcrm_bulk"
];
// 5. SAVE THE JOB ID ON THE BATCH RECORD SO THE CALLBACK CAN FIND IT
jobIdStr = jobResp.getJSON("details").getJSON("id");
batchUpdateResp = zoho.crm.updateRecord("Import_Batches",batchIdInt,{"Bulk_Job_Id":jobIdStr});
info batchUpdateResp;

A few details carry the weight:

  • Quote text columns and double any quotes inside them. A company name with a comma breaks an unquoted CSV row and shifts every column after it.
  • ignore_empty: true stops blank cells from wiping out values already on the record, which is what you want on most upserts.
  • find_by: Email tells an upsert how to match existing records. Update and upsert need it; insert doesn't.
  • Deluge's compress returns at most a 2 MB zip. CSV text compresses well, so that covers a lot of rows, but very wide or very large files are better built outside Deluge and uploaded from there.
  • The connection needs the bulk scopes listed on Zoho's create job page, such as ZohoCRM.bulk.ALL.

Diagram of the callback pattern: the Deluge function creates the bulk job with a callback URL that carries its own batch reference, Zoho posts to a CRM function exposed as a REST API when the job finishes, and that function looks up the job, reads the counts, and starts the follow-up steps

Catch the callback with a CRM function

The callback URL can point at a Zoho CRM function exposed as a REST API. In Setup > Developer Hub > Functions, open the function, turn on REST API, and choose an authentication method; Zoho's REST API functions guide covers both options and recommends OAuth 2.0 for production. The function receives the request in crmAPIRequest.

The trick we use: put your own reference in the callback URL. Notice &batch= on the callback above (an API-key function URL already has a ?, so the reference joins with &). When the callback arrives, the function reads that reference from the query parameters, looks up which job and which source batch it belongs to, and finishes the work:

//
// REST API FUNCTION: CALLED BY ZOHO WHEN A BULK WRITE JOB FINISHES
//
paramsMp = crmAPIRequest.get("params");
batchRefStr = ifnull(paramsMp.get("batch"),"");
// THE JOB ID WAS SAVED ON THE BATCH RECORD WHEN THE JOB WAS CREATED
batchLst = zoho.crm.searchRecords("Import_Batches","(Name:equals:" + batchRefStr + ")");
if(batchLst.size() > 0)
{
	batchMp = batchLst.get(0);
	jobIdStr = batchMp.getJSON("Bulk_Job_Id");
	jobResp = invokeurl
	[
		url :"https://www.zohoapis.com/crm/bulk/v8/write/" + jobIdStr
		type :GET
		connection:"zcrm_bulk"
	];
	info jobResp;
	// WRITE THE OUTCOME BACK TO THE BATCH, THEN RUN THE STEPS WORKFLOWS WOULD HAVE
	updateMp = Map();
	updateMp.put("Bulk_Job_Status",jobResp.getJSON("status"));
	updateResp = zoho.crm.updateRecord("Import_Batches",batchMp.getJSON("id"),updateMp);
}
return {"status":"received"};

Zoho documents the job's status values as ADDED, IN PROGRESS, COMPLETED, and FAILED, and the job details include added_count, updated_count, skipped_count, and total_count for the file. Skipped rows explain themselves in the Error column of the result CSV, so a skipped count above zero is the cue to download it.

Treat the REST API endpoint like a password. Anyone with an API-key URL can run the function, so keep it out of shared docs and rotate it if it leaks.

Exporting with the Bulk Read API

Bulk Read is the same idea in reverse. POST a job to /crm/bulk/v8/read with the module, the fields you want, and optional criteria:

//
// EXPORT CONTACTED LEADS: LAST NAME, EMAIL, OWNER
//
queryMp = Map();
queryMp.put("module",{"api_name":"Leads"});
queryMp.put("fields",{"Last_Name","Email","Owner"});
queryMp.put("criteria",{"field":{"api_name":"Lead_Status"},"comparator":"equal","value":"Contacted"});
readMp = Map();
readMp.put("callback",{"url":callbackUrlStr,"method":"post"});
readMp.put("query",queryMp);
readResp = invokeurl
[
	url :"https://www.zohoapis.com/crm/bulk/v8/read"
	type :POST
	body:readMp.toString()
	headers:{"Content-Type":"application/json"}
	connection:"zcrm_bulk"
];
info readResp;

From Zoho's Bulk Read documentation, including the overview and create job pages:

Limit Value
Records per export job 200,000
More than that A new job with the next page number, up to page 500
Download available for 1 day after the job completes
Download requests 10 per minute
Formats CSV, or ICS for Events
Not supported Notes, attachments, emails, related and cross-module data

The one-day download window matters. If the callback handler fails and nobody notices until Monday, the file is gone and the job has to run again.

Side-by-side limits for Zoho CRM bulk jobs: Bulk Write takes 25,000 records per job from a zipped CSV up to 25 MB and keeps results 7 days; Bulk Read exports 200,000 records per job, pages up to 500, and keeps the download one day

The same pattern works outside CRM

Zoho Analytics has its own bulk export API with a callback URL, and we've used the identical shape in client projects: start an export job, point its callback at a CRM REST function, let that function download the rows, process them, and write the results back to Analytics as a JSON file import. When a service has no callback, we hand the job ID to Zoho Flow, which has a Delay step that waits a set number of minutes before calling the next function to collect the results.

What we usually set up

  • A validation pass before upload: required fields present, emails well formed, IDs that exist. Failed jobs still cost 500 credits.
  • An import batch record in CRM that stores the job ID, the source file, and the outcome, so every bulk job is traceable.
  • A callback function that writes the counts back and alerts someone when skipped_count is above zero.
  • Follow-up steps that replace the workflow rules bulk write skips, run once against the new records.
  • Upserts matched on a stable key, usually email or an external ID, so a rerun doesn't create duplicates.

For smaller syncs, our invokeUrl guide covers calling APIs one request at a time, and our Deluge tips post shows how we chunk lists into batches with subList.

Planning a large data move

Start with the numbers: how many records, how often, and what has to happen to each one after it lands. That tells you whether you need the standard API, Bulk Write, or a mix. If you're facing a migration, a nightly sync with another system, or a one-time cleanup of a few hundred thousand records, that's the kind of work our API integration and Zoho migration projects cover. Or tell us what you need to move.

Frequently asked questions

How many records can the Zoho CRM Bulk Write API handle?

Up to 25,000 records per job, from one CSV with up to 200 columns, zipped to 25 MB or less. Each job costs 500 API credits whatever its outcome.

How many records can I export with the Zoho CRM Bulk Read API?

Up to 200,000 records per export job. For more, run another job with the next page number, up to page 500. The download link stays available for one day after the job finishes.

Do workflow rules run on records created by Bulk Write?

No. Zoho lists workflows as not supported for bulk write, so plan any follow-up automation, such as assignment or notifications, as a separate step after the job completes.

Can I use the Bulk Write API from Deluge?

Yes. Build the CSV as text, turn it into a file with toFile, zip it with compress, upload it with invokeUrl, then create the job. Deluge's compress returns at most a 2 MB zip, which covers most jobs because CSV compresses well.

Free Zoho Game Plan

Moving a big dataset in or out of Zoho CRM?

Free 30-minute call. A one-page written plan afterward. No obligation.

  1. 01Book a 30-minute call. Pick a time that suits you. You talk with a senior Zoho expert, not a sales rep.
  2. 02Walk us through it. Your current Zoho setup or what you're planning, the processes that matter most, and where things break or slow down.
  3. 03Get your plan in writing. A one-page plan: what to fix or build first, a rough cost range, and which engagement fits. It's yours to keep whether you work with us or not.

Or send us a message