Let's Talk Run the Diagnostic Diagnostic

Zoho CRM Functions as a REST API: Catch Webhooks

Any standalone function in Zoho CRM can become a REST endpoint. Turn on its REST API option, and Zoho gives you a URL that other systems can post to: a form tool, a website, a payment processor, a calling platform. The function reads the request through crmAPIRequest, does its work in Deluge, and can send back a proper status code with crmAPIResponse. No middleware, no server of your own.

We use this on most of our API integration projects that push data into Zoho CRM. This guide covers the setup and three patterns from real builds: a webhook router, a callback that finds its way back to the right record, and a catcher for jobs that finish later.

Diagram of a Zoho CRM function used as a webhook endpoint: an outside system posts to the function's URL with an API key or OAuth token, the function reads crmAPIRequest, routes the event to the right parser function, and returns crmAPIResponse with a status code

Turning a function into an endpoint

From Zoho's guide to exposing functions as REST APIs:

  1. Write the function as a standalone function in Setup > Developer Hub > Functions. Zoho recommends standalone functions for REST endpoints.
  2. Open the function, go to the Overview tab, and find the REST API section.
  3. Choose how callers authenticate. Zoho generates a unique endpoint URL for the function.
  4. Give that URL to the system that will call it.

The endpoint accepts GET, POST, PUT, and DELETE.

API key or OAuth?

API key OAuth 2.0
How the caller proves itself Holding the URL A Zoho access token in the Authorization header
Token refresh None The caller refreshes expired tokens
Fits Inbound webhooks from tools that can only post to a URL Partner systems and your own apps that can run an OAuth flow

With an API key, the URL is the secret. Zoho's guide says anyone with it can invoke the function, so keep it out of front-end code, shared documents, and public repositories. For webhooks, we add a second check: most senders can include a shared secret in a header or field, and the function rejects any request that doesn't carry it.

Reading the request

Everything the caller sent arrives in crmAPIRequest:

Key Contains
method GET, POST, PUT, or DELETE
headers The request headers
params Query string parameters
body The raw body, usually JSON
auth_type How the caller authenticated
file_content An uploaded file, if there is one
user_info The authenticated user and organization

A minimal webhook catcher:

//
// WEBHOOK CATCHER: CHECK THE SHARED SECRET, THEN READ THE PAYLOAD
//
headersMp = ifnull(crmAPIRequest.getJSON("headers"),Map());
if(headersMp.getJSON("x-webhook-secret") != "REPLACE_WITH_YOUR_SECRET")
{
	respMp = Map();
	respMp.put("status_code",401);
	respMp.put("body","unauthorized");
	return {"crmAPIResponse":respMp};
}
bodyMp = crmAPIRequest.getJSON("body");
eventStr = ifnull(bodyMp.getJSON("event"),"");
info "EVENT RECEIVED: " + eventStr;

Log the raw body with info while you're building. The function's logs in Developer Hub show exactly what the sender posts, which is often not quite what its documentation says.

Sending a response back

By default the endpoint returns 200 with whatever the function returns. When the caller needs a specific status, return a map under crmAPIResponse:

//
// TELL THE CALLER WHAT HAPPENED: 201 ON CREATE, 400 ON BAD INPUT
//
respMp = Map();
if(emailStr = "")
{
	respMp.put("status_code",400);
	respMp.put("body",{"error":"email is required"}.toString());
	return {"crmAPIResponse":respMp};
}
createResp = zoho.crm.createRecord("Leads",leadMp);
respMp.put("status_code",201);
respMp.put("body",{"lead_id":createResp.getJSON("id")}.toString());
return {"crmAPIResponse":respMp};

The keys are status_code (a number), body (a string), and optionally headers and content-type, which defaults to JSON. A value of the wrong type returns a PATTERN_NOT_MATCHED error, so convert maps to strings with toString() before you put them in body.

Status codes matter more than they look. Most webhook senders retry when they get an error, so a 400 for a payload you'll never accept stops pointless retries, and a 200 tells the sender to move on.

Pattern 1: one endpoint, many event types

A website that sends products, orders, customers, courses, and tickets doesn't need five endpoints. On one integration between a WordPress site and Zoho CRM, a single endpoint reads the object type and hands the payload to a parser function written for that type:

//
// WEBHOOK ROUTER: ONE ENDPOINT, ONE PARSER FUNCTION PER OBJECT TYPE
//
dataMp = crmAPIRequest.getJSON("body");
objectStr = ifnull(dataMp.getJSON("data_object"),"");
if(objectStr.containsIgnoreCase("product"))
{
	standalone.catch_parse_wp_product(dataMp);
}
else if(objectStr.containsIgnoreCase("order"))
{
	standalone.catch_parse_wp_order(dataMp);
}
else if(objectStr.containsIgnoreCase("customer"))
{
	standalone.catch_parse_wp_customer(dataMp);
}
else if(objectStr.containsIgnoreCase("course"))
{
	standalone.catch_parse_wp_course(dataMp);
}
return "";

The router stays short and never changes when a parser does. Each parser is a normal standalone function you can test on its own by passing it a sample payload, and adding a new event type is one more branch. Order matters with containsIgnoreCase: test the more specific names first, so "product_variant" reaches its own parser before the "product" branch catches it.

Diagram of a webhook router: WordPress posts every event to one Zoho CRM function, which reads data_object and calls a separate standalone parser function for products, orders, customers, courses, and tickets

Pattern 2: send the CRM ID out, get it back in the callback

Many services work asynchronously. You ask them to do something, such as sending a ringless voicemail, and they post the result to your webhook minutes later. The hard part is knowing which CRM record that result belongs to.

The answer is to send the CRM record IDs out with the request. Most services have a metadata, reference, or "foreign ID" field that they echo back in every webhook. On one build that sends voicemails from Zoho CRM, the outbound request carries the contact's ID and the related record's ID, each with a zcrm_ prefix. When the delivery result comes back, the catcher strips the prefix and knows exactly which records to update:

//
// DELIVERY CALLBACK: THE IDS WE SENT OUT COME BACK IN foreign_id
//
bodyMp = crmAPIRequest.getJSON("body");
contactIdStr = bodyMp.getJSON("foreign_id").getJSON("contact_id").replaceAll("zcrm_","");
resourceIdStr = bodyMp.getJSON("foreign_id").getJSON("resource_id").replaceAll("zcrm_","");
statusStr = ifnull(bodyMp.getJSON("status"),"");
// LOG THE OUTCOME AS A CALL ON THE CONTACT, LINKED TO THE RELATED RECORD
callMp = Map();
callMp.put("Subject","Voicemail " + statusStr);
callMp.put("Who_Id",contactIdStr);
callMp.put("$se_module","Resources");
callMp.put("What_Id",resourceIdStr);
callMp.put("Call_Type","Outbound");
// ISO 8601 WITH YOUR ORG'S UTC OFFSET, E.G. -04:00 FOR EASTERN DAYLIGHT TIME
callMp.put("Call_Start_Time",zoho.currenttime.toString("yyyy-MM-dd'T'HH:mm:ss") + utcOffsetStr);
createResp = zoho.crm.createRecord("Calls",callMp);
info createResp;

No searching by phone number, no guessing. The prefix also makes the IDs easy to spot in the other system's logs and stops them being mistaken for that system's own IDs.

Diagram of a foreign ID round trip: Zoho CRM sends a request with zcrm_ prefixed contact and record IDs, the outside service does the work and posts a callback with the same foreign_id, and the catcher function strips the prefix and updates the right records

Pattern 3: catch a job that finishes later

Some APIs take a while: big exports, file conversions, data enrichment. Instead of polling, give them the function's URL as a callback. Zoho Analytics works this way. Its asynchronous bulk export accepts a callbackUrl, and Zoho posts the job's details there when the export finishes or fails.

On a build that syncs a large advisor database from Zoho Analytics into Zoho CRM, a scheduled function starts the export with the CRM function's URL as the callback. When the export is ready, the callback receives a body that includes the downloadUrl, fetches the file, and syncs the rows:

//
// ANALYTICS EXPORT CALLBACK: FETCH THE FINISHED FILE FROM THE downloadUrl
//
bodyMp = crmAPIRequest.getJSON("body");
downloadUrlStr = bodyMp.getJSON("downloadUrl");
zaHeaderMp = Map();
zaHeaderMp.put("ZANALYTICS-ORGID",zaOrgIdStr);
exportResp = invokeurl
[
	url :downloadUrlStr
	type :GET
	headers:zaHeaderMp
	connection:"zanalytics"
];
rowsLst = ifnull(exportResp.getJSON("data"),List());
info "ROWS TO SYNC: " + rowsLst.size();

Watch the clock here. Zoho's function limits give functions called through the REST API 10 seconds, while scheduled functions get 15 minutes. When a callback could bring in more rows than you can process in 10 seconds, keep the catcher thin: save the download URL to a record and return. A scheduled function then picks up the saved URL and does the heavy lifting with time to spare.

Things to get right

  • Make catchers safe to run twice. Senders retry. Before creating a record, check whether one with the sender's event or order ID already exists, or use an upsert.
  • Return fast. Do the minimum while the sender waits, and move slow work to a schedule.
  • Check a shared secret on API-key endpoints, as shown above.
  • Remember the quota. Calls to function endpoints count toward the organization's daily API limit, according to Zoho's guide.
  • Keep one place to look. Log each payload and outcome, either with info or to a log module, so "did we get that webhook?" has an answer.

When the function needs to call out to other systems as part of handling the webhook, our invokeUrl guide covers the outbound side, including error handling and retries. And if the incoming data needs cleaning before it lands in CRM, the dedupe pattern in our Deluge tips post applies here too.

Connecting your systems to Zoho CRM

A function endpoint is often all it takes to get a website, payment tool, or calling platform talking to Zoho CRM. If you have a system that needs to push data in, or callbacks that need to land on the right records, that's what our API integration and Zoho CRM work covers.

Frequently asked questions

Can a Zoho CRM function receive webhooks?

Yes. Open a standalone function in Setup > Developer Hub > Functions, turn on its REST API option, and Zoho generates an endpoint URL. Other systems post to that URL, and the function reads the request through crmAPIRequest.

What is the difference between API key and OAuth for a CRM function endpoint?

With API key, the generated URL is the credential, so anyone who has it can call the function. That suits inbound webhooks. With OAuth 2.0, the caller sends a Zoho access token in the Authorization header, which Zoho recommends for production integrations.

What is in crmAPIRequest?

The HTTP method, headers, query parameters, the raw body, the authentication type, any uploaded file content, and details about the authenticated user and organization.

How do I return a custom status code from a Zoho CRM function?

Return a map under the key crmAPIResponse with status_code, body, and optionally headers and content-type. Without it, the function returns its normal output with a 200 status.

How long can a Zoho CRM REST API function run?

Zoho's function limits give functions called through the REST API 10 seconds. For heavier work, have the endpoint save what it received and let a scheduled function, which gets 15 minutes, do the processing.

Free Zoho Game Plan

Got a system that needs to push data into 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