diff --git a/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java b/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java
index 5826743e..f0bbbbe7 100644
--- a/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java
+++ b/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java
@@ -1,5 +1,7 @@
 package com.cloudinary;
 
+import com.cloudinary.api.signing.ApiResponseSignatureVerifier;
+import com.cloudinary.api.signing.NotificationRequestSignatureVerifier;
 import com.cloudinary.strategies.AbstractApiStrategy;
 import com.cloudinary.strategies.AbstractUploaderStrategy;
 import com.cloudinary.strategies.StrategyLoader;
@@ -8,8 +10,6 @@
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.*;
 
@@ -127,27 +127,46 @@ public String signedPreloadedImage(Map result) {
     }
 
     public String apiSignRequest(Map<String, Object> paramsToSign, String apiSecret) {
-        Collection<String> params = new ArrayList<String>();
-        for (Map.Entry<String, Object> param : new TreeMap<String, Object>(paramsToSign).entrySet()) {
-            if (param.getValue() instanceof Collection) {
-                params.add(param.getKey() + "=" + StringUtils.join((Collection) param.getValue(), ","));
-            } else if (param.getValue() instanceof Object[]) {
-                params.add(param.getKey() + "=" + StringUtils.join((Object[]) param.getValue(), ","));
-            } else {
-                if (StringUtils.isNotBlank(param.getValue())) {
-                    params.add(param.getKey() + "=" + param.getValue().toString());
-                }
-            }
-        }
-        String to_sign = StringUtils.join(params, "&");
-        MessageDigest md = null;
-        try {
-            md = MessageDigest.getInstance("SHA-1");
-        } catch (NoSuchAlgorithmException e) {
-            throw new RuntimeException("Unexpected exception", e);
-        }
-        byte[] digest = md.digest(getUTF8Bytes(to_sign + apiSecret));
-        return StringUtils.encodeHexString(digest);
+        return Util.produceSignature(paramsToSign, apiSecret);
+    }
+
+    /**
+     * Verifies that Cloudinary notification request is genuine by checking its signature.
+     *
+     * Cloudinary can asynchronously process your e.g. image uploads requests. This is achieved by calling back API you
+     * specified during preparing of upload request as soon as it has been processed. See Upload Notifications in
+     * Cloudinary documentation for more details. In order to make sure it is Cloudinary calling your API back, hashed
+     * message authentication codes (HMAC's) based on SHA-1 hashing function and configured Cloudinary API secret key
+     * are used for signing the requests.
+     *
+     * The following method serves as a convenient utility to perform the verification procedure.
+     *
+     * @param body Cloudinary Notification request body represented as string
+     * @param timestamp Cloudinary Notification request custom X-Cld-Timestamp HTTP header value
+     * @param signature Cloudinary Notification request custom X-Cld-Signature HTTP header value, i.e. the HMAC
+     * @param validFor desired period of request validity since issued, in seconds, for protection against replay attacks
+     * @return whether request signature is valid or not
+     */
+    public boolean verifyNotificationSignature(String body, String timestamp, String signature, long validFor) {
+        return new NotificationRequestSignatureVerifier(config.apiSecret).verifySignature(body, timestamp, signature, validFor);
+    }
+
+    /**
+     * Verifies that Cloudinary API response is genuine by checking its signature.
+     *
+     * Cloudinary can add a signature value in the response to API methods returning public id's and versions. In order
+     * to make sure it is genuine Cloudinary response, hashed message authentication codes (HMAC's) based on SHA-1 hashing
+     * function and configured Cloudinary API secret key are used for signing the responses.
+     *
+     * The following method serves as a convenient utility to perform the verification procedure.
+     *
+     * @param publicId publicId response field value
+     * @param version version response field value
+     * @param signature signature response field value, i.e. the HMAC
+     * @return whether response signature is valid or not
+     */
+    public boolean verifyApiResponseSignature(String publicId, String version, String signature) {
+        return new ApiResponseSignatureVerifier(config.apiSecret).verifySignature(publicId, version, signature);
     }
 
     public void signRequest(Map<String, Object> params, Map<String, Object> options) {
@@ -236,14 +255,6 @@ private String buildUrl(String base, Map<String, Object> params) throws Unsuppor
         return urlBuilder.toString();
     }
 
-    byte[] getUTF8Bytes(String string) {
-        try {
-            return string.getBytes("UTF-8");
-        } catch (java.io.UnsupportedEncodingException e) {
-            throw new RuntimeException("Unexpected exception", e);
-        }
-    }
-
     @Deprecated
     public static Map asMap(Object... values) {
         return ObjectUtils.asMap(values);
diff --git a/cloudinary-core/src/main/java/com/cloudinary/Url.java b/cloudinary-core/src/main/java/com/cloudinary/Url.java
index 17a95d1c..24bfdce1 100644
--- a/cloudinary-core/src/main/java/com/cloudinary/Url.java
+++ b/cloudinary-core/src/main/java/com/cloudinary/Url.java
@@ -393,7 +393,7 @@ public String generate(String source) {
             toSign = StringUtils.removeStartingChars(toSign, '/');
             toSign = StringUtils.mergeSlashesInUrl(toSign);
 
-            byte[] digest = md.digest(cloudinary.getUTF8Bytes(toSign + this.config.apiSecret));
+            byte[] digest = md.digest(Util.getUTF8Bytes(toSign + this.config.apiSecret));
             signature = Base64Coder.encodeURLSafeString(digest);
             signature = "s--" + signature.substring(0, 8) + "--";
         }
@@ -536,7 +536,7 @@ public String unsignedDownloadUrlPrefix(String source, String cloudName, boolean
 
     private String shard(String input) {
         CRC32 crc32 = new CRC32();
-        crc32.update(cloudinary.getUTF8Bytes(input));
+        crc32.update(Util.getUTF8Bytes(input));
         return String.valueOf((crc32.getValue() % 5 + 5) % 5 + 1);
     }
 
diff --git a/cloudinary-core/src/main/java/com/cloudinary/Util.java b/cloudinary-core/src/main/java/com/cloudinary/Util.java
index a3af8833..8c4eb7c0 100644
--- a/cloudinary-core/src/main/java/com/cloudinary/Util.java
+++ b/cloudinary-core/src/main/java/com/cloudinary/Util.java
@@ -4,6 +4,8 @@
 import com.cloudinary.utils.StringUtils;
 import org.cloudinary.json.JSONObject;
 
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.*;
 
 public class Util {
@@ -226,4 +228,55 @@ private static void putArray(String name, Map from, Map<String, Object> to) {
     protected static String timestamp() {
         return Long.toString(System.currentTimeMillis() / 1000L);
     }
+
+    /**
+     * Encodes passed string value into a sequence of bytes using the UTF-8 charset.
+     *
+     * @param string string value to encode
+     * @return byte array representing passed string value
+     */
+    public static byte[] getUTF8Bytes(String string) {
+        try {
+            return string.getBytes("UTF-8");
+        } catch (java.io.UnsupportedEncodingException e) {
+            throw new RuntimeException("Unexpected exception", e);
+        }
+    }
+
+    /**
+     * Calculates signature, or hashed message authentication code (HMAC) of provided parameters name-value pairs and
+     * secret value using SHA-1 hashing algorithm.
+     * <p>
+     * Argument for hashing function is built by joining sorted parameter name-value pairs into single string in the
+     * same fashion as HTTP GET method uses, and concatenating the result with secret value in the end. Method supports
+     * arrays/collections as parameter values. In this case, the elements of array/collection are joined into single
+     * comma-delimited string prior to inclusion into the result.
+     *
+     * @param paramsToSign parameter name-value pairs list represented as instance of {@link Map}
+     * @param apiSecret    secret value
+     * @return hex-string representation of signature calculated based on provided parameters map and secret
+     */
+    public static String produceSignature(Map<String, Object> paramsToSign, String apiSecret) {
+        Collection<String> params = new ArrayList<String>();
+        for (Map.Entry<String, Object> param : new TreeMap<String, Object>(paramsToSign).entrySet()) {
+            if (param.getValue() instanceof Collection) {
+                params.add(param.getKey() + "=" + StringUtils.join((Collection) param.getValue(), ","));
+            } else if (param.getValue() instanceof Object[]) {
+                params.add(param.getKey() + "=" + StringUtils.join((Object[]) param.getValue(), ","));
+            } else {
+                if (StringUtils.isNotBlank(param.getValue())) {
+                    params.add(param.getKey() + "=" + param.getValue().toString());
+                }
+            }
+        }
+        String to_sign = StringUtils.join(params, "&");
+        MessageDigest md = null;
+        try {
+            md = MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("Unexpected exception", e);
+        }
+        byte[] digest = md.digest(getUTF8Bytes(to_sign + apiSecret));
+        return StringUtils.encodeHexString(digest);
+    }
 }
diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/ApiResponseSignatureVerifier.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/ApiResponseSignatureVerifier.java
new file mode 100644
index 00000000..33224204
--- /dev/null
+++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/ApiResponseSignatureVerifier.java
@@ -0,0 +1,46 @@
+package com.cloudinary.api.signing;
+
+import com.cloudinary.Util;
+import com.cloudinary.utils.ObjectUtils;
+import com.cloudinary.utils.StringUtils;
+
+import static com.cloudinary.utils.StringUtils.emptyIfNull;
+
+/**
+ * The {@code ApiResponseSignatureVerifier} class is responsible for verifying Cloudinary Upload API response signatures.
+ */
+public class ApiResponseSignatureVerifier {
+    private final String secretKey;
+
+    /**
+     * Initializes new instance of {@code ApiResponseSignatureVerifier} class with a secret key required to perform
+     * API response signatures verification.
+     *
+     * @param secretKey shared secret key string which is used to sign and verify authenticity of API responses
+     */
+    public ApiResponseSignatureVerifier(String secretKey) {
+        if (StringUtils.isBlank(secretKey)) {
+            throw new IllegalArgumentException("Secret key is required");
+        }
+
+        this.secretKey = secretKey;
+    }
+
+    /**
+     * Checks whether particular Cloudinary Upload API response signature matches expected signature.
+     *
+     * The task is performed by generating signature using same hashing algorithm as used on Cloudinary servers and
+     * comparing the result with provided actual signature.
+     *
+     * @param publicId public id of uploaded resource as stated in upload API response
+     * @param version version of uploaded resource as stated in upload API response
+     * @param signature signature of upload API response, usually passed via X-Cld-Signature custom HTTP response header
+     *
+     * @return true if response signature passed verification procedure
+     */
+    public boolean verifySignature(String publicId, String version, String signature) {
+        return Util.produceSignature(ObjectUtils.asMap(
+                "public_id", emptyIfNull(publicId),
+                "version", emptyIfNull(version)), secretKey).equals(signature);
+    }
+}
diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifier.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifier.java
new file mode 100644
index 00000000..497e491e
--- /dev/null
+++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifier.java
@@ -0,0 +1,63 @@
+package com.cloudinary.api.signing;
+
+import static com.cloudinary.utils.StringUtils.emptyIfNull;
+
+/**
+ * The {@code NotificationRequestSignatureVerifier} class is responsible for verifying authenticity and integrity
+ * of Cloudinary Upload notifications.
+ */
+public class NotificationRequestSignatureVerifier {
+    private final SignedPayloadValidator signedPayloadValidator;
+
+    /**
+     * Initializes new instance of {@code NotificationRequestSignatureVerifier} with secret key value.
+     *
+     * @param secretKey shared secret key string which is used to sign and verify authenticity of notifications
+     */
+    public NotificationRequestSignatureVerifier(String secretKey) {
+        this.signedPayloadValidator = new SignedPayloadValidator(secretKey);
+    }
+
+    /**
+     * Verifies signature of Cloudinary Upload notification.
+     *
+     * @param body      notification message body, represented as string
+     * @param timestamp value of X-Cld-Timestamp custom HTTP header of notification message, representing notification
+     *                  issue timestamp
+     * @param signature actual signature value, usually passed via X-Cld-Signature custom HTTP header of notification
+     *                  message
+     * @return true if notification passed verification procedure
+     */
+    public boolean verifySignature(String body, String timestamp, String signature) {
+        return signedPayloadValidator.validateSignedPayload(
+                emptyIfNull(body) + emptyIfNull(timestamp),
+                signature);
+    }
+
+    /**
+     * Verifies signature of Cloudinary Upload notification.
+     * <p>
+     * Differs from {@link #verifySignature(String, String, String)} in additional validation which consists of making
+     * sure the notification being verified is still not expired based on timestamp parameter value.
+     *
+     * @param body            notification message body, represented as string
+     * @param timestamp       value of X-Cld-Timestamp custom HTTP header of notification message, representing notification
+     *                        issue timestamp
+     * @param signature       actual signature value, usually passed via X-Cld-Signature custom HTTP header of notification
+     *                        message
+     * @param secondsValidFor the amount of time, in seconds, the notification message is considered valid by client
+     * @return true if notification passed verification procedure
+     */
+    public boolean verifySignature(String body, String timestamp, String signature, long secondsValidFor) {
+        long parsedTimestamp;
+        try {
+            parsedTimestamp = Long.parseLong(timestamp);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("Provided timestamp is not a valid number", e);
+        }
+
+        return verifySignature(body, timestamp, signature) &&
+                (System.currentTimeMillis() - parsedTimestamp <= secondsValidFor * 1000L);
+    }
+
+}
diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/SignedPayloadValidator.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/SignedPayloadValidator.java
new file mode 100644
index 00000000..40339db6
--- /dev/null
+++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/SignedPayloadValidator.java
@@ -0,0 +1,39 @@
+package com.cloudinary.api.signing;
+
+import com.cloudinary.Util;
+import com.cloudinary.utils.StringUtils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import static com.cloudinary.utils.StringUtils.emptyIfNull;
+
+class SignedPayloadValidator {
+    private final String secretKey;
+    private final MessageDigest messageDigest;
+
+    SignedPayloadValidator(String secretKey) {
+        if (StringUtils.isBlank(secretKey)) {
+            throw new IllegalArgumentException("Secret key is required");
+        }
+
+        this.secretKey = secretKey;
+        this.messageDigest = acquireMessageDigest();
+    }
+
+    boolean validateSignedPayload(String signedPayload, String signature) {
+        String expectedSignature =
+                StringUtils.encodeHexString(
+                        messageDigest.digest(Util.getUTF8Bytes(emptyIfNull(signedPayload) + secretKey)));
+
+        return expectedSignature.equals(signature);
+    }
+
+    private static MessageDigest acquireMessageDigest() {
+        try {
+            return MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("Unexpected exception", e);
+        }
+    }
+}
diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/package-info.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/package-info.java
new file mode 100644
index 00000000..a95f5e74
--- /dev/null
+++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * The package holds classes used internally to implement verification procedures of authenticity and integrity of
+ * client communication with Cloudinary servers. Verification is in most cases based on calculating and comparing so called
+ * signatures, or hashed message authentication codes (HMAC) - string values calculated based on message payload, some
+ * secret key value shared between communicating parties and SHA-1 hashing function.
+ */
+package com.cloudinary.api.signing;
\ No newline at end of file
diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/StringUtils.java b/cloudinary-core/src/main/java/com/cloudinary/utils/StringUtils.java
index e3bec84b..f12a33cb 100644
--- a/cloudinary-core/src/main/java/com/cloudinary/utils/StringUtils.java
+++ b/cloudinary-core/src/main/java/com/cloudinary/utils/StringUtils.java
@@ -398,4 +398,14 @@ public static String mergeSlashesInUrl(String url) {
 
         return builder.toString();
     }
+
+    /**
+     * Returns empty string value when passed string value is null or empty, the passed string itself otherwise.
+     *
+     * @param str string value to evaluate
+     * @return passed string value or empty string, if the passed string is null or empty
+     */
+    public static String emptyIfNull(String str) {
+        return isEmpty(str) ? "" : str;
+    }
 }
diff --git a/cloudinary-core/src/test/java/com/cloudinary/api/signing/ApiResponseSignatureVerifierTest.java b/cloudinary-core/src/test/java/com/cloudinary/api/signing/ApiResponseSignatureVerifierTest.java
new file mode 100644
index 00000000..a198b8fc
--- /dev/null
+++ b/cloudinary-core/src/test/java/com/cloudinary/api/signing/ApiResponseSignatureVerifierTest.java
@@ -0,0 +1,26 @@
+package com.cloudinary.api.signing;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ApiResponseSignatureVerifierTest {
+    @Test
+    public void testVerifySignature() {
+        ApiResponseSignatureVerifier verifier = new ApiResponseSignatureVerifier("X7qLTrsES31MzxxkxPPA-pAGGfU");
+
+        boolean actual = verifier.verifySignature("tests/logo.png", "1", "08d3107a5b2ad82e7d82c0b972218fbf20b5b1e0");
+
+        assertTrue(actual);
+    }
+
+    @Test
+    public void testVerifySignatureFail() {
+        ApiResponseSignatureVerifier verifier = new ApiResponseSignatureVerifier("X7qLTrsES31MzxxkxPPA-pAGGfU");
+
+        boolean actual = verifier.verifySignature("tests/logo.png", "1", "doesNotMatchForSure");
+
+        assertFalse(actual);
+    }
+}
diff --git a/cloudinary-core/src/test/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifierTest.java b/cloudinary-core/src/test/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifierTest.java
new file mode 100644
index 00000000..c38bf47a
--- /dev/null
+++ b/cloudinary-core/src/test/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifierTest.java
@@ -0,0 +1,71 @@
+package com.cloudinary.api.signing;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class NotificationRequestSignatureVerifierTest {
+    @Test
+    public void testVerifySignature() {
+        NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret");
+
+        boolean actual = verifier.verifySignature(
+                "{}",
+                "0",
+                "f9aa4471d2a88ff244424cca2444edf7d7ac3596");
+
+        assertTrue(actual);
+    }
+
+    @Test
+    public void testVerifySignatureFailWhenSignatureDoesntMatch() {
+        NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret");
+
+        boolean actual = verifier.verifySignature(
+                "{}",
+                "0",
+                "notMatchingForSure");
+
+        assertFalse(actual);
+    }
+
+    @Test
+    public void testVerifySignatureFailWhenTooOld() {
+        NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret");
+
+        boolean actual = verifier.verifySignature(
+                "{}",
+                "0",
+                "f9aa4471d2a88ff244424cca2444edf7d7ac3596",
+                1000L);
+
+        assertFalse(actual);
+    }
+
+    @Test
+    public void testVerifySignaturePassWhenStillValid() {
+        NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret");
+
+        boolean actual = verifier.verifySignature(
+                "{}",
+                "0",
+                "f9aa4471d2a88ff244424cca2444edf7d7ac3596",
+                Long.MAX_VALUE / 1000L);
+
+        assertTrue(actual);
+    }
+
+    @Test
+    public void testVerifySignatureFailWhenStillValidButSignatureDoesntMatch() {
+        NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret");
+
+        boolean actual = verifier.verifySignature(
+                "{}",
+                "0",
+                "notMatchingForSure",
+                Long.MAX_VALUE / 1000L);
+
+        assertFalse(actual);
+    }
+}
