diff --git a/README.md b/README.md
index ea2f673390b7f50a5ece0cdcb4a11770f76fe72f..546fbdaa77f2d8180f081e73804e44adfaadfedc 100644
--- a/README.md
+++ b/README.md
@@ -576,7 +576,7 @@ The [Google Places Details API](https://developers.google.com/places/documentati
 * **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**: 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})`
+* **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/configuration.rb b/lib/geocoder/configuration.rb
index 2bb428db431c84b1879e27c24a75961a621fb4bd..6206e3a7ee5537a97f44bdce8c7dac2ab07194c9 100644
--- a/lib/geocoder/configuration.rb
+++ b/lib/geocoder/configuration.rb
@@ -62,6 +62,7 @@ module Geocoder
       :distances,
       :basic_auth,
       :for_storage,
+      :token,
       :logger,
       :kernel_logger_level
     ]
@@ -106,6 +107,7 @@ module Geocoder
       @data[:cache_prefix] = "geocoder:" # prefix (string) to use for all cache keys
       @data[:basic_auth]   = {}          # user and password for basic auth ({:user => "user", :password => "password"})
       @data[:for_storage]  = nil         # will the result be stored for non-caching purposes (boolean)
+      @data[:token]  = nil               # token object for authentication
       @data[:logger]       = :kernel     # :kernel or Logger instance
       @data[:kernel_logger_level] = ::Logger::WARN # log level, if kernel logger is used
 
diff --git a/lib/geocoder/esri_token.rb b/lib/geocoder/esri_token.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f0520844e0dbfaded9632e2f6f7580bf1f41bc13
--- /dev/null
+++ b/lib/geocoder/esri_token.rb
@@ -0,0 +1,18 @@
+module Geocoder::Token
+  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 valid?
+      @expires_at > Time.now
+    end
+  end
+end
diff --git a/lib/geocoder/lookups/esri.rb b/lib/geocoder/lookups/esri.rb
index b032bcf434cf3a76d10715dcfbbf4df85d3c31a3..a7f3f2cee83a1fa29046066358186de0ef95d938 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
@@ -15,6 +16,24 @@ module Geocoder::Lookup
         url_query_string(query)
     end
 
+    def generate_token(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: configuration.api_key[0],
+        client_secret: configuration.api_key[1],
+        grant_type: 'client_credentials',
+        expiration: expires # (minutes) max: 20160, default: 1 day
+
+      if JSON.parse(getToken.body)['error']
+        raise_error(Geocoder::InvalidApiKey) || Geocoder.log(:warn, "Couldn't generate ESRI token: invalid API key.")
+      else
+        token_value = JSON.parse(getToken.body)['access_token']
+        expires_at = Time.now + expires.minutes
+        Geocoder::EsriToken.new(token_value, expires_at)
+      end
+    end
+
     private # ---------------------------------------------------------------
 
     def results(query)
@@ -41,28 +60,19 @@ module Geocoder::Lookup
       else
         params[:text] = query.sanitized_text
       end
-      params[:token] = token if configuration.api_key
+      params[:token] = token
       params[:forStorage] = configuration.for_storage if configuration.for_storage
       params.merge(super)
     end
 
     def token
-      unless token_is_valid
-        getToken = Net::HTTP.post_form URI('https://www.arcgis.com/sharing/rest/oauth2/token'),
-          f: 'json',
-          client_id: configuration.api_key[0],
-          client_secret: configuration.api_key[1],
-          grant_type: 'client_credentials',
-          expiration: 1440 # valid for one day,
-
-          @token = JSON.parse(getToken.body)['access_token']
-          @token_expires = Time.now + 1.day
+      if configuration.token && configuration.token.valid? # 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 = generate_token
+        Geocoder.configure(:esri => {:token => token_instance})
+        token_instance.to_s
       end
-      return @token
-    end
-
-    def token_is_valid
-      @token && @token_expires > Time.now
     end
 
   end