Zoho CRM Bulk Write API: Limits, Steps and Example
How the Zoho CRM Bulk Write API works, its documented limits, when to use it instead of insert or upsert, and a worked Python import.

Zoho CRM Bulk Write API guide: upload a zipped CSV, create the job, check status and read results. Covers the 25,000-record and 500-credit limits.
TL;DR: The Zoho CRM Bulk Write API inserts, updates or upserts large record sets asynchronously. Upload a zipped CSV, create a job with the file ID, module and operation, check its status, then download a result file showing each row's outcome. Per Zoho's documentation, one CSV holds up to 25,000 records and bulk writes don't trigger workflows.
Key takeaways
- Bulk Write = zipped CSV → job → status → result file.
- Up to 25,000 records per CSV, 200 columns (Zoho docs).
- 500 credits per job, success or not.
- Workflows don't fire; run automation separately.
- Use find_by for upserts on a unique field.
Loading tens of thousands of records into Zoho CRM one API call at a time is slow, consumes a lot of API credits and is prone to partial failures halfway through. For large imports and periodic mass updates, the Zoho CRM Bulk Write API offers a different model: you upload a zipped CSV file, start an asynchronous job and download a result file that tells you exactly what happened to every row. This guide explains how Bulk Write works, its documented limits, how it compares with the standard insert and upsert APIs, and how to design a dependable import around it, with a worked example in Python.
What is the Zoho CRM Bulk Write API?
According to Zoho's Bulk Write API overview, a bulk write follows five steps:
- Prepare a CSV file whose first row contains field API names, with one record per row, all for the same module, and compress it into a ZIP file
- Upload the ZIP file to Zoho's upload endpoint, which returns a file_id
- Create a bulk write job with the file_id, the module, the operation (insert, update or upsert) and optional field mappings and a callback URL
- Wait for completion, either through the callback or by checking the job status, which moves through ADDED, IN PROGRESS and COMPLETED
- Download the result, a zipped CSV containing the first mapped columns plus the record ID, status and error for every row
Because the job runs in the background, your code does not hold a connection open while thousands of records are processed.
What are the Zoho CRM Bulk Write API limits?
Zoho's Bulk Write limitations page lists the constraints that shape any design:
LimitValueRecords per CSV file25,000Column headers per file200ZIP file size for modules such as Leads and ContactsOne CSV file, up to 25 MBFiles per bulk write requestOne zipped fileCredits per job500, deducted regardless of the job's outcomeWorkflowsNot triggered by bulk writeUnsupported dataAttachments, notes, file and image uploads, formula and auto-number fields, among others
Two of these matter more than they first appear. Workflows do not run, so any automation that normally fires on record creation, such as assignment emails or field updates, will not happen and must be handled separately. And each job costs 500 credits whether it succeeds or not, so small jobs are expensive.
Related reading: Zoho CRM API limits.
Should you use Bulk Write or the standard insert/upsert API?
SituationBetter choiceReasonA few records at a time, in real timeInsert or upsert APIsImmediate response, workflows runUp to a few thousand records occasionallyUpsert API in batches of 100Simple, and fewer credits than a bulk job for small volumesTens of thousands of recordsBulk WriteOne job per 25,000 records, row-level resultsImports that must trigger workflowsStandard APIs, or Bulk Write followed by separate processingBulk Write does not run workflowsOngoing sync of changesStandard APIs or scheduled syncBulk jobs suit batches, not trickles
The standard insert and upsert APIs accept up to 100 records per call. As a rough comparison, 25,000 records through upsert means 250 calls, while Bulk Write needs a single job. Whether that saves credits depends on your edition's credit rules, so check the numbers for your account before choosing.
Related reading: Zoho CRM mass update options.
How do you upsert with find_by and lookups?
For update and upsert jobs, the job definition must include find_by: the API name of a unique field, or the record ID, used to match rows to existing records. For inserts, find_by is optional; if you include it, rows that match existing records are skipped rather than duplicated. Lookup fields use column headers such as Account_Name.id when the file contains record IDs. The ignore_empty option controls whether blank cells overwrite existing values or are skipped, which matters greatly for partial updates.
How do you import 60,000 contacts with the Bulk Write API?
Suppose a company is consolidating contacts from an events platform into Zoho CRM: 60,000 rows, matched on email, which is a unique field in Contacts. The script splits the export into files of 25,000 rows, zips each, uploads it, creates an upsert job and waits for the result. It is illustrative and untested; tokens, the organisation ID and field names are placeholders.
import csv, io, time, zipfile, requests
API = "https://www.zohoapis.com" # use the api_domain returned with your token
UPLOAD = "https://upload.zoho.com/crm/v8/upload"
def zip_rows(header, rows):
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(header)
writer.writerows(rows)
out = io.BytesIO()
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("contacts.csv", buf.getvalue())
return out.getvalue()
def run_job(token, org_id, zipped):
auth = {"Authorization": f"Zoho-oauthtoken {token}"}
upload_headers = dict(auth)
upload_headers.update({"feature": "bulk-write", "X-CRM-ORG": org_id})
up = requests.post(UPLOAD, headers=upload_headers,
files={"file": ("contacts.zip", zipped)}, timeout=60)
file_id = up.json()["details"]["file_id"]
job = requests.post(f"{API}/crm/bulk/v8/write", headers=auth, timeout=30, json={
"operation": "upsert",
"ignore_empty": True,
"resource": [{"type": "data", "module": {"api_name": "Contacts"},
"file_id": file_id, "find_by": "Email"}],
})
job_id = job.json()["details"]["id"]
while True:
time.sleep(30)
status = requests.get(f"{API}/crm/bulk/v8/write/{job_id}", headers=auth, timeout=30).json()
if status.get("status") == "COMPLETED":
return status["result"]["download_url"]
Each file becomes one job, so the 60,000 contacts need three jobs: two of 25,000 and one of 10,000. In production, add a callback URL instead of polling, a timeout for jobs that never complete and handling for failed uploads. The download URL may need the same authorisation header; check the download result documentation for your version.
After each job, the result file is checked row by row:
JobRowsAddedUpdatedErrorsMain error cause125,0009,40015,480120Invalid email format225,00011,05013,88070Missing last name310,0004,2005,76040Invalid email format
The 230 failed rows are fixed in a spreadsheet and resubmitted through the standard upsert API, which is cheaper than another bulk job for so few records. These figures are hypothetical, but checking every result file is not optional; without it, errors stay invisible.
Related reading: CRM migration services.
Designing a dependable bulk import
- Validate the CSV before uploading: required fields, formats, picklist values and file size
- Keep the source row number in a column, so result rows can be traced back
- Handle workflow side effects separately, since bulk write does not trigger them
- Run a small trial job in a sandbox first to confirm mappings
- Schedule large jobs outside peak hours, and record job IDs, counts and credits used
- Store result files for audit
Think about record ownership too. Because bulk write does not run workflows, do not rely on automation to set owners afterwards. Include an Owner column, or update ownership in a separate step, and confirm in a sandbox trial who owns the imported records, so they do not all land with the integration user.
Bulk Write is the counterpart of Bulk Read for exports; our guide to the Zoho CRM Bulk Read API covers that side. For large updates driven from inside the CRM, our guide to Zoho CRM mass update explains how batch workers keep them fast.
Frequently asked questions
What is the Zoho CRM Bulk Write API?
It is Zoho CRM's asynchronous API for inserting, updating or upserting large numbers of records from a zipped CSV file. Your code starts a job and later reads a result file instead of holding a connection open.
How many records can one bulk write job handle?
Zoho's Bulk Write limitations page lists up to 25,000 records per CSV file, up to 200 column headers, and one zipped file per request. Larger loads are split across several jobs.
How many credits does a bulk write job use?
Per Zoho's documentation, each job deducts 500 API credits, regardless of whether it succeeds.
Does Bulk Write trigger workflows?
No. Zoho's documentation states workflows are not triggered by bulk write, so any automation you rely on must be run separately after the import.
When should we use the upsert API instead?
Use the standard insert or upsert APIs for small, frequent or real-time changes. Use Bulk Write for large, periodic imports and mass updates where a per-row result file matters.
Plan your bulk imports with us
We design bulk imports and mass updates for Zoho CRM with validation, result checking and safe retries. See our Zoho CRM automation services, or talk to us about your data loads.
Related reading: CRM API integration.
About the Author
Manav Sharma is the founder of Insyrge and a CRM solutions architect. He works across Zoho, Salesforce, HubSpot, Odoo and more, designing the CRM setups, integrations and automations that keep sales and operations teams working from the same data.
Need Help Implementing This in Your Business?
Our certified Zoho consultants and automation experts can help you design and deploy custom workflows tailored to your operations.
Book Free Consultation