Upload Images from Zoho Creator to Cloudinary with Deluge

Cloudinary Image Management from Zoho Creator

Creator stores images perfectly well, but it will not resize them, crop to a face, convert to WebP or serve them from a CDN. Cloudinary does all of that from the URL. Upload once from Deluge and every variant you ever need is a query string away.

Good news for once — this API has not moved. The endpoint api.cloudinary.com/v1_1/{cloud_name}/image/upload is unchanged, unsigned uploads with a whitelisted preset still work, and SHA-1 is still the default signature algorithm (SHA-256 is opt-in). Signing code written in 2016 is still correct. The updates here are Deluge-side: getUrl/postUrl replaced with invokeurl, and the file fetched through an OAuth connection rather than a report private link.

Before you start

  • A Cloudinary account — cloud name, API key and API secret from the dashboard.
  • A Zoho OAuth connection named creator_oauth_connection to pull the file out of Creator.
  • Fields for Cloudinary_URL and Cloudinary_PublicId. Store the public id — it is what lets you delete or transform the asset later.

Deluge function

// ============================================
// UPLOAD A CREATOR FILE TO CLOUDINARY (signed)
// ============================================

void Updates.uploadToCloudinary(int recordId)
{
    CLOUD_NAME = "YOUR_CLOUD_NAME";
    API_KEY    = "YOUR_CLOUDINARY_API_KEY";
    API_SECRET = "YOUR_CLOUDINARY_API_SECRET";

    rec = Photos[ID == recordId];

    // 1. Fetch the file out of Creator
    imageFile = invokeurl
    [
        url  : "https://creatorapp.zohopublic.com/YOUR_ACCOUNT/YOUR_APP/report/Photos/"
               + recordId + "/Photo/download"
        type : GET
        connection : "creator_oauth_connection"
    ];

    // 2. Build the signature.
    //    Sign EVERY param you send except file, cloud_name, resource_type
    //    and api_key. Sort alphabetically, join with &, then append the
    //    secret directly with no separator.
    timestamp = zoho.currenttime.toString("yyyy-MM-dd HH:mm:ss").toTime().getTime() / 1000;
    folder    = "creator-uploads";

    toSign    = "folder=" + folder + "&timestamp=" + timestamp;
    signature = zoho.encryption.sha1(toSign + API_SECRET);

    // 3. Upload
    params = Map();
    params.put("api_key",API_KEY);
    params.put("timestamp",timestamp);
    params.put("folder",folder);
    params.put("signature",signature);

    response = invokeurl
    [
        url    : "https://api.cloudinary.com/v1_1/" + CLOUD_NAME + "/image/upload"
        type   : POST
        parameters : params
        files  : imageFile
    ];

    secureUrl = response.getJSON("secure_url");
    if(secureUrl != null)
    {
        rec.Cloudinary_URL      = secureUrl;
        rec.Cloudinary_PublicId = response.getJSON("public_id");
        info "Uploaded: " + secureUrl;
    }
    else
    {
        // On a bad signature Cloudinary echoes the exact string it expected.
        info "Cloudinary error: " + response.toString();
    }
}

The signature is where this goes wrong

Almost every failed Cloudinary integration is a signature problem, and the rule is precise: take every parameter you are sending except file, cloud_name, resource_type and api_key; write them as name=value; sort them alphabetically by name; join with &; then append your API secret with no separator at all. Hash the result.

Cloudinary will tell you the answer. When a signature is rejected the error echoes back the exact string it expected you to sign — something like String to sign - 'folder=creator-uploads&timestamp=1000000000'. Log the full response and compare it against what you built. This turns an afternoon of guessing into two minutes.

Notes

  • Do not store resized copies. Cloudinary transforms on the fly from the URL — inserting /w_300,h_300,c_fill/ into the path gives you a thumbnail with no second upload and no second record. Storing variants is the most common way people overspend here.
  • Unsigned uploads are simpler but public. An upload preset lets you skip the signature entirely, which is fine for a public form and wrong for anything else — anyone who finds the preset name can upload to your account.
  • Timestamps are seconds, not milliseconds. Cloudinary rejects a request whose timestamp is more than an hour from its clock, and passing milliseconds is the usual cause.
  • Keep the public id. Without it you cannot delete the asset programmatically, and orphaned images are what quietly consume a free tier.

This script is part of the free Creator Scripts Deluge Library.

All 39 Deluge scripts, the full Zoho Creator course, and every downloadable asset are now free. Get free access →