diff --git a/README.md b/README.md
index f90d0dfa03c11c4fee9248694e0fbbbd2ec297d2..c4418e5ba64b7f9657ca8674d3615a1f4c83d6ce 100644
--- a/README.md
+++ b/README.md
@@ -555,15 +555,15 @@ The [Google Places Details API](https://developers.google.com/places/documentati
 
 #### ESRI (`:esri`)
 
-* **API key**: none
+* **API key**: optional (set `Geocoder.configure(:esri => {:api_key => ["client_id", "client_secret"]})`)
 * **Quota**: Required for some scenarios (see Terms of Service)
 * **Region**: world
 * **SSL support**: yes
 * **Languages**: English
-* **Documentation**: http://resources.arcgis.com/en/help/arcgis-online-geocoding-rest-api/
+* **Documentation**: https://developers.arcgis.com/rest/geocode/api-reference/overview-world-geocoding-service.htm
 * **Terms of Service**: http://www.esri.com/legal/software-license
-* **Limitations**: ?
-* **Notes**: You can specify which projection you want to use by setting, for example: `Geocoder.configure(:esri => {:outSR => 102100})`.
+* **Limitations**: Requires API key if results will be stored. Using API key will also remove rate limit.
+* **Notes**: You can specify which projection you want to use by setting, for example: `Geocoder.configure(:esri => {:outSR => 102100})`. If you will store results, set the flag and provide API key: `Geocoder.configure(:esri => {:api_key => ["client_id", "client_secret"], :for_storage => true})`. If you want to, you can also supply an ESRI token directly: `Geocoder.configure(:esri => {:token => Geocoder::EsriToken.new('TOKEN', Time.now + 1.day})`
 
 #### Mapzen (`:mapzen`)
 
diff --git a/lib/geocoder/esri_token.rb b/lib/geocoder/esri_token.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a68f01555e5c62199e92f64d642b09709bff2b9b
--- /dev/null
+++ b/lib/geocoder/esri_token.rb
@@ -0,0 +1,38 @@
+module Geocoder
+  class EsriToken
+    attr_accessor :value, :expires_at
+
+    def initialize(value, expires_at)
+      @value = value
+      @expires_at = expires_at
+    end
+
+    def to_s
+        @value
+    end
+
+    def active?
+      @expires_at > Time.now
+    end
+
+    def self.generate_token(client_id, client_secret, expires=1440)
+      # creates a new token that will expire in 1 day by default
+      getToken = Net::HTTP.post_form URI('https://www.arcgis.com/sharing/rest/oauth2/token'),
+        f: 'json',
+        client_id: client_id,
+        client_secret: client_secret,
+        grant_type: 'client_credentials',
+        expiration: expires # (minutes) max: 20160, default: 1 day
+
+      response = JSON.parse(getToken.body)
+
+      if response['error']
+        Geocoder.log(:warn, response['error'])
+      else
+        token_value = response['access_token']
+        expires_at = Time.now + expires.minutes
+        new(token_value, expires_at)
+      end
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/esri.rb b/lib/geocoder/lookups/esri.rb
index d50da69499b09fe790e6eacdd939738bfb73afde..122c8c8b368825d1949ce6dbc8a5468faba52192 100644
--- a/lib/geocoder/lookups/esri.rb
+++ b/lib/geocoder/lookups/esri.rb
@@ -1,5 +1,6 @@
 require 'geocoder/lookups/base'
 require "geocoder/results/esri"
+require 'geocoder/esri_token'
 
 module Geocoder::Lookup
   class Esri < Base
@@ -41,8 +42,20 @@ module Geocoder::Lookup
       else
         params[:text] = query.sanitized_text
       end
+      params[:token] = token
+      params[:forStorage] = configuration[:for_storage] if configuration[:for_storage]
       params.merge(super)
     end
 
+    def token
+      if configuration[:token] && configuration[:token].active? # if we have a token, use it
+        configuration[:token].to_s
+      elsif configuration.api_key # generate a new token if we have credentials
+        token_instance = EsriToken.generate_token(*configuration.api_key)
+        Geocoder.configure(:esri => {:token => token_instance})
+        token_instance.to_s
+      end
+    end
+
   end
 end
diff --git a/test/unit/lookups/esri_test.rb b/test/unit/lookups/esri_test.rb
index 6d946c9a1d7db5a026e26f53748f5d668b16895b..c9e171eec5e43e92bd04d592402f0958160d725a 100644
--- a/test/unit/lookups/esri_test.rb
+++ b/test/unit/lookups/esri_test.rb
@@ -1,5 +1,6 @@
 # encoding: utf-8
 require 'test_helper'
+require 'geocoder/esri_token'
 
 class EsriTest < GeocoderTestCase
 
@@ -15,6 +16,16 @@ class EsriTest < GeocoderTestCase
       res
   end
 
+  def test_query_for_geocode_with_token_for_storage
+    token = Geocoder::EsriToken.new('xxxxx', Time.now + 1.day)
+    Geocoder.configure(esri: {token: token, for_storage: true})
+    query = Geocoder::Query.new("Bluffton, SC")
+    lookup = Geocoder::Lookup.get(:esri)
+    res = lookup.query_url(query)
+    assert_equal "http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find?f=pjson&forStorage=true&outFields=%2A&text=Bluffton%2C+SC&token=xxxxx",
+      res
+  end
+
   def test_query_for_reverse_geocode
     query = Geocoder::Query.new([45.423733, -75.676333])
     lookup = Geocoder::Lookup.get(:esri)
@@ -90,4 +101,8 @@ class EsriTest < GeocoderTestCase
     assert_equal [40.744050000000001, -74.000241000000003, 40.756050000000002, -73.988241000000002],
       result.viewport
   end
+
+  def teardown
+    Geocoder.configure(esri: {token: nil, for_storage: nil})
+  end
 end