diff --git a/README.md b/README.md
index 68e8a79902f5a854b6082e3e69e0c85352b095be..d4db141b72abd678a530594fc81936f4ae14584e 100644
--- a/README.md
+++ b/README.md
@@ -856,6 +856,18 @@ This uses the PostcodeAnywhere UK Geocode service, this will geocode any string
 * **Documentation**: https://db-ip.com/api/doc.php
 * **Terms of Service**: https://db-ip.com/tos.php
 
+#### Ipdata.co (`:ipdata_co`)
+
+* **API key**: optional, see: https://ipdata.co/pricing.html
+* **Quota**: 1500/day (up to 600k with paid API keys)
+* **Region**: world
+* **SSL support**: yes
+* **Languages**: English
+* **Documentation**: https://ipdata.co/docs.html
+* **Terms of Service**: https://ipdata.co/terms.html
+* **Limitations**: ?
+
+
 ### IP Address Local Database Services
 
 #### MaxMind Local (`:maxmind_local`) - EXPERIMENTAL
diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb
index 2d71041905c289b5990bfe0b3fa4b4b335a3d28a..b756f768f40e9fe94d6b80fa56505bf688a72c41 100644
--- a/lib/geocoder/lookup.rb
+++ b/lib/geocoder/lookup.rb
@@ -71,6 +71,7 @@ module Geocoder
         :maxmind_geoip2,
         :ipinfo_io,
         :ipapi_com,
+        :ipdata_co,
         :db_ip_com
       ]
     end
diff --git a/lib/geocoder/lookups/ipdata_co.rb b/lib/geocoder/lookups/ipdata_co.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0e18fd8367daebaf6561330e16776a602d641aaf
--- /dev/null
+++ b/lib/geocoder/lookups/ipdata_co.rb
@@ -0,0 +1,57 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/ipdata_co'
+
+module Geocoder::Lookup
+  class IpdataCo < Base
+
+    def name
+      "ipdata.co"
+    end
+
+    def supported_protocols
+      [:https]
+    end
+
+    def query_url(query)
+      "#{protocol}://#{host}/#{query.sanitized_text}"
+    end
+
+    private # ---------------------------------------------------------------
+
+    def results(query)
+      Geocoder.configure(:ipdata_co => {:http_headers => { "api-key" => configuration.api_key }}) if configuration.api_key
+      # don't look up a loopback address, just return the stored result
+      return [reserved_result(query.text)] if query.loopback_ip_address?
+      # note: Ipdata.co returns plain text on bad request
+      (doc = fetch_data(query)) ? [doc] : []
+    end
+
+    def reserved_result(ip)
+      {
+        "ip"           => ip,
+        "city"         => "",
+        "region_code"  => "",
+        "region_name"  => "",
+        "metrocode"    => "",
+        "zipcode"      => "",
+        "latitude"     => "0",
+        "longitude"    => "0",
+        "country_name" => "Reserved",
+        "country_code" => "RD"
+      }
+    end
+
+    def host
+      "api.ipdata.co"
+    end
+
+    def check_response_for_errors!(response)
+      if response.code.to_i == 403
+        raise_error(Geocoder::RequestDenied) ||
+          Geocoder.log(:warn, "Geocoding API error: 403 API key does not exist")
+      else
+        super(response)
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/results/ipdata_co.rb b/lib/geocoder/results/ipdata_co.rb
new file mode 100644
index 0000000000000000000000000000000000000000..497ac156d4c5441947b1e19870e60e41d85c77ff
--- /dev/null
+++ b/lib/geocoder/results/ipdata_co.rb
@@ -0,0 +1,45 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class IpdataCo < Base
+
+    def address(format = :full)
+      s = state_code.to_s == "" ? "" : ", #{state_code}"
+      "#{city}#{s} #{postal_code}, #{country}".sub(/^[ ,]*/, "")
+    end
+
+    def city
+      @data['city']
+    end
+
+    def state
+      @data['region']
+    end
+
+    def state_code
+      @data['region_code']
+    end
+
+    def country
+      @data['country_name']
+    end
+
+    def country_code
+      @data['country_code']
+    end
+
+    def postal_code
+      @data['postal']
+    end
+
+    def self.response_attributes
+      %w[ip asn organisation currency currency_symbol calling_code flag time_zone is_eu]
+    end
+
+    response_attributes.each do |a|
+      define_method a do
+        @data[a]
+      end
+    end
+  end
+end
diff --git a/test/fixtures/ipdata_co_74_200_247_59 b/test/fixtures/ipdata_co_74_200_247_59
new file mode 100644
index 0000000000000000000000000000000000000000..a2da12b53fbe78ef8d97a2bb219c80e7af35c706
--- /dev/null
+++ b/test/fixtures/ipdata_co_74_200_247_59
@@ -0,0 +1,24 @@
+{
+    "ip": "74.200.247.59",
+    "city": "Jersey City",
+    "region": "New Jersey",
+    "region_code": "NJ",
+    "country_name": "United States",
+    "country_code": "US",
+    "continent_name": "North America",
+    "continent_code": "NA",
+    "latitude": 40.7209,
+    "longitude": -74.0468,
+    "asn": "AS22576",
+    "organisation": "DataPipe, Inc.",
+    "postal": "07302",
+    "currency": "USD",
+    "currency_symbol": "$",
+    "calling_code": "1",
+    "flag": "https://ipdata.co/flags/us.png",
+    "time_zone": "America/New_York",
+    "is_eu": false,
+    "suspicious_factors": {
+        "is_tor": false
+    }
+}
\ No newline at end of file
diff --git a/test/fixtures/ipdata_co_8_8_8 b/test/fixtures/ipdata_co_8_8_8
new file mode 100644
index 0000000000000000000000000000000000000000..2462e1270a88da040f6f434f9f52faf215522c5f
--- /dev/null
+++ b/test/fixtures/ipdata_co_8_8_8
@@ -0,0 +1 @@
+8.8.8 does not appear to be an IPv4 or IPv6 address
\ No newline at end of file
diff --git a/test/fixtures/ipdata_co_no_results b/test/fixtures/ipdata_co_no_results
new file mode 100644
index 0000000000000000000000000000000000000000..896eaf56355459a4b59807ba85e079402455917f
--- /dev/null
+++ b/test/fixtures/ipdata_co_no_results
@@ -0,0 +1 @@
+0.0.0 does not appear to be an IPv4 or IPv6 address
\ No newline at end of file
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 87e11da448827acbaeec7ebb5a45b9c9516fb826..4857dfed71f30051ee1e0abd24a840b8257576bb 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -373,6 +373,14 @@ module Geocoder
       end
     end
 
+    require 'geocoder/lookups/ipdata_co'
+    class IpdataCo
+      private
+      def default_fixture_filename
+        "ipdata_co_74_200_247_59"
+      end
+    end
+
     require 'geocoder/lookups/ban_data_gouv_fr'
     class BanDataGouvFr
       private
diff --git a/test/unit/error_handling_test.rb b/test/unit/error_handling_test.rb
index ca20f3e5e40caa3e0bd1118e3b490c86219d0070..689d1fe56c46d6ddfbb9f472d1ab4c863d9ea6e2 100644
--- a/test/unit/error_handling_test.rb
+++ b/test/unit/error_handling_test.rb
@@ -19,7 +19,7 @@ class ErrorHandlingTest < GeocoderTestCase
 
   def test_always_raise_response_parse_error
     Geocoder.configure(:always_raise => [Geocoder::ResponseParseError])
-    [:freegeoip, :google, :okf].each do |l|
+    [:freegeoip, :google, :ipdata_co, :okf].each do |l|
       lookup = Geocoder::Lookup.get(l)
       set_api_key!(l)
       assert_raises Geocoder::ResponseParseError do
@@ -29,7 +29,7 @@ class ErrorHandlingTest < GeocoderTestCase
   end
 
   def test_never_raise_response_parse_error
-    [:freegeoip, :google, :okf].each do |l|
+    [:freegeoip, :google, :ipdata_co, :okf].each do |l|
       lookup = Geocoder::Lookup.get(l)
       set_api_key!(l)
       silence_warnings do
diff --git a/test/unit/lookup_test.rb b/test/unit/lookup_test.rb
index 8dcaf8047fa61f1d239a10d50eae2c63a5ba0571..7f21877f77b4359c3fe6b8c37a70ac5e123214c9 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, :ipinfo_io, :ipapi_com].include? l # does not use query string
+      next if [:freegeoip, :maxmind_local, :telize, :pointpin, :geoip2, :maxmind_geoip2, :mapbox, :ipdata_co, :ipinfo_io, :ipapi_com].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"}
diff --git a/test/unit/lookups/ipdata_co_test.rb b/test/unit/lookups/ipdata_co_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5acdcf72e7b5c19fdb5ac1e57e3402fc2a4fed21
--- /dev/null
+++ b/test/unit/lookups/ipdata_co_test.rb
@@ -0,0 +1,35 @@
+# encoding: utf-8
+require 'test_helper'
+
+class IpdataCoTest < GeocoderTestCase
+
+  def setup
+    Geocoder.configure(ip_lookup: :ipdata_co)
+  end
+
+  def test_result_on_ip_address_search
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::IpdataCo)
+  end
+
+  def test_invalid_json
+    Geocoder.configure(:always_raise => [Geocoder::ResponseParseError])
+    assert_raise Geocoder::ResponseParseError do
+      Geocoder.search("8.8.8", ip_address: true)
+    end
+  end
+
+  def test_result_components
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Jersey City, NJ 07302, United States", result.address
+  end
+
+  def test_not_authorized
+    Geocoder.configure(always_raise: [Geocoder::RequestDenied])
+    lookup = Geocoder::Lookup.get(:ipdata_co)
+      assert_raises Geocoder::RequestDenied do
+        response = MockHttpResponse.new(code: 403)
+        lookup.send(:check_response_for_errors!, response)
+    end
+  end
+end