For Zoho Services only:

I'm actually part of a bigger team at Ascent Business Solutions where we have support technicians and project consultants. Support is for smaller technical fixes but this can include developments, reports or integrations; depending on the size of the task. Projects are for more time-consuming developments such as revamps of the Zoho Suite of apps or on-site training. The advantage of a team is that if I am out-of-office for a day or so, there is always someone at Ascent Business Solutions who can deal with any queries/issues you may have.

Our support rates can be found and purchased at A support bundle doesn't have an expiry date. So whether we can do what you want within the bundle and a year later need further support, if there are minutes left on the bundle then there is no additional charge.

Our project rates for bigger developments can be found at and will involve a dedicated project consultant along with developers who will hold your hand through the development process.

If you want help building a solution for one of the Zoho Apps in the Zoho Suite, contact us on 0121 293 8140 (UK) or by email at You can also visit our website at

I regularly build and specialize in 2-way API integrations for Xero, Shopify and eBay.

ZohoCRM & Xero Real-Time Invoices: Receive Webhook

So this is an article expanding on my article Zoho Deluge - Connect to Xero API and explores how instead of a schedule, we can get Xero to tell ZohoCRM whenever an invoice or contact gets updated in Xero.

I used to use ZohoCRM schedules to pull when an invoice has been paid, but ZohoCRM schedules run only every 2 hours. You can set up 2 schedules: one that runs on even hours and one that runs on odd hours to change this to every hour. It's not enough for some, so the below details on how you can get Xero to tell Zoho when a contact or invoice has been updated immediately.

Here's an overview of how we achieve this:
  1. Set up a Zoho CRM function to receive the webhook.
  2. Setup a webhook in Xero.
  3. Setup a PHP script on a server that will validate the Xero webhook.
Yes, you're going to need an external server to host a file that can respond to Xero with HTTP statuses as their webhook validation process is a little stricter than Zoho's.

Setup a Zoho CRM Function:
  1. Login to Zoho CRM > Setup > Functions > New Function
  2. Function Name: fn_API_ReceiveXeroWebhook
  3. Display Name: FN - Xero - Receive Webhook
  4. Description: Function used to receive webhooks from Xero and parse these into data for invoices and contacts.
  5. Category: Standalone
  6. Click on Create
  7. Give it the parameter: crmAPIRequest (string)
  8. Dump in the following code:
    m_Payload = crmAPIRequest;
    // change the "to" to your own email and whatever subject you want
    	from: zoho.adminuserid
    	to: "This email address is being protected from spambots. You need JavaScript enabled to view it."
    	subject: "Xero Webhook for MyClient"
    	message: m_Payload
    return "";
    1.  m_Payload = crmAPIRequest; 
    2.  // change the "to" to your own email and whatever subject you want 
    3.  sendmail 
    4.  [ 
    5.      from: zoho.adminuserid 
    6.      to: "This email address is being protected from spambots. You need JavaScript enabled to view it." 
    7.      subject: "Xero Webhook for MyClient" 
    8.      message: m_Payload 
    9.  ] 
    10.  return ""
  9. Save this and return to the list of functions
  10. Hover over the function you just created, click on the 3 dots/ellipsis and select "REST API"
  11. Switch on the API Key and copy the URL to clipboard or a text editor
  12. Click on "Save"
Setup a Xero webhook:
  1. Login to the page and click on "My Apps".
  2. Select the app you are going to setup a webhook for (see my Zoho Deluge - Connect to Xero API article for setting up an app)
  3. Select "Webhooks" in the left sidebar menu
  4. Tick "Contacts" and "Invoices" and enter the Delivery URL as the API Key you noted earlier:
    ZohoCRM & Xero Real-Time Invoices: Receive Webhook : Setup in Xero
  5. Click on "Save"
  6. Click on "Send 'Intent to receive'"
  7. If you setup the above function correctly, you should receive an email with a bunch of JSON data:

    however... you won't receive contact/invoice updates until you complete the next step

  8. Complete webhook setup by passing 'Intent to receive' required stage.
    ZohoCRM & Xero Real-Time Invoices: Intent to Receive REQUIRED
    There are several issues with doing this, one is that there will always be a "body" in the response to Xero from ZohoCRM, something in the form of:
      "code": "success",
      "details": {
        "output": "",
        "userMessage": [
        "output_type": "string",
        "id": "123456789012345678"
      "message": "function executed successfully"
    1.  { 
    2.    "code": "success", 
    3.    "details": { 
    4.      "output": "", 
    5.      "userMessage": [ 
    6.        null 
    7.      ], 
    8.      "output_type": "string", 
    9.      "id": "123456789012345678" 
    10.    }, 
    11.    "message": "function executed successfully" 
    12.  } 
    Another is that we can't customize the response to Xero from ZohoCRM. And lastly, you might think a script that responds 200 will then mean you can change it after... not so. And just to top it off, Xero will test both a valid and invalid signed webhook.

    So I'm going to cheat, by setting up a PHP script on a server we own to return a 200 status:

    1. Have a hosting server ready which has SSL installed.
    2. Add a PHP script with the following code (change the webhooks key to your own - and REST API function URL):
      // enter the webhooks key for your app (in Xero under Manage App > Webhooks)
      $v_Xero_Webhooks_Key = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHI+111122223333444455556666777788889/a1b2c3d4e5f6g7h8i9==";
      // get the headers
      $a_Headers = getallheaders();
      $v_Xero_Signature = $a_Headers[ 'x-xero-signature' ];
      // get the body
      $j_Body = @file_get_contents( "php://input" );
      // compute a sha256 hash (HMACSHA256) using the body and your secret key
      $v_Hashed_Payload = base64_encode( hash_hmac( "sha256", $j_Body, $v_Xero_Webhooks_Key, true) );
      // timing attack safe string comparison
      if ( hash_equals( $v_Xero_Signature, $v_Hashed_Payload ) ){
          // return response to Xero
          header( "HTTP/1.1 200 OK" );
          http_response_code( 200 );
          // took me a few hours to work out this code and get Xero to approve of this webhook so let's now push the data to Zoho
          $v_Zoho_Url = "<YOUR_ZOHO_API_KEY>";
          // build and send via cURL
          $ch = curl_init();
          curl_setopt( $ch, CURLOPT_HEADER, 0 );
          curl_setopt( $ch, CURLOPT_URL, $v_Zoho_Url );
          curl_setopt( $ch, CURLOPT_POST, 1 );
          curl_setopt( $ch, CURLOPT_POSTFIELDS, $j_Body );
          curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
          $r_Zoho_Response = curl_exec( $ch );
          curl_close( $ch );
          // return response to Xero (used as Xero will test both a valid and invalid request)
          header('HTTP/1.1 401 Unauthorized');
      1.  <?php 
      3.  // enter the webhooks key for your app (in Xero under Manage App > Webhooks) 
      4.  $v_Xero_Webhooks_Key = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHI+111122223333444455556666777788889/a1b2c3d4e5f6g7h8i9=="
      6.  // get the headers 
      7.  $a_Headers = getallheaders()
      8.  $v_Xero_Signature = $a_Headers[ 'x-xero-signature' ]
      10.  // get the body 
      11.  $j_Body = @file_get_contents( "php://input" )
      13.  // compute a sha256 hash (HMACSHA256) using the body and your secret key 
      14.  $v_Hashed_Payload = base64_encode( hash_hmac( "sha256", $j_Body, $v_Xero_Webhooks_Key, true) )
      16.  // timing attack safe string comparison 
      17.  if ( hash_equals( $v_Xero_Signature, $v_Hashed_Payload ) ){ 
      19.      // return response to Xero 
      20.      header( "HTTP/1.1 200 OK" )
      21.      http_response_code( 200 )
      23.      // took me a few hours to work out this code and get Xero to approve of this webhook so let's now push the data to Zoho 
      24.      $v_Zoho_Url = "<YOUR_ZOHO_API_KEY>"
      26.      // build and send via cURL 
      27.      $ch = curl_init()
      28.      curl_setopt( $ch, CURLOPT_HEADER, 0 )
      29.      curl_setopt( $ch, CURLOPT_URL, $v_Zoho_Url )
      30.      curl_setopt( $ch, CURLOPT_POST, 1 )
      31.      curl_setopt( $ch, CURLOPT_POSTFIELDS, $j_Body )
      32.      curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true )
      33.      $r_Zoho_Response = curl_exec( $ch )
      34.      curl_close( $ch )
      36.  }else{ 
      38.      // return response to Xero (used as Xero will test both a valid and invalid request) 
      39.      header('HTTP/1.1 401 Unauthorized')
      40.      http_response_code(401)
      42.  } 
      43.  ?> 
    3. Change the Delivery URL to the location of this PHP script and click on "Save"
    4. Click on "Send 'intent to receive' and Xero will test both a valid and invalid signed webhook.
    5. You should get an OK status:
      ZohoCRM & Xero Real-Time Invoices: Status OK
  9. Now return to your ZohoCRM function and here's the code I'm using for this:
    // init
    m_Blank = Map();
    m_Payload = crmAPIRequest;
    m_Body = ifnull(m_Payload.get("body"),m_Blank);
    // check if events was in the payload
    	// parse events out of the webhook (a list)
    	l_Events = m_Body.get("events").toJSONList();
    	// loop through the events
    	for each r_Event in l_Events
    			// event type will be UPDATE and eventCategory will be our module
    			v_Event = r_Event.get("eventCategory") + " " + r_Event.get("eventType");
    			// get the xero ID of the record (hexadecimal)
    			v_Xero_ResourceID = r_Event.get("resourceId");
    			// do different things based on the category of the update
    				// get contact record from Xero to see what changed...
    			else if(r_Event.get("eventCategory").containsIgnoreCase("INVOICE"))
    				// get invoice record from Xero to see what changed...
    return "";
    1.  // 
    2.  // init 
    3.  m_Blank = Map()
    4.  m_Payload = crmAPIRequest; 
    5.  m_Body = ifnull(m_Payload.get("body"),m_Blank)
    6.  // 
    7.  // check if events was in the payload 
    8.  if(!isnull(m_Body.get("events"))) 
    9.  { 
    10.      // 
    11.      // parse events out of the webhook (a list) 
    12.      l_Events = m_Body.get("events").toJSONList()
    13.      // 
    14.      // loop through the events 
    15.      for each r_Event in l_Events 
    16.      { 
    17.          if(!isnull(r_Event.get("eventCategory"))) 
    18.          { 
    19.              // 
    20.              // event type will be UPDATE and eventCategory will be our module 
    21.              v_Event = r_Event.get("eventCategory") + " " + r_Event.get("eventType")
    22.              // 
    23.              // get the xero ID of the record (hexadecimal) 
    24.              v_Xero_ResourceID = r_Event.get("resourceId")
    25.              // 
    26.              // do different things based on the category of the update 
    27.              if(r_Event.get("eventCategory").containsIgnoreCase("CONTACT")) 
    28.              { 
    29.                  // get contact record from Xero to see what changed... 
    30.              } 
    31.              else if(r_Event.get("eventCategory").containsIgnoreCase("INVOICE")) 
    32.              { 
    33.                  // get invoice record from Xero to see what changed... 
    34.              } 
    35.          } 
    36.      } 
    37.  } 
    38.  return ""
  10. You can test this by updating a contact or draft invoice record.

Update 2022:
Still to be tested - Zoho were adamant that this would not be possible using CRM but since I have found documentation that might allow it in Zoho. I will test with a Demo Company and document this below:
response = Map();
return {"crmAPIResponse":response};
  1.  response = Map()
  2.  response.put("status_code",204)
  3.  return {"crmAPIResponse":response}

Additional Note(s):
  • You should have a field (single-line) called Xero Ref ID on both the contact/invoice module for referring to so as to push/pull deltas.
  • After all that work, you only get the ID of a record that was changed. Here's a sample response:
      "events": [
          "resourceUrl": "",
          "resourceId": "aaaabbbb-1111-cccc-2222-d3e4f5d3e4f5",
          "eventDateUtc": "2022-01-10T17:53:48.886",
          "eventType": "UPDATE",
          "eventCategory": "CONTACT",
          "tenantId": "99998888-ffff-7777-eeee-d6e5f4d6e5f4",
          "tenantType": "ORGANISATION"
      "firstEventSequence": 5,
      "lastEventSequence": 5,
    1.  { 
    2.    "events": [ 
    3.      { 
    4.        "resourceUrl": "", 
    5.        "resourceId": "aaaabbbb-1111-cccc-2222-d3e4f5d3e4f5", 
    6.        "eventDateUtc": "2022-01-10T17:53:48.886", 
    7.        "eventType": "UPDATE", 
    8.        "eventCategory": "CONTACT", 
    9.        "tenantId": "99998888-ffff-7777-eeee-d6e5f4d6e5f4", 
    10.        "tenantType": "ORGANISATION" 
    11.      } 
    12.    ], 
    13.    "firstEventSequence": 5, 
    14.    "lastEventSequence": 5, 
    15.    "entropy": "ABCDEFGHIJKLMNOPQRST" 
    16.  } 

Category: Zoho :: Article: 794

Credit where Credit is Due:

Feel free to copy, redistribute and share this information. All that we ask is that you attribute credit and possibly even a link back to this website as it really helps in our search engine rankings.

Disclaimer: The information on this website is provided without warranty and any content is merely the opinion of the author. Please try to test in development environments prior to adapting them to your production environments. The articles are written in good faith and, at the time of print, are working examples used in a commercial setting.

Thank you for visiting and, as always, we hope this website was of some use to you!

Kind Regards,

Joel Lipman

Related Articles

Joes Revolver Map

Joes Word Cloud


Badge - Certified Zoho Creator Associate
Badge - Certified Zoho Creator Associate

Donate & Support

If you like my content, and would like to support this sharing site, feel free to donate using a method below:

Donate to Joel Lipman via PayPal

Donate to Joel Lipman with Bitcoin - Valid till 8 May 2022 3QnhmaBX7LQSRsC9hh6Je9rGQKEGNQNfPb
© 2022 Joel Lipman .com. All Rights Reserved.