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
- Why Connect Zoho CRM With UPS?
- How the UPS API Authentication Works
- Prerequisites and Setup Checklist
- Step 1 — Getting an OAuth Access Token
- Step 2 — Building the Shipment Request
- Step 3 — Understanding the Shipment Response
- Full Deluge Code Reference
- Common Errors and How to Fix Them
- What to Build Next
- 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.
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:
- You send your Client ID and Client Secret to the UPS token endpoint, encoded as a Base64 string in the Authorization header.
- UPS validates your credentials and returns a temporary access token (valid for a limited time, usually a few hours).
- You include that access token as a Bearer token in the Authorization header of your shipment request.
- 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.
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
invokeurlcalls to work, Zoho may require you to whitelist the UPS domain. Set this up under Setup → Developer Space → Connections if needed.
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
shiprestricts 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.
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. |
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:
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
ShipmentIdentificationNumberfrom 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
GraphicImagein the response is the label in Base64. Decode it withzoho.encryption.base64Decodeand 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.