diff --git a/lib/common/grpc-service.js b/lib/common/grpc-service.js
index 48e48b7b4e03..c2f8ad49f00a 100644
--- a/lib/common/grpc-service.js
+++ b/lib/common/grpc-service.js
@@ -33,95 +33,10 @@ var path = require('path');
 var Service = require('./service.js');
 
 /**
- * @const {object} - A map of protobuf codes to HTTP status codes.
+ * @type {module:common/util}
  * @private
  */
-var HTTP_ERROR_CODE_MAP = {
-  0: {
-    code: 200,
-    message: 'OK'
-  },
-
-  1: {
-    code: 499,
-    message: 'Client Closed Request'
-  },
-
-  2: {
-    code: 500,
-    message: 'Internal Server Error'
-  },
-
-  3: {
-    code: 400,
-    message: 'Bad Request'
-  },
-
-  4: {
-    code: 504,
-    message: 'Gateway Timeout'
-  },
-
-  5: {
-    code: 404,
-    message: 'Not Found'
-  },
-
-  6: {
-    code: 409,
-    message: 'Conflict'
-  },
-
-  7: {
-    code: 403,
-    message: 'Forbidden'
-  },
-
-  8: {
-    code: 429,
-    message: 'Too Many Requests'
-  },
-
-  9: {
-    code: 412,
-    message: 'Precondition Failed'
-  },
-
-  10: {
-    code: 409,
-    message: 'Conflict'
-  },
-
-  11: {
-    code: 400,
-    message: 'Bad Request'
-  },
-
-  12: {
-    code: 501,
-    message: 'Not Implemented'
-  },
-
-  13: {
-    code: 500,
-    message: 'Internal Server Error'
-  },
-
-  14: {
-    code: 503,
-    message: 'Service Unavailable'
-  },
-
-  15: {
-    code: 500,
-    message: 'Internal Server Error'
-  },
-
-  16: {
-    code: 401,
-    message: 'Unauthorized'
-  }
-};
+var util = require('./util.js');
 
 /**
  * Service is a base class, meant to be inherited from by a "service," like
@@ -243,8 +158,8 @@ GrpcService.prototype.request = function(protoOpts, reqOpts, callback) {
 
   service[protoOpts.method](reqOpts, function(err, resp) {
     if (err) {
-      if (HTTP_ERROR_CODE_MAP[err.code]) {
-        var httpError = HTTP_ERROR_CODE_MAP[err.code];
+      if (util.HTTP_ERROR_CODE_MAP[err.code]) {
+        var httpError = util.HTTP_ERROR_CODE_MAP[err.code];
         err.code = httpError.code;
       }
 
diff --git a/lib/common/util.js b/lib/common/util.js
index e15f9d6e367a..39fda2651ec8 100644
--- a/lib/common/util.js
+++ b/lib/common/util.js
@@ -545,3 +545,96 @@ function normalizeArguments(globalContext, localConfig, options) {
 }
 
 util.normalizeArguments = normalizeArguments;
+
+/**
+ * @const {object} - A map of protobuf codes to HTTP status codes.
+ * @private
+ */
+var HTTP_ERROR_CODE_MAP = {
+  0: {
+    code: 200,
+    message: 'OK'
+  },
+
+  1: {
+    code: 499,
+    message: 'Client Closed Request'
+  },
+
+  2: {
+    code: 500,
+    message: 'Internal Server Error'
+  },
+
+  3: {
+    code: 400,
+    message: 'Bad Request'
+  },
+
+  4: {
+    code: 504,
+    message: 'Gateway Timeout'
+  },
+
+  5: {
+    code: 404,
+    message: 'Not Found'
+  },
+
+  6: {
+    code: 409,
+    message: 'Conflict'
+  },
+
+  7: {
+    code: 403,
+    message: 'Forbidden'
+  },
+
+  8: {
+    code: 429,
+    message: 'Too Many Requests'
+  },
+
+  9: {
+    code: 412,
+    message: 'Precondition Failed'
+  },
+
+  10: {
+    code: 409,
+    message: 'Conflict'
+  },
+
+  11: {
+    code: 400,
+    message: 'Bad Request'
+  },
+
+  12: {
+    code: 501,
+    message: 'Not Implemented'
+  },
+
+  13: {
+    code: 500,
+    message: 'Internal Server Error'
+  },
+
+  14: {
+    code: 503,
+    message: 'Service Unavailable'
+  },
+
+  15: {
+    code: 500,
+    message: 'Internal Server Error'
+  },
+
+  16: {
+    code: 401,
+    message: 'Unauthorized'
+  }
+};
+
+util.HTTP_ERROR_CODE_MAP = HTTP_ERROR_CODE_MAP;
diff --git a/lib/vision/index.js b/lib/vision/index.js
index 1132828de324..bdbf589ae837 100644
--- a/lib/vision/index.js
+++ b/lib/vision/index.js
@@ -29,6 +29,7 @@ var fs = require('fs');
 var is = require('is');
 var nodeutil = require('util');
 var prop = require('propprop');
+var request = require('request');
 
 /**
  * @type {module:storage/file}
@@ -315,12 +316,20 @@ Vision.prototype.detect = function(images, options, callback) {
         return;
       }
 
+      var originalResp = extend(true, {}, resp);
+      var errors = [];
+
       function mergeArrayOfObjects(arr) {
         return extend.apply(null, arr);
       }
 
       function formatAnnotationBuilder(type) {
         return function(annotation) {
+          if (type === 'error') {
+            errors.push(Vision.formatError_(annotation));
+            return [];
+          }
+
           var formatMethodMap = {
             faceAnnotations: Vision.formatFaceAnnotation_,
             imagePropertiesAnnotation: Vision.formatImagePropertiesAnnotation_,
@@ -335,8 +344,6 @@ Vision.prototype.detect = function(images, options, callback) {
         };
       }
 
-      var originalResp = extend(true, {}, resp);
-
       var detections = images
         .map(function() {
           // Group detections by image...
@@ -412,8 +419,21 @@ Vision.prototype.detect = function(images, options, callback) {
           return annotations;
         });
 
+      var respErrors = null;
+      if (errors.length > 0) {
+        respErrors = errors;
+      }
+
       // If only a single image was given, expose it from the array.
-      callback(null, isSingleImage ? detections[0] : detections, originalResp);
+      if (isSingleImage) {
+        if (respErrors) {
+          respErrors = respErrors[0];
+        }
+
+        detections = detections[0];
+      }
+
+      callback(respErrors, detections, originalResp);
     });
   });
 };
@@ -1178,6 +1198,24 @@ Vision.findImages_ = function(images, callback) {
       return;
     }
 
+    // File is a URL.
+    if (image.indexOf('http') === 0) {
+      request({
+        method: 'GET',
+        uri: image,
+        encoding: 'base64'
+      }, function(err, resp, body) {
+        if (err) {
+          callback(err);
+          return;
+        }
+
+        callback(null, { content: body });
+      });
+
+      return;
+    }
+
     // File exists on disk.
     fs.readFile(image, { encoding: 'base64' }, function(err, contents) {
       if (err) {
@@ -1234,6 +1272,17 @@ Vision.formatEntityAnnotation_ = function(entityAnnotation, options) {
   return formattedEntityAnnotation;
 };
 
+/**
+ * Format an error from the API.
+ *
+ * @private
+ */
+Vision.formatError_ = function(error) {
+  var statusError = new Error(error.message);
+  statusError.code = util.HTTP_ERROR_CODE_MAP[error.code].code;
+  return statusError;
+};
+
 /**
  * Format a raw face annotation response from the API.
  *
diff --git a/system-test/vision.js b/system-test/vision.js
index ce7ddfbb47f1..73b635ef4847 100644
--- a/system-test/vision.js
+++ b/system-test/vision.js
@@ -17,6 +17,8 @@
 'use strict';
 
 var assert = require('assert');
+var fs = require('fs');
+var http = require('http');
 var is = require('is');
 var multiline = require('multiline');
 var path = require('path');
@@ -36,6 +38,29 @@ describe('Vision', function() {
     vision = new Vision(env);
   });
 
+  it('should detect from a URL', function(done) {
+    var server = http.createServer(function(req, res) {
+      fs.readFile(IMAGES.logo, function(err, resp) {
+        assert.ifError(err);
+        res.end(resp);
+      });
+    });
+
+    server.listen(8800, function(err) {
+      assert.ifError(err);
+
+      var url = 'http://localhost:8800/logo.png';
+
+      vision.detectLogos(url, function(err, logos) {
+        assert.ifError(err);
+
+        assert.deepEqual(logos, ['Google']);
+
+        done();
+      });
+    });
+  });
+
   it('should perform multiple detections', function(done) {
     var detectionTypes = ['faces', 'labels', 'safeSearch'];