diff --git a/README.md b/README.md
index a71dd671ee3c43122361116ce2dace66faf19fe3..92cfbc28b94fc9699424ab8deafb6248edad3dc5 100644
--- a/README.md
+++ b/README.md
@@ -749,6 +749,15 @@ This uses the PostcodeAnywhere UK Geocode service, this will geocode any string
 * **Limitations**: ?
 * **Notes**: You must specify which MaxMind service you are using in your configuration, and also basic authentication. For example: `Geocoder.configure(:maxmind_geoip2 => {:service => :country, :basic_auth => {:user => '', :password => ''}})`.
 
+#### IPInfo.io (`:ipinfo_io`)
+
+* **API key**: optional - see http://ipinfo.io/pricing
+* **Quota**: 1,000/day - more with api key
+* **Region**: world
+* **SSL support**: no (not without access key - see http://ipinfo.io/pricing)
+* **Languages**: English
+* **Documentation**: http://ipinfo.io/developers
+* **Terms of Service**: http://ipinfo.io/developers
 
 ### IP Address Local Database Services
 
diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb
index 92d5a50b2069319c9dda6c6576aa0aa413359041..962f5a79c74bfe6f05db49aeeb9207ad9168109f 100644
--- a/lib/geocoder/lookup.rb
+++ b/lib/geocoder/lookup.rb
@@ -62,7 +62,8 @@ module Geocoder
         :maxmind_local,
         :telize,
         :pointpin,
-        :maxmind_geoip2
+        :maxmind_geoip2,
+        :ipinfo_io
       ]
     end
 
diff --git a/lib/geocoder/lookups/ipinfo_io.rb b/lib/geocoder/lookups/ipinfo_io.rb
new file mode 100644
index 0000000000000000000000000000000000000000..22efcbefeb37ebd08f53057019178a72911a4ae8
--- /dev/null
+++ b/lib/geocoder/lookups/ipinfo_io.rb
@@ -0,0 +1,41 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/ipinfo_io'
+
+module Geocoder::Lookup
+  class IpinfoIo < Base
+
+    def name
+      "Ipinfo.io"
+    end
+
+    def query_url(query)
+      "#{protocol}://ipinfo.io/#{query.sanitized_text}/geo"
+    end
+
+    # currently doesn't support HTTPS
+    def supported_protocols
+      [:http]
+    end
+
+    private # ---------------------------------------------------------------
+
+    def results(query)
+      # don't look up a loopback address, just return the stored result
+      return [reserved_result(query.text)] if query.loopback_ip_address?
+      if (doc = fetch_data(query)).nil? or doc['code'] == 401 or empty_result?(doc)
+        []
+      else
+        [doc]
+      end
+    end
+
+    def empty_result?(doc)
+      !doc.is_a?(Hash) or doc.keys == ["ip"]
+    end
+
+    def reserved_result(ip)
+      {"message" => "Input string is not a valid IP address", "code" => 401}
+    end
+
+  end
+end
diff --git a/lib/geocoder/results/ipinfo_io.rb b/lib/geocoder/results/ipinfo_io.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4968a498499b1cb071fcb804adcc39251b5cf25b
--- /dev/null
+++ b/lib/geocoder/results/ipinfo_io.rb
@@ -0,0 +1,56 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class IpinfoIo < Base
+
+    def address(format = :full)
+      "#{city} #{postal_code}, #{country}".sub(/^[ ,]*/, "")
+    end
+
+    def latitude
+      @data['loc'].split(',')[0].to_f
+    end
+
+    def longitude
+      @data['loc'].split(',')[1].to_f
+    end
+
+    def coordinates
+        [@data['loc'].split(',')[0].to_f, @data['loc'].split(',')[1].to_f]
+    end
+
+    def city
+      @data['city']
+    end
+
+    def state
+      @data['region']
+    end
+
+    def country
+      @data['country']
+    end
+
+    def postal_code
+      @data['postal']
+    end
+
+    def country_code
+      @data.fetch('country', '')
+    end
+
+    def state_code
+      @data.fetch('region_code', '')
+    end
+
+    def self.response_attributes
+      %w['ip', 'city', 'region', 'country', 'latitude', 'longitude', 'postal_code']
+    end
+
+    response_attributes.each do |a|
+      define_method a do
+        @data[a]
+      end
+    end
+  end
+end
diff --git a/test/fixtures/ipinfo_io_8_8_8_8 b/test/fixtures/ipinfo_io_8_8_8_8
new file mode 100644
index 0000000000000000000000000000000000000000..7d213307f1b1c68df96e52edfa3b2aad658f0710
--- /dev/null
+++ b/test/fixtures/ipinfo_io_8_8_8_8
@@ -0,0 +1,8 @@
+{
+  "ip": "8.8.8.8",
+  "city": "Mountain View",
+  "region": "California",
+  "country": "US",
+  "loc": "37.3845,-122.0881",
+  "postal": "94040"
+}
diff --git a/test/fixtures/ipinfo_io_no_results b/test/fixtures/ipinfo_io_no_results
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 973bd083f9a5e3ab15a3326e398266fc9b7bbb10..73c9cac423684bfd364eb4633d6d31920d27e295 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -320,6 +320,15 @@ module Geocoder
         "latlon_6000_universal_blvd"
       end
     end
+
+    require 'geocoder/lookups/ipinfo_io'
+    class IpinfoIo
+      private
+      def default_fixture_filename
+        "ipinfo_io_8_8_8_8"
+      end
+    end
+
   end
 end
 
diff --git a/test/unit/lookup_test.rb b/test/unit/lookup_test.rb
index 1fba832a3e2103eb42cee90f70e4c78736a6f440..002c6a840d05dd446f58449d8151a17a299cdfcc 100644
--- a/test/unit/lookup_test.rb
+++ b/test/unit/lookup_test.rb
@@ -24,7 +24,7 @@ class LookupTest < GeocoderTestCase
 
   def test_query_url_contains_values_in_params_hash
     Geocoder::Lookup.all_services_except_test.each do |l|
-      next if [:freegeoip, :maxmind_local, :telize, :pointpin, :geoip2, :maxmind_geoip2, :mapbox].include? l # does not use query string
+      next if [:freegeoip, :maxmind_local, :telize, :pointpin, :geoip2, :maxmind_geoip2, :mapbox, :ipinfo_io].include? l # does not use query string
       set_api_key!(l)
       url = Geocoder::Lookup.get(l).query_url(Geocoder::Query.new(
         "test", :params => {:one_in_the_hand => "two in the bush"}