How to Generate Access token and Automate UPS Shipping from Zoho CRM Using Deluge (Step-by-Step)

How to Automate UPS Shipping from Zoho CRM Using Deluge (Step-by-Step)

Introduction

A step-by-step developer guide for connecting Zoho CRM to the UPS Shipping API using Deluge scripting — covering OAuth token generation, shipment creation, and label retrieval, all without leaving your CRM.

In This Article

  1. Why Connect Zoho CRM With UPS?
  2. How the UPS API Authentication Works
  3. Prerequisites and Setup Checklist
  4. Step 1 — Getting an OAuth Access Token
  5. Step 2 — Building the Shipment Request
  6. Step 3 — Understanding the Shipment Response
  7. Full Deluge Code Reference
  8. Common Errors and How to Fix Them
  9. What to Build Next
  10. Conclusion

1. Why Connect Zoho CRM With UPS?

If your business ships physical goods, you already know the friction that comes with switching between systems. A sales rep closes a deal in Zoho CRM, then someone else logs into a separate shipping platform, manually enters the recipient details, generates a label, and pastes a tracking number back into the CRM. It works — but it is slow, repetitive, and just waiting for a copy-paste error to cause a delivery to go to the wrong address.

The better approach is to let Zoho CRM talk directly to UPS through their shipping API. Once connected, you can trigger a shipment, generate a label, and save the tracking number to a CRM record — all from a single button click or an automated workflow. No switching tabs, no manual data entry, no mistakes from copying addresses by hand.

This guide walks through exactly how to do that using Deluge, Zoho CRM's built-in scripting language. The integration covers two steps: authenticating with the UPS API using OAuth 2.0, and then submitting a shipment request and handling the response. By the end, you will have working code you can drop into a Zoho function and customise for your own shipment workflow.

ⓘ What is the UPS Shipping API? UPS provides a REST API called the Shipments API (part of their Developer Kit) that lets you programmatically create shipments, generate shipping labels, calculate rates, and track packages. The API uses OAuth 2.0 for authentication, meaning you first obtain a temporary access token, then use that token for subsequent requests.

2. How the UPS API Authentication Works

Before you can submit a shipment, you need to prove to UPS that your application is authorised to use their API. They use the OAuth 2.0 client credentials flow for this — the same standard used by most modern APIs including Google, Salesforce, and Microsoft.

Here is how the process works at a high level:

  1. You send your Client ID and Client Secret to the UPS token endpoint, encoded as a Base64 string in the Authorization header.
  2. UPS validates your credentials and returns a temporary access token (valid for a limited time, usually a few hours).
  3. You include that access token as a Bearer token in the Authorization header of your shipment request.
  4. UPS processes the shipment and returns a response containing the shipment details and label data.

The reason for this two-step approach is security. Your actual Client Secret never travels with the shipment request — only the short-lived token does. If that token were ever intercepted, it would expire quickly on its own.

⚠ UPS test vs. production environment The code in this guide uses the UPS production endpoint (onlinetools.ups.com). UPS also provides a Customer Integration Environment (CIE) at wwwcie.ups.com for testing. Always develop and test against the CIE before switching to production credentials.

3. Prerequisites and Setup Checklist

Get these in place before writing a single line of code. Missing any one of them will cause the function to fail, often with a cryptic error that is hard to trace back to the root cause.

  • UPS Developer Account — Register at developer.ups.com and create an application. This gives you your Client ID, Client Secret, and access to your Shipper Account Number.
  • UPS Shipper Account Number — This is the six-character account number tied to your UPS billing account (e.g., 0Y15W6). It appears in shipment requests as the payer account and the shipper identifier.
  • Merchant ID — A UPS-issued identifier required as a custom header (x-merchant-id) on both the token and shipment requests. You get this from your UPS developer portal.
  • Zoho CRM with function access — You need access to Zoho CRM's developer console (Setup → Developer Space → Functions) where you can write and execute Deluge code.
  • A Zoho Connection configured — For the invokeurl calls to work, Zoho may require you to whitelist the UPS domain. Set this up under Setup → Developer Space → Connections if needed.
 Store credentials securely Never hardcode your Client Secret or Shipper Number directly in a function that other CRM users can view. Use Zoho CRM's built-in Custom Variables (under Setup → Developer Space → Variables) to store sensitive values. Reference them in Deluge using zoho.adminuserurl or a secure variable fetch. This guide uses plain variables for readability — replace them with secure references in production.

4. Step 1 — Getting an OAuth Access Token

The first thing the function needs to do is request an access token from UPS. This is a POST request to the UPS token endpoint, authenticated using your Client ID and Client Secret encoded together as a Base64 string.

Here is how that looks in Deluge:

// Your UPS API credentials
client_id = "YOUR_CLIENT_ID_HERE";
client_secret = "YOUR_CLIENT_SECRET_HERE";
token_url = "https://onlinetools.ups.com/security/v1/oauth/token";

// Combine and Base64-encode credentials for Basic Auth
auth_str = client_id + ":" + client_secret;
encodedAuth = zoho.encryption.base64Encode(auth_str);

// Build the request headers
headers_token = Map();
headers_token.put("Content-Type", "application/x-www-form-urlencoded");
headers_token.put("x-merchant-id", "YOUR_MERCHANT_ID");
headers_token.put("Authorization", "Basic " + encodedAuth);

// Body parameters required by the OAuth client_credentials flow
bodyParams = Map();
bodyParams.put("grant_type", "client_credentials");
bodyParams.put("scope", "ship");

// Make the POST request to get the token
token_response = invokeurl
[
    url : token_url
    type : POST
    parameters : bodyParams
    headers : headers_token
];

info "Token Response: " + token_response;
access_token = token_response.get("access_token");
info "Access Token: " + access_token;

What Each Part Does

Breaking this down so it is clear what each block is responsible for:

  • zoho.encryption.base64Encode — Zoho's built-in Base64 encoder. The UPS token endpoint expects your credentials in the format clientId:clientSecret, Base64-encoded, passed as a Basic Authorization header. This line handles that encoding automatically.
  • Content-Type: application/x-www-form-urlencoded — The token endpoint expects form-encoded body parameters, not JSON. This is a common source of confusion. If you send JSON here, UPS will return a 400 error.
  • grant_type: client_credentials — Tells UPS you are using the machine-to-machine OAuth flow, where no user login is involved. The scope value ship restricts the token to shipment-related operations.
  • x-merchant-id header — Required on every request to UPS, including the token request. Omitting it causes authentication to fail even if your credentials are correct.
  • token_response.get("access_token") — The response from UPS is a JSON object. This line extracts just the token string you will need for the next step.
⚠ Token expiry UPS access tokens expire after a set period (typically 14,400 seconds — about 4 hours). In a production function, you may want to cache the token and check its expiry time before requesting a new one on every run. For low-volume workflows, requesting a fresh token each time is simpler and perfectly acceptable.

5. Step 2 — Building and Submitting the Shipment Request

With a valid access token in hand, the next step is to construct the shipment payload and send it to the UPS Shipments API. This is where you define who is shipping, who is receiving, what service to use, package dimensions and weight, label format, and any notifications.

The shipment request is a deeply nested JSON object. Here is the complete Deluge code for this step:

// Shipment API endpoint (production)
shipment_url = "https://onlinetools.ups.com/api/shipments/v1/ship?additionaladdressvalidation=city";

// Headers for the shipment request — Bearer token goes here
headers_ship = Map();
headers_ship.put("Content-Type", "application/json");
headers_ship.put("Authorization", "Bearer " + access_token);
headers_ship.put("x-merchant-id", "YOUR_MERCHANT_ID");
headers_ship.put("transId", "e2b7d510-29cb-4c4d-8ab0-3d8a92876c91");
headers_ship.put("transactionSrc", "ZOHOCRM");

// Build the shipment payload
shipment_body = {
  "ShipmentRequest": {
    "Shipment": {
      "Description": "Inbound Karger",
      "Shipper": {
        "Name": "Smart Service",
        "AttentionName": "Smart Servicios",
        "TaxIdentificationNumber": "456789",
        "Phone": {"Number": "+34 971 571 044"},
        "ShipperNumber": "0Y15W6",
        "Address": {
          "AddressLine": "Font i Monteros 6 2a planta",
          "City": "Palma de Mallorca",
          "StateProvinceCode": "ES",
          "PostalCode": "07003",
          "CountryCode": "ES"
        }
      },
      "ReturnService": {"Code": "9"},
      "ShipTo": {
        "Name": "Smart Service",
        "AttentionName": "Smart Servicios",
        "Phone": {"Number": "+34 971 571 044"},
        "TaxIdentificationNumber": "456999",
        "Address": {
          "AddressLine": "Font i Monteros 6 2a planta",
          "City": "Palma de Mallorca",
          "StateProvinceCode": "ES",
          "PostalCode": "07003",
          "CountryCode": "ES"
        }
      },
      "ShipFrom": {
        "Name": "Familie Pascal Karger",
        "AttentionName": "Test Company",
        "Phone": {"Number": "+41764752096"},
        "TaxIdentificationNumber": "456999",
        "Address": {
          "AddressLine": ["Am Hinkeln 6"],
          "City": "Schwerte",
          "PostalCode": "58239",
          "CountryCode": "DE"
        }
      },
      "PaymentInformation": {
        "ShipmentCharge": {
          "Type": "01",
          "BillShipper": {"AccountNumber": "0Y15W6"}
        }
      },
      "Service": {"Code": "07", "Description": "UPS Express"},
      "Package": [{
        "Description": "Inbound Karger",
        "Packaging": {"Code": "01"},
        "PackageWeight": {
          "UnitOfMeasurement": {"Code": "KGS"},
          "Weight": ".3"
        }
      }],
      "ShipmentServiceOptions": {
        "Notification": {
          "NotificationCode": "2",
          "EMail": {
            "EMailAddress": "ups@smart-servicios.com"
          }
        }
      }
    },
    "LabelSpecification": {
      "LabelImageFormat": {"Code": "GIF"}
    }
  }
};

// Send the shipment request
shipment_response = invokeurl
[
    url : shipment_url
    type : POST
    parameters : shipment_body.toString()
    headers : headers_ship
];

info "Shipment Response: " + shipment_response;

Key Fields Explained

The shipment JSON has a lot of fields. Here is what each major section is actually doing:

Field / Section What It Controls
Shipper The UPS account holder sending the shipment. ShipperNumber must match your UPS account.
ShipTo The recipient of the package. In production, pull this from the CRM record dynamically.
ShipFrom The physical origin of the package. Can differ from the Shipper if a third-party sender is used.
Service.Code "07" = UPS Express Worldwide. Other common codes: "11" = Standard, "65" = UPS Saver. Check UPS docs for a full list.
ReturnService.Code "9" creates a return label. Remove this block entirely if you do not need a return label.
Packaging.Code "01" = UPS Letter. "02" = Customer Supplied Package. "03" = Tube. Use "02" for most standard box shipments.
LabelImageFormat The format of the shipping label returned in the response. Options: GIF, PNG, PDF, ZPL (for thermal printers).
transId header A unique transaction ID you generate per request. Used by UPS for support and debugging. Use a UUID or generate one dynamically.
 Making it dynamic In a real CRM workflow, you would replace the hardcoded address and contact values with variables pulled from the CRM record. For example: shipTo_name = record.get("Contact_Name"); and then reference shipTo_name inside the JSON body. This is what turns a static proof-of-concept into a fully automated shipping workflow.

6. Step 3 — Understanding the Shipment Response

When UPS successfully processes a shipment, the response contains several useful pieces of information. Knowing what to look for in the response is important because this is where you extract the tracking number to save back to the CRM record.

The response structure looks roughly like this (simplified):

{
  "ShipmentResponse": {
    "Response": {
      "ResponseStatus": {
        "Code": "1",
        "Description": "Success"
      }
    },
    "ShipmentResults": {
      "ShipmentIdentificationNumber": "1Z0Y15W60123456789",
      "PackageResults": {
        "TrackingNumber": "1Z0Y15W60123456789",
        "ShippingLabel": {
          "ImageFormat": {"Code": "GIF"},
          "GraphicImage": "<base64-encoded label data>"
        }
      }
    }
  }
}

To extract the tracking number and save it back to the CRM record, add this Deluge code after your shipment request:

// Navigate the response to extract the tracking number
shipment_results = shipment_response
    .get("ShipmentResponse")
    .get("ShipmentResults");

tracking_number = shipment_results.get("ShipmentIdentificationNumber");
info "Tracking Number: " + tracking_number;

// Save the tracking number to a field on your CRM record
// Replace "YOUR_MODULE", "YOUR_RECORD_ID", and "Tracking_Number" with your actual values
update_resp = zoho.crm.updateRecord(
    "YOUR_MODULE",
    "YOUR_RECORD_ID",
    {"Tracking_Number": tracking_number}
);
info "CRM Update: " + update_resp;

The GraphicImage field inside ShippingLabel contains the shipping label as a Base64-encoded image string. You can decode this and attach it to the CRM record as a file, or email it directly to the relevant contact using Zoho's built-in mail functions.

7. Full Deluge Code Reference

Here is the complete function combining both steps — token request followed by shipment creation — so you have a single block to work from:

// ============================================================
// STEP 1: GET OAUTH ACCESS TOKEN FROM UPS
// ============================================================

client_id = "YOUR_CLIENT_ID_HERE";
client_secret = "YOUR_CLIENT_SECRET_HERE";
token_url = "https://onlinetools.ups.com/security/v1/oauth/token";

auth_str = client_id + ":" + client_secret;
encodedAuth = zoho.encryption.base64Encode(auth_str);

headers_token = Map();
headers_token.put("Content-Type", "application/x-www-form-urlencoded");
headers_token.put("x-merchant-id", "YOUR_MERCHANT_ID");
headers_token.put("Authorization", "Basic " + encodedAuth);

bodyParams = Map();
bodyParams.put("grant_type", "client_credentials");
bodyParams.put("scope", "ship");

token_response = invokeurl
[
    url : token_url
    type : POST
    parameters : bodyParams
    headers : headers_token
];

access_token = token_response.get("access_token");
info "Access Token: " + access_token;

// ============================================================
// STEP 2: CREATE SHIPMENT
// ============================================================

shipment_url = "https://onlinetools.ups.com/api/shipments/v1/ship?additionaladdressvalidation=city";

headers_ship = Map();
headers_ship.put("Content-Type", "application/json");
headers_ship.put("Authorization", "Bearer " + access_token);
headers_ship.put("x-merchant-id", "YOUR_MERCHANT_ID");
headers_ship.put("transId", "e2b7d510-29cb-4c4d-8ab0-3d8a92876c91");
headers_ship.put("transactionSrc", "ZOHOCRM");

shipment_body = {"ShipmentRequest":{"Shipment":{"Description":"Inbound Karger","Shipper":{"Name":"Smart Service","AttentionName":"Smart Servicios","TaxIdentificationNumber":"456789","Phone":{"Number":"+34 971 571 044"},"ShipperNumber":"0Y15W6","Address":{"AddressLine":"Font i Monteros 6 2a planta","City":"Palma de Mallorca","StateProvinceCode":"ES","PostalCode":"07003","CountryCode":"ES"}},"ReturnService":{"Code":"9"},"ShipTo":{"Name":"Smart Service","AttentionName":"Smart Servicios","Phone":{"Number":"+34 971 571 044"},"TaxIdentificationNumber":"456999","Address":{"AddressLine":"Font i Monteros 6 2a planta","City":"Palma de Mallorca","StateProvinceCode":"ES","PostalCode":"07003","CountryCode":"ES"}},"ShipFrom":{"Name":"Familie Pascal Karger","AttentionName":"Test Company","Phone":{"Number":"+41764752096"},"TaxIdentificationNumber":"456999","Address":{"AddressLine":["Am Hinkeln 6"],"City":"Schwerte","PostalCode":"58239","CountryCode":"DE"}},"PaymentInformation":{"ShipmentCharge":{"Type":"01","BillShipper":{"AccountNumber":"0Y15W6"}}},"Service":{"Code":"07","Description":"UPS Express"},"Package":[{"Description":"Inbound Karger","Packaging":{"Code":"01"},"PackageWeight":{"UnitOfMeasurement":{"Code":"KGS"},"Weight":".3"}}],"ShipmentServiceOptions":{"Notification":{"NotificationCode":"2","EMail":{"EMailAddress":"ups@smart-servicios.com"}}},"ItemizedChargesRequestedIndicator":"","RatingMethodRequestedIndicator":"","TaxInformationIndicator":"","ShipmentRatingOptions":{"NegotiatedRatesIndicator":""}},"LabelSpecification":{"LabelImageFormat":{"Code":"GIF"}}}};

shipment_response = invokeurl
[
    url : shipment_url
    type : POST
    parameters : shipment_body.toString()
    headers : headers_ship
];

info "Shipment Response: " + shipment_response;

8. Common Errors and How to Fix Them

The UPS API returns descriptive error messages, but the cause is not always obvious. Here are the errors that come up most often when building this integration for the first time:

 401 Unauthorized on the token request
Your Client ID and Client Secret combination is wrong, or the Base64 encoding is malformed. Double-check that you are concatenating them as clientId + ":" + clientSecret before encoding — the colon separator is required by the OAuth spec. Also verify there are no extra spaces around your credentials.
 Missing or invalid x-merchant-id header
The x-merchant-id header is required on both the token request and the shipment request. If you include it on one but forget the other, you will get an unexpected 400 or 401 response. Make sure it is present in both headers_token and headers_ship.
 ShipperNumber does not match the authenticated account
The ShipperNumber in the shipment body and the AccountNumber under BillShipper must both match the UPS account associated with your Client ID. Using a different account number here causes UPS to reject the request with a billing or authorisation error.
 access_token is null after the token request
This usually means the token request itself failed and the response does not contain a valid token. Add info token_response; immediately after the invokeurl call to see the full raw response and identify the actual error message UPS returned.
 Invalid address — PostalCode and CountryCode mismatch
UPS validates addresses on the server side. If the postal code does not match the country code, or the city name does not match the postal code, the shipment will be rejected. The additionaladdressvalidation=city query parameter in the URL makes this validation stricter. Remove it during testing if you want to skip city-level validation temporarily.

9. What to Build Next

Once the basic shipment creation is working, there are several natural next steps that make the integration significantly more useful in day-to-day operations:

  • Pull address data from CRM fields dynamically. Instead of hardcoding the ShipTo address, fetch it from the Deal or Contact record that triggers the function. This makes every shipment unique with no manual input required.
  • Save the tracking number back to the CRM record. After a successful shipment, extract the ShipmentIdentificationNumber from the response and update the Deal or Order record with it so the whole team can track from within CRM.
  • Attach the shipping label as a file. The GraphicImage in the response is the label in Base64. Decode it with zoho.encryption.base64Decode and attach it to the record using Zoho's file API.
  • Add error handling. Wrap the shipment call in a check for the response status code. If UPS returns an error, log it to a custom Notes field on the record rather than silently failing.
  • Build a rate comparison step. Before creating the shipment, call the UPS Rating API to get quotes for different service levels and present them to the user before committing to a specific service code.

10. Conclusion

Connecting Zoho CRM to UPS through Deluge is one of those integrations that looks intimidating on paper but follows a very logical pattern once you break it down. Step one gets you a token. Step two uses that token to create a shipment. Everything else — extracting tracking numbers, attaching labels, making the data dynamic — is just building on top of those two foundations.

The code in this guide is production-tested and covers the most common configuration for international shipments. Your implementation will naturally look different — different module names, different address fields, potentially different UPS service codes — but the structure stays the same regardless of those specifics.

If you run into an issue the troubleshooting section does not cover, the most useful thing you can do is log the full raw response from both the token call and the shipment call using info statements. UPS error messages are generally descriptive enough to point you in the right direction. And if you have a working variation of this setup or ran into an error not mentioned here, drop it in the comments — it will likely save someone else a frustrating afternoon.

Post a Comment