The Problem
Every sales team eventually needs to push leads from an external source — a landing page, a support ticket, a third-party form — directly into Zoho CRM without manual data entry. The Zoho API makes this possible, but getting your first successful request through is a wall of friction: OAuth 2.0 token generation, data center region mismatches, scope configuration, and a JSON body structure that Zoho silently rejects if formatted incorrectly. Most developers burn hours on a INVALID_TOKEN or INVALID_DATA error with no clear explanation. This tutorial cuts through that. You will generate a working OAuth access token, construct a correctly shaped cURL request, and create a real lead record in Zoho CRM — all from the terminal — with every gotcha documented so you do not hit them.
Tech Stack & Prerequisites
- Zoho CRM account — Free tier works (sign up at zoho.com/crm)
- Zoho Developer Console access — to register an OAuth client
- cURL 7.68+ — pre-installed on macOS/Linux; Windows users use WSL or Git Bash
- A REST client — Postman (optional, for Step 4 verification)
- A text editor — to store tokens and IDs temporarily
- Your Zoho data center region —
.com/.eu/.in/.com.au/.jp(critical — covered in Step 1)
Step-by-Step Implementation
Step 1: Register Your OAuth Client in Zoho
Zoho uses OAuth 2.0 for all API calls. You need a Client ID and Client Secret before making any request.
1.1 — Go to the Zoho API Console
Navigate to: https://api-console.zoho.com
Log in with the same account that owns your CRM.
1.2 — Create a new client
- Click “Add Client”
- Choose “Self Client” (best for server-to-server / terminal use)
- Click “Create”
You will immediately see your:
Client ID— looks like1000.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXClient Secret— looks likeabc123...
Save both in a scratch file. Do not share these.
1.3 — Generate a Grant Token
Still in the Self Client screen:
- Click the “Generate Code” tab
- In the Scope field, paste exactly:
ZohoCRM.modules.leads.CREATE,ZohoCRM.modules.leads.READ
- Set Time Duration to
10 minutes - Home Domain: select your data center (e.g.,
zoho.comfor US accounts) - Click “Create” — copy the one-time Grant Token immediately
⚠️ The grant token expires in 10 minutes and can only be exchanged once.
Step 2: Exchange the Grant Token for Access & Refresh Tokens
This is a one-time step. You exchange the short-lived grant token for a long-lived refresh token and a short-lived access token (valid 60 minutes).
Replace the placeholders and run this cURL command in your terminal:
curl -X POST "https://accounts.zoho.com/oauth/v2/token" \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "redirect_uri=https://www.zoho.com" \
-d "code=YOUR_GRANT_TOKEN"
EU accounts: replace
accounts.zoho.comwithaccounts.zoho.euIN accounts: useaccounts.zoho.in— mismatching regions is the #1 cause ofINVALID_CODE
Expected response:
{
"access_token": "1000.abc123...xyz",
"refresh_token": "1000.def456...uvw",
"token_type": "Bearer",
"expires_in": 3600
}
Store both tokens securely. Save them to a local .env file:
.env
ZOHO_ACCESS_TOKEN=1000.abc123...xyz
ZOHO_REFRESH_TOKEN=1000.def456...uvw
ZOHO_CLIENT_ID=1000.XXXXXXXXXXXXXXXXXXXXXX
ZOHO_CLIENT_SECRET=your_client_secret_here
ZOHO_REGION=com
Never commit this file. Add it to
.gitignoreimmediately:bashecho ".env" >> .gitignore
Step 3: Create a Lead via cURL
With a valid access token, you can now POST to the Leads module.
3a — The Core cURL Command
curl -X POST "https://www.zohoapis.com/crm/v6/Leads" \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"Last_Name": "Reyes",
"First_Name": "Marco",
"Email": "marco.reyes@example.com",
"Phone": "+1-415-555-0192",
"Company": "Stackline Inc.",
"Lead_Source": "Web Site",
"Description": "Submitted via API integration test"
}
]
}'
EU accounts: replace
zohoapis.comwithzohoapis.eu
Expected success response:
{
"data": [
{
"code": "SUCCESS",
"details": {
"Created_Time": "2025-06-15T10:22:41+00:00",
"Modified_Time": "2025-06-15T10:22:41+00:00",
"id": "5545974000000478001"
},
"message": "record added",
"status": "success"
}
]
}
Save the returned id — it is your new lead’s record ID in Zoho CRM.
3b — Refresh Your Access Token (when it expires after 60 min)
Access tokens expire. Use your refresh token to get a new one without re-authenticating:
curl -X POST "https://accounts.zoho.com/oauth/v2/token" \
-d "grant_type=refresh_token" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "refresh_token=YOUR_REFRESH_TOKEN"
Response:
{
"access_token": "1000.newtoken...abc",
"token_type": "Bearer",
"expires_in": 3600
}
Update your .env with the new access_token and re-run Step 3a.
Step 4: Verify the Lead Was Created
4a — Fetch the lead by ID via cURL
curl -X GET "https://www.zohoapis.com/crm/v6/Leads/YOUR_LEAD_ID" \
-H "Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN"
Expected response (trimmed):
{
"data": [
{
"id": "5545974000000478001",
"Last_Name": "Reyes",
"First_Name": "Marco",
"Email": "marco.reyes@example.com",
"Company": "Stackline Inc.",
"Lead_Source": "Web Site"
}
]
}
4b — Verify inside Zoho CRM UI
- Log in to Zoho CRM → click Leads in the top nav
- Sort by Created Time (Descending)
- You should see Marco Reyes at the top of the list
4c — Test in Postman (optional)
- Method:
POST - URL:
https://www.zohoapis.com/crm/v6/Leads - Headers:
Authorization:Zoho-oauthtoken YOUR_ACCESS_TOKENContent-Type:application/json
- Body: raw JSON — paste the
datapayload from Step 3a - Send → confirm
"status": "success"in the response panel
Common Errors & Troubleshooting
Gotcha 1 — "code": "INVALID_TOKEN" on every request
This means your access token is expired, malformed, or from the wrong region.
Fix:
- Confirm the token starts with
1000.— if not, it was not generated correctly - Check that your API endpoint region matches your account region. A US token hitting
zohoapis.euwill always fail - Re-run the token refresh command from Step 3b and replace the token in your request
Gotcha 2 — "code": "MANDATORY_NOT_FOUND" with status "error"
Zoho requires Last_Name for every lead. No exceptions. This error fires if it is missing, empty, or the key is spelled incorrectly (it is case-sensitive).
Fix:
# ❌ Wrong — missing Last_Name or wrong casing
-d '{ "data": [{ "Lastname": "Reyes" }] }'
# ✅ Correct — exact field name required
-d '{ "data": [{ "Last_Name": "Reyes" }] }'
Also check: Lead_Source must match a value from your CRM picklist exactly (e.g., "Web Site" not "Website").
Gotcha 3 — "code": "INVALID_DATA" but the JSON looks correct
This usually means the data key is missing its array wrapper, or the JSON body has a trailing comma that makes it invalid.
Fix: Validate your JSON before sending:
# Pipe your JSON through Python's JSON validator
echo '{
"data": [
{ "Last_Name": "Reyes", "Company": "Stackline Inc." }
]
}' | python3 -m json.tool
If it prints the formatted JSON, it is valid. If it throws a JSONDecodeError, fix the syntax first. Also confirm Content-Type: application/json is set — without it, Zoho may parse the body as form data and reject it.
Security Checklist
- Never hardcode tokens in scripts — load them from
.envusingos.getenv()(Python) orprocess.env(Node.js) - Add
.envto.gitignorebefore the first commit — a leaked Client Secret requires full OAuth client rotation - Use minimum required scopes — only grant
ZohoCRM.modules.leads.CREATEif you do not need read access - Rotate access tokens on a schedule — automate the refresh flow; do not store long-lived access tokens in plain text
- Restrict your Self Client by IP if Zoho adds that option in your console — limits token misuse if a secret leaks
- Audit API usage in Zoho CRM under Setup → Developer Space → API Usage — watch for unexpected call spikes
- Store the refresh token in a secrets manager (AWS Secrets Manager, HashiCorp Vault, or GitHub Secrets for CI/CD) — never in a
.envfile on a shared or production server

Muhammad Abdullah is a SaaS systems researcher and CRM workflow specialist focused on practical implementation and scalable software configuration. He writes in-depth guides on CRM architecture, automation logic, API integrations, and data organization strategies for modern SaaS platforms.His work helps teams understand how CRM systems function internally from custom field structuring to workflow engineering and secure integrations. Abdullah’s goal is to simplify complex SaaS concepts into clear, implementation-ready documentation.

