Repository: stripe/stripe-node
Pull request #1198 (2021-07-26 22:25:19)

Add support for SearchResult.
### Notify

r? @richardm-stripe 

### Summary

Adds support for search methods and `search_result` objects returned by the Stripe API. This requires updating auto pagination to support this. These paginate similarly to lists, however they rely on a `next_page` token included in the response rather than using object IDs + `starting_before`/`ending_after`. Thus, only forward pagination is supported

### Test plan

Added unit tests. We'll want to add some more rigorous tests once a resource is using this.

richardm-stripe: This LGTM, but I wonder - would the code be simpler if we removed all relative paths and used `fullPath` everywhere? I don't think there is any virtue to relative paths.
dcr-stripe: Thanks for the comments!

For full paths, I'll lay down the ground work in this PR and then in a follow up can update the other methods to use it as it depends on basic methods.

r? @richardm-stripe 
dcr-stripe: Removing reviews for a second. I think it'd be good to split our the fullPath logic of this PR into its own PR.
dcr-stripe: I split out https://github.com/stripe/stripe-node/pull/1200/files for full path support. This should be good to go now.

r? @richardm-stripe 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f560b9b..f8a7e2a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,35 @@
 # CHANGELOG
 
+## 8.171.0 - 2021-08-27
+* [#1226](https://github.com/stripe/stripe-node/pull/1226) API Updates
+  * Add support for `cancellation_reason` on `BillingPortalConfigurationCreateParams.features.subscription_cancel`, `BillingPortalConfigurationUpdateParams.features.subscription_cancel`, and `BillingPortal.Configuration.features.subscription_cancel`
+
+## 8.170.0 - 2021-08-19
+* [#1223](https://github.com/stripe/stripe-node/pull/1223) API Updates
+  * Add support for new value `fil` on enums `CheckoutSessionCreateParams.locale` and `Checkout.Session.locale`
+  * Add support for new value `au_arn` on enums `Checkout.Session.customer_details.tax_ids[].type`, `Invoice.customer_tax_ids[].type`, and `TaxId.type`
+  * Add support for new value `au_arn` on enums `CustomerCreateParams.tax_id_data[].type`, `InvoiceUpcomingParams.customer_details.tax_ids[].type`, `InvoiceUpcomingLinesParams.customer_details.tax_ids[].type`, and `TaxIdCreateParams.type`
+* [#1221](https://github.com/stripe/stripe-node/pull/1221) Add client name property to HttpClient.
+* [#1219](https://github.com/stripe/stripe-node/pull/1219) Update user agent computation to handle environments without process.
+* [#1218](https://github.com/stripe/stripe-node/pull/1218) Add an HttpClient interface and NodeHttpClient implementation.
+* [#1217](https://github.com/stripe/stripe-node/pull/1217) Update nock.
+
+## 8.169.0 - 2021-08-11
+* [#1215](https://github.com/stripe/stripe-node/pull/1215) API Updates
+  * Add support for `locale` on `BillingPortalSessionCreateParams` and `BillingPortal.Session`
+  * Change type of `Invoice.collection_method` and `Subscription.collection_method` from `nullable(enum('charge_automatically'|'send_invoice'))` to `enum('charge_automatically'|'send_invoice')`
+
+## 8.168.0 - 2021-08-04
+* [#1211](https://github.com/stripe/stripe-node/pull/1211) API Updates
+  * Change type of `PaymentIntentCreateParams.payment_method_options.sofort.preferred_language`, `PaymentIntentUpdateParams.payment_method_options.sofort.preferred_language`, and `PaymentIntentConfirmParams.payment_method_options.sofort.preferred_language` from `enum` to `emptyStringable(enum)`
+  * Change `Price.tax_behavior`, `Product.tax_code`, `Quote.automatic_tax`, and `TaxRate.tax_type` to be required
+
+## 8.167.0 - 2021-07-28
+* [#1206](https://github.com/stripe/stripe-node/pull/1206) Fix Typescript definition for `StripeResource.LastResponse.headers`
+* [#1205](https://github.com/stripe/stripe-node/pull/1205) Prevent concurrent initial `uname` invocations
+* [#1199](https://github.com/stripe/stripe-node/pull/1199) Explicitly define basic method specs
+* [#1200](https://github.com/stripe/stripe-node/pull/1200) Add support for `fullPath` on method specs
+
 ## 8.166.0 - 2021-07-28
 * [#1203](https://github.com/stripe/stripe-node/pull/1203) API Updates
   * Bugfix: add missing autopagination methods to `Quote.listLineItems` and `Quote.listComputedUpfrontLineItems`
diff --git a/VERSION b/VERSION
index 24be86a1..42d72832 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-8.166.0
+8.171.0
diff --git a/lib/StripeMethod.basic.js b/lib/StripeMethod.basic.js
index c807ea18..b9be5302 100644
--- a/lib/StripeMethod.basic.js
+++ b/lib/StripeMethod.basic.js
@@ -2,6 +2,8 @@
 
 const stripeMethod = require('./StripeMethod');
 
+// DEPRECATED: These were kept for backwards compatibility in case users were
+// using this, but basic methods are now explicitly defined on a resource.
 module.exports = {
   create: stripeMethod({
     method: 'POST',
diff --git a/lib/StripeMethod.js b/lib/StripeMethod.js
index 4c6c544d..78018da2 100644
--- a/lib/StripeMethod.js
+++ b/lib/StripeMethod.js
@@ -39,7 +39,9 @@ function stripeMethod(spec) {
       callback
     );
 
-    if (spec.methodType === 'list') {
+    // Please note `spec.methodType === 'search'` is beta functionality and this
+    // interface is subject to change/removal at any time.
+    if (spec.methodType === 'list' || spec.methodType === 'search') {
       const autoPaginationMethods = makeAutoPaginationMethods(
         this,
         args,
diff --git a/lib/StripeResource.js b/lib/StripeResource.js
index 51661387..e16bd901 100644
--- a/lib/StripeResource.js
+++ b/lib/StripeResource.js
@@ -1,7 +1,5 @@
 'use strict';
 
-const http = require('http');
-const https = require('https');
 const path = require('path');
 
 const utils = require('./utils');
@@ -14,8 +12,7 @@ const {
   StripeAPIError,
 } = require('./Error');
 
-const defaultHttpAgent = new http.Agent({keepAlive: true});
-const defaultHttpsAgent = new https.Agent({keepAlive: true});
+const HttpClient = require('./net/HttpClient');
 
 // Provide extension mechanism for Stripe Resource Sub-Classes
 StripeResource.extend = utils.protoExtend;
@@ -44,6 +41,8 @@ function StripeResource(stripe, deprecatedUrlData) {
   this.resourcePath = this.path;
   this.path = utils.makeURLInterpolator(this.path);
 
+  // DEPRECATED: This was kept for backwards compatibility in case users were
+  // using this, but basic methods are now explicitly defined on a resource.
   if (this.includeBasic) {
     this.includeBasic.forEach(function(methodName) {
       this[methodName] = StripeResource.BASIC_METHODS[methodName];
@@ -102,35 +101,39 @@ StripeResource.prototype = {
     };
   },
 
-  _addHeadersDirectlyToResponse(res, headers) {
+  _addHeadersDirectlyToObject(obj, headers) {
     // For convenience, make some headers easily accessible on
     // lastResponse.
 
     // NOTE: Stripe responds with lowercase header names/keys.
-    res.requestId = headers['request-id'];
-    res.stripeAccount = res.stripeAccount || headers['stripe-account'];
-    res.apiVersion = res.apiVersion || headers['stripe-version'];
-    res.idempotencyKey = res.idempotencyKey || headers['idempotency-key'];
+    obj.requestId = headers['request-id'];
+    obj.stripeAccount = obj.stripeAccount || headers['stripe-account'];
+    obj.apiVersion = obj.apiVersion || headers['stripe-version'];
+    obj.idempotencyKey = obj.idempotencyKey || headers['idempotency-key'];
   },
 
-  _makeResponseEvent(req, res, headers) {
+  _makeResponseEvent(requestEvent, statusCode, headers) {
     const requestEndTime = Date.now();
-    const requestDurationMs = requestEndTime - req._requestStart;
+    const requestDurationMs = requestEndTime - requestEvent.request_start_time;
 
     return utils.removeNullish({
       api_version: headers['stripe-version'],
       account: headers['stripe-account'],
       idempotency_key: headers['idempotency-key'],
-      method: req._requestEvent.method,
-      path: req._requestEvent.path,
-      status: res.statusCode,
-      request_id: res.requestId,
+      method: requestEvent.method,
+      path: requestEvent.path,
+      status: statusCode,
+      request_id: this._getRequestId(headers),
       elapsed: requestDurationMs,
-      request_start_time: req._requestStart,
+      request_start_time: requestEvent.request_start_time,
       request_end_time: requestEndTime,
     });
   },
 
+  _getRequestId(headers) {
+    return headers['request-id'];
+  },
+
   /**
    * Used by methods with spec.streaming === true. For these methods, we do not
    * buffer successful responses into memory or do parse them into stripe
@@ -141,17 +144,31 @@ StripeResource.prototype = {
    * still be buffered/parsed and handled by _jsonResponseHandler -- see
    * makeRequest)
    */
-  _streamingResponseHandler(req, callback) {
+  _streamingResponseHandler(requestEvent, callback) {
     return (res) => {
-      const headers = res.headers || {};
-      this._addHeadersDirectlyToResponse(res, headers);
+      const headers = res.getHeaders();
 
-      res.once('end', () => {
-        const responseEvent = this._makeResponseEvent(req, res, headers);
+      const streamCompleteCallback = () => {
+        const responseEvent = this._makeResponseEvent(
+          requestEvent,
+          res.getStatusCode(),
+          headers
+        );
         this._stripe._emitter.emit('response', responseEvent);
-        this._recordRequestMetrics(res.requestId, responseEvent.elapsed);
-      });
-      return callback(null, res);
+        this._recordRequestMetrics(
+          this._getRequestId(headers),
+          responseEvent.elapsed
+        );
+      };
+
+      const stream = res.toStream(streamCompleteCallback);
+
+      // This is here for backwards compatibility, as the stream is a raw
+      // HTTP response in Node and the legacy behavior was to mutate this
+      // response.
+      this._addHeadersDirectlyToObject(stream, headers);
+
+      return callback(null, stream);
     };
   },
 
@@ -160,73 +177,79 @@ StripeResource.prototype = {
    * parses the JSON and returns it (i.e. passes it to the callback) if there
    * is no "error" field. Otherwise constructs/passes an appropriate Error.
    */
-  _jsonResponseHandler(req, callback) {
+  _jsonResponseHandler(requestEvent, callback) {
     return (res) => {
-      let response = '';
-
-      res.setEncoding('utf8');
-      res.on('data', (chunk) => {
-        response += chunk;
-      });
-      res.once('end', () => {
-        const headers = res.headers || {};
-        this._addHeadersDirectlyToResponse(res, headers);
-        const responseEvent = this._makeResponseEvent(req, res, headers);
-        this._stripe._emitter.emit('response', responseEvent);
-
-        try {
-          response = JSON.parse(response);
-
-          if (response.error) {
-            let err;
-
-            // Convert OAuth error responses into a standard format
-            // so that the rest of the error logic can be shared
-            if (typeof response.error === 'string') {
-              response.error = {
-                type: response.error,
-                message: response.error_description,
-              };
+      const headers = res.getHeaders();
+      const requestId = this._getRequestId(headers);
+      const statusCode = res.getStatusCode();
+
+      const responseEvent = this._makeResponseEvent(
+        requestEvent,
+        statusCode,
+        headers
+      );
+      this._stripe._emitter.emit('response', responseEvent);
+
+      res
+        .toJSON()
+        .then(
+          (jsonResponse) => {
+            if (jsonResponse.error) {
+              let err;
+
+              // Convert OAuth error responses into a standard format
+              // so that the rest of the error logic can be shared
+              if (typeof jsonResponse.error === 'string') {
+                jsonResponse.error = {
+                  type: jsonResponse.error,
+                  message: jsonResponse.error_description,
+                };
+              }
+
+              jsonResponse.error.headers = headers;
+              jsonResponse.error.statusCode = statusCode;
+              jsonResponse.error.requestId = requestId;
+
+              if (statusCode === 401) {
+                err = new StripeAuthenticationError(jsonResponse.error);
+              } else if (statusCode === 403) {
+                err = new StripePermissionError(jsonResponse.error);
+              } else if (statusCode === 429) {
+                err = new StripeRateLimitError(jsonResponse.error);
+              } else {
+                err = StripeError.generate(jsonResponse.error);
+              }
+
+              throw err;
             }
 
-            response.error.headers = headers;
-            response.error.statusCode = res.statusCode;
-            response.error.requestId = res.requestId;
-
-            if (res.statusCode === 401) {
-              err = new StripeAuthenticationError(response.error);
-            } else if (res.statusCode === 403) {
-              err = new StripePermissionError(response.error);
-            } else if (res.statusCode === 429) {
-              err = new StripeRateLimitError(response.error);
-            } else {
-              err = StripeError.generate(response.error);
-            }
-            return callback.call(this, err, null);
-          }
-        } catch (e) {
-          return callback.call(
-            this,
-            new StripeAPIError({
+            return jsonResponse;
+          },
+          (e) => {
+            throw new StripeAPIError({
               message: 'Invalid JSON received from the Stripe API',
-              response,
               exception: e,
               requestId: headers['request-id'],
-            }),
-            null
-          );
-        }
-
-        this._recordRequestMetrics(res.requestId, responseEvent.elapsed);
-
-        // Expose res object
-        Object.defineProperty(response, 'lastResponse', {
-          enumerable: false,
-          writable: false,
-          value: res,
-        });
-        callback.call(this, null, response);
-      });
+            });
+          }
+        )
+        .then(
+          (jsonResponse) => {
+            this._recordRequestMetrics(requestId, responseEvent.elapsed);
+
+            // Expose raw response object.
+            const rawResponse = res.getRawResponse();
+            this._addHeadersDirectlyToObject(rawResponse, headers);
+            Object.defineProperty(jsonResponse, 'lastResponse', {
+              enumerable: false,
+              writable: false,
+              value: rawResponse,
+            });
+
+            callback.call(this, null, jsonResponse);
+          },
+          (e) => callback.call(this, e, null)
+        );
     };
   },
 
@@ -263,15 +286,15 @@ StripeResource.prototype = {
 
     // The API may ask us not to retry (e.g., if doing so would be a no-op)
     // or advise us to retry (e.g., in cases of lock timeouts); we defer to that.
-    if (res.headers && res.headers['stripe-should-retry'] === 'false') {
+    if (res.getHeaders()['stripe-should-retry'] === 'false') {
       return false;
     }
-    if (res.headers && res.headers['stripe-should-retry'] === 'true') {
+    if (res.getHeaders()['stripe-should-retry'] === 'true') {
       return true;
     }
 
     // Retry on conflict errors.
-    if (res.statusCode === 409) {
+    if (res.getStatusCode() === 409) {
       return true;
     }
 
@@ -280,7 +303,7 @@ StripeResource.prototype = {
     // Note that we expect the stripe-should-retry header to be false
     // in most cases when a 500 is returned, since our idempotency framework
     // would typically replay it anyway.
-    if (res.statusCode >= 500) {
+    if (res.getStatusCode() >= 500) {
       return true;
     }
 
@@ -432,22 +455,18 @@ StripeResource.prototype = {
           ? options.settings.timeout
           : this._stripe.getApiField('timeout');
 
-      const isInsecureConnection =
-        this._stripe.getApiField('protocol') === 'http';
-      let agent = this._stripe.getApiField('agent');
-      if (agent == null) {
-        agent = isInsecureConnection ? defaultHttpAgent : defaultHttpsAgent;
-      }
-
-      const req = (isInsecureConnection ? http : https).request({
-        host: host || this._stripe.getApiField('host'),
-        port: this._stripe.getApiField('port'),
-        path,
-        method,
-        agent,
-        headers,
-        ciphers: 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!MD5',
-      });
+      const req = this._stripe
+        .getApiField('httpClient')
+        .makeRequest(
+          host || this._stripe.getApiField('host'),
+          this._stripe.getApiField('port'),
+          path,
+          method,
+          headers,
+          requestData,
+          this._stripe.getApiField('protocol'),
+          timeout
+        );
 
       const requestStartTime = Date.now();
 
@@ -464,77 +483,48 @@ StripeResource.prototype = {
 
       const maxRetries = this._getMaxNetworkRetries(options.settings);
 
-      req._requestEvent = requestEvent;
-
-      req._requestStart = requestStartTime;
-
       this._stripe._emitter.emit('request', requestEvent);
 
-      req.setTimeout(timeout, this._timeoutHandler(timeout, req, callback));
-
-      req.once('response', (res) => {
-        if (this._shouldRetry(res, requestRetries, maxRetries)) {
-          return retryRequest(
-            makeRequest,
-            apiVersion,
-            headers,
-            requestRetries,
-            ((res || {}).headers || {})['retry-after']
-          );
-        } else {
-          if (options.streaming && res.statusCode < 400) {
-            return this._streamingResponseHandler(req, callback)(res);
+      req
+        .then((res) => {
+          if (this._shouldRetry(res, requestRetries, maxRetries)) {
+            return retryRequest(
+              makeRequest,
+              apiVersion,
+              headers,
+              requestRetries,
+              res.getHeaders()['retry-after']
+            );
+          } else if (options.streaming && res.getStatusCode() < 400) {
+            return this._streamingResponseHandler(requestEvent, callback)(res);
+          } else {
+            return this._jsonResponseHandler(requestEvent, callback)(res);
           }
-          return this._jsonResponseHandler(req, callback)(res);
-        }
-      });
+        })
+        .catch((error) => {
+          if (this._shouldRetry(null, requestRetries, maxRetries)) {
+            return retryRequest(
+              makeRequest,
+              apiVersion,
+              headers,
+              requestRetries,
+              null
+            );
+          } else {
+            const isTimeoutError =
+              error.code && error.code === HttpClient.TIMEOUT_ERROR_CODE;
 
-      req.on('error', (error) => {
-        if (this._shouldRetry(null, requestRetries, maxRetries)) {
-          return retryRequest(
-            makeRequest,
-            apiVersion,
-            headers,
-            requestRetries,
-            null
-          );
-        } else {
-          if (error.code === 'ETIMEDOUT') {
             return callback.call(
               this,
               new StripeConnectionError({
-                message: `Request aborted due to timeout being reached (${timeout}ms)`,
+                message: isTimeoutError
+                  ? `Request aborted due to timeout being reached (${timeout}ms)`
+                  : this._generateConnectionErrorMessage(requestRetries),
                 detail: error,
               })
             );
           }
-          return callback.call(
-            this,
-            new StripeConnectionError({
-              message: this._generateConnectionErrorMessage(requestRetries),
-              detail: error,
-            }),
-            null
-          );
-        }
-      });
-
-      req.once('socket', (socket) => {
-        if (socket.connecting) {
-          socket.once(
-            isInsecureConnection ? 'connect' : 'secureConnect',
-            () => {
-              // Send payload; we're safe:
-              req.write(requestData);
-              req.end();
-            }
-          );
-        } else {
-          // we're already connected
-          req.write(requestData);
-          req.end();
-        }
-      });
+        });
     };
 
     const prepareAndMakeRequest = (error, data) => {
diff --git a/lib/autoPagination.js b/lib/autoPagination.js
index 067079ef..7304a0bc 100644
--- a/lib/autoPagination.js
+++ b/lib/autoPagination.js
@@ -6,15 +6,43 @@ const utils = require('./utils');
 function makeAutoPaginationMethods(self, requestArgs, spec, firstPagePromise) {
   const promiseCache = {currentPromise: null};
   const reverseIteration = isReverseIteration(requestArgs);
-  let listPromise = firstPagePromise;
+  let pagePromise = firstPagePromise;
   let i = 0;
 
-  function iterate(listResult) {
+  // Search and List methods iterate differently.
+  // Search relies on a `next_page` token and can only iterate in one direction.
+  // List relies on either an `ending_before` or `starting_after` field with
+  // an item ID to paginate and is bi-directional.
+  //
+  // Please note: spec.methodType === 'search' is beta functionality and is
+  // subject to change/removal at any time.
+  let getNextPagePromise;
+  if (spec.methodType === 'search') {
+    getNextPagePromise = (pageResult) => {
+      if (!pageResult.next_page) {
+        throw Error(
+          'Unexpected: Stripe API response does not have a well-formed `next_page` field, but `has_more` was true.'
+        );
+      }
+      return makeRequest(self, requestArgs, spec, {
+        next_page: pageResult.next_page,
+      });
+    };
+  } else {
+    getNextPagePromise = (pageResult) => {
+      const lastId = getLastId(pageResult, reverseIteration);
+      return makeRequest(self, requestArgs, spec, {
+        [reverseIteration ? 'ending_before' : 'starting_after']: lastId,
+      });
+    };
+  }
+
+  function iterate(pageResult) {
     if (
       !(
-        listResult &&
-        listResult.data &&
-        typeof listResult.data.length === 'number'
+        pageResult &&
+        pageResult.data &&
+        typeof pageResult.data.length === 'number'
       )
     ) {
       throw Error(
@@ -22,26 +50,24 @@ function makeAutoPaginationMethods(self, requestArgs, spec, firstPagePromise) {
       );
     }
 
-    if (i < listResult.data.length) {
-      const idx = reverseIteration ? listResult.data.length - 1 - i : i;
-      const value = listResult.data[idx];
+    if (i < pageResult.data.length) {
+      const idx = reverseIteration ? pageResult.data.length - 1 - i : i;
+      const value = pageResult.data[idx];
       i += 1;
+
       return {value, done: false};
-    } else if (listResult.has_more) {
+    } else if (pageResult.has_more) {
       // Reset counter, request next page, and recurse.
       i = 0;
-      const lastId = getLastId(listResult, reverseIteration);
-      listPromise = makeRequest(self, requestArgs, spec, {
-        [reverseIteration ? 'ending_before' : 'starting_after']: lastId,
-      });
-      return listPromise.then(iterate);
+      pagePromise = getNextPagePromise(pageResult);
+      return pagePromise.then(iterate);
     }
     return {value: undefined, done: true};
   }
 
   function asyncIteratorNext() {
     return memoizedPromise(promiseCache, (resolve, reject) => {
-      return listPromise
+      return pagePromise
         .then(iterate)
         .then(resolve)
         .catch(reject);
diff --git a/lib/net/HttpClient.js b/lib/net/HttpClient.js
new file mode 100644
index 00000000..407286e3
--- /dev/null
+++ b/lib/net/HttpClient.js
@@ -0,0 +1,65 @@
+'use strict';
+
+/* eslint-disable class-methods-use-this */
+
+/**
+ * Encapsulates the logic for issuing a request to the Stripe API. This is an
+ * experimental interface and is not yet stable.
+ */
+class HttpClient {
+  /** The client name used for diagnostics. */
+  getClientName() {
+    throw new Error('getClientName not implemented.');
+  }
+
+  makeRequest(
+    host,
+    port,
+    path,
+    method,
+    headers,
+    requestData,
+    protocol,
+    timeout
+  ) {
+    throw new Error('makeRequest not implemented.');
+  }
+
+  /** Helper to make a consistent timeout error across implementations. */
+  static makeTimeoutError() {
+    const timeoutErr = new TypeError(HttpClient.TIMEOUT_ERROR_CODE);
+    timeoutErr.code = HttpClient.TIMEOUT_ERROR_CODE;
+    return timeoutErr;
+  }
+}
+
+HttpClient.TIMEOUT_ERROR_CODE = 'ETIMEDOUT';
+
+class HttpClientResponse {
+  constructor(statusCode, headers) {
+    this._statusCode = statusCode;
+    this._headers = headers;
+  }
+
+  getStatusCode() {
+    return this._statusCode;
+  }
+
+  getHeaders() {
+    return this._headers;
+  }
+
+  getRawResponse() {
+    throw new Error('getRawResponse not implemented.');
+  }
+
+  toStream(streamCompleteCallback) {
+    throw new Error('toStream not implemented.');
+  }
+
+  toJSON() {
+    throw new Error('toJSON not implemented.');
+  }
+}
+
+module.exports = {HttpClient, HttpClientResponse};
diff --git a/lib/net/NodeHttpClient.js b/lib/net/NodeHttpClient.js
new file mode 100644
index 00000000..96e0a7ec
--- /dev/null
+++ b/lib/net/NodeHttpClient.js
@@ -0,0 +1,127 @@
+'use strict';
+
+/* eslint-disable class-methods-use-this */
+
+const http = require('http');
+const https = require('https');
+
+const {HttpClient, HttpClientResponse} = require('./HttpClient');
+
+const defaultHttpAgent = new http.Agent({keepAlive: true});
+const defaultHttpsAgent = new https.Agent({keepAlive: true});
+
+/**
+ * HTTP client which uses the Node `http` and `https` packages to issue
+ * requests.`
+ */
+class NodeHttpClient extends HttpClient {
+  constructor(agent) {
+    super();
+    this._agent = agent;
+  }
+
+  /** @override. */
+  getClientName() {
+    return 'node';
+  }
+
+  makeRequest(
+    host,
+    port,
+    path,
+    method,
+    headers,
+    requestData,
+    protocol,
+    timeout
+  ) {
+    const isInsecureConnection = protocol === 'http';
+
+    let agent = this._agent;
+    if (!agent) {
+      agent = isInsecureConnection ? defaultHttpAgent : defaultHttpsAgent;
+    }
+
+    const requestPromise = new Promise((resolve, reject) => {
+      const req = (isInsecureConnection ? http : https).request({
+        host: host,
+        port: port,
+        path,
+        method,
+        agent,
+        headers,
+        ciphers: 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!MD5',
+      });
+
+      req.setTimeout(timeout, () => {
+        req.destroy(HttpClient.makeTimeoutError());
+      });
+
+      req.on('response', (res) => {
+        resolve(new NodeHttpClientResponse(res));
+      });
+
+      req.on('error', (error) => {
+        reject(error);
+      });
+
+      req.once('socket', (socket) => {
+        if (socket.connecting) {
+          socket.once(
+            isInsecureConnection ? 'connect' : 'secureConnect',
+            () => {
+              // Send payload; we're safe:
+              req.write(requestData);
+              req.end();
+            }
+          );
+        } else {
+          // we're already connected
+          req.write(requestData);
+          req.end();
+        }
+      });
+    });
+
+    return requestPromise;
+  }
+}
+
+class NodeHttpClientResponse extends HttpClientResponse {
+  constructor(res) {
+    super(res.statusCode, res.headers || {});
+    this._res = res;
+  }
+
+  getRawResponse() {
+    return this._res;
+  }
+
+  toStream(streamCompleteCallback) {
+    // The raw response is itself the stream, so we just return that. To be
+    // backwards comaptible, we should invoke the streamCompleteCallback only
+    // once the stream has been fully consumed.
+    this._res.once('end', () => streamCompleteCallback());
+    return this._res;
+  }
+
+  toJSON() {
+    return new Promise((resolve, reject) => {
+      let response = '';
+
+      this._res.setEncoding('utf8');
+      this._res.on('data', (chunk) => {
+        response += chunk;
+      });
+      this._res.once('end', () => {
+        try {
+          resolve(JSON.parse(response));
+        } catch (e) {
+          reject(e);
+        }
+      });
+    });
+  }
+}
+
+module.exports = {NodeHttpClient, NodeHttpClientResponse};
diff --git a/lib/resources/AccountLinks.js b/lib/resources/AccountLinks.js
index 4d8769e1..41e02b1e 100644
--- a/lib/resources/AccountLinks.js
+++ b/lib/resources/AccountLinks.js
@@ -3,9 +3,13 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'account_links',
 
-  includeBasic: ['create'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
 });
diff --git a/lib/resources/ApplePayDomains.js b/lib/resources/ApplePayDomains.js
index 86fa7fde..76a8c595 100644
--- a/lib/resources/ApplePayDomains.js
+++ b/lib/resources/ApplePayDomains.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'apple_pay/domains',
 
-  includeBasic: ['create', 'retrieve', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{domain}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{domain}',
+  }),
 });
diff --git a/lib/resources/ApplicationFees.js b/lib/resources/ApplicationFees.js
index e7a40a22..ebd6372a 100644
--- a/lib/resources/ApplicationFees.js
+++ b/lib/resources/ApplicationFees.js
@@ -8,7 +8,16 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'application_fees',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   createRefund: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/BalanceTransactions.js b/lib/resources/BalanceTransactions.js
index 18ae4025..adbe3d41 100644
--- a/lib/resources/BalanceTransactions.js
+++ b/lib/resources/BalanceTransactions.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'balance_transactions',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/BillingPortal/Configurations.js b/lib/resources/BillingPortal/Configurations.js
index 025410c2..ff320016 100644
--- a/lib/resources/BillingPortal/Configurations.js
+++ b/lib/resources/BillingPortal/Configurations.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'billing_portal/configurations',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{configuration}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{configuration}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/BillingPortal/Sessions.js b/lib/resources/BillingPortal/Sessions.js
index 1d294847..04f0ef68 100644
--- a/lib/resources/BillingPortal/Sessions.js
+++ b/lib/resources/BillingPortal/Sessions.js
@@ -3,9 +3,13 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'billing_portal/sessions',
 
-  includeBasic: ['create'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
 });
diff --git a/lib/resources/Charges.js b/lib/resources/Charges.js
index 901ab1af..99ef97d7 100644
--- a/lib/resources/Charges.js
+++ b/lib/resources/Charges.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'charges',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{charge}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{charge}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   capture: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Checkout/Sessions.js b/lib/resources/Checkout/Sessions.js
index 46a30898..91192db8 100644
--- a/lib/resources/Checkout/Sessions.js
+++ b/lib/resources/Checkout/Sessions.js
@@ -8,7 +8,21 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'checkout/sessions',
 
-  includeBasic: ['create', 'retrieve', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{session}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   listLineItems: stripeMethod({
     method: 'GET',
diff --git a/lib/resources/CountrySpecs.js b/lib/resources/CountrySpecs.js
index bf541db2..789f9905 100644
--- a/lib/resources/CountrySpecs.js
+++ b/lib/resources/CountrySpecs.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'country_specs',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{country}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Coupons.js b/lib/resources/Coupons.js
index 4787164c..11bed027 100644
--- a/lib/resources/Coupons.js
+++ b/lib/resources/Coupons.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'coupons',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{coupon}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{coupon}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{coupon}',
+  }),
 });
diff --git a/lib/resources/CreditNotes.js b/lib/resources/CreditNotes.js
index 735f1746..512e5a7c 100644
--- a/lib/resources/CreditNotes.js
+++ b/lib/resources/CreditNotes.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'credit_notes',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   preview: stripeMethod({
     method: 'GET',
diff --git a/lib/resources/Customers.js b/lib/resources/Customers.js
index 6e0cb080..208f4a13 100644
--- a/lib/resources/Customers.js
+++ b/lib/resources/Customers.js
@@ -8,7 +8,31 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'customers',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{customer}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{customer}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{customer}',
+  }),
 
   deleteDiscount: stripeMethod({
     method: 'DELETE',
diff --git a/lib/resources/Disputes.js b/lib/resources/Disputes.js
index e9ac3b74..13187589 100644
--- a/lib/resources/Disputes.js
+++ b/lib/resources/Disputes.js
@@ -8,7 +8,21 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'disputes',
 
-  includeBasic: ['retrieve', 'update', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{dispute}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{dispute}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   close: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/EphemeralKeys.js b/lib/resources/EphemeralKeys.js
index cfe14083..2f9b0269 100644
--- a/lib/resources/EphemeralKeys.js
+++ b/lib/resources/EphemeralKeys.js
@@ -8,8 +8,6 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'ephemeral_keys',
 
-  includeBasic: ['del'],
-
   create: stripeMethod({
     method: 'POST',
     path: '',
@@ -21,4 +19,9 @@ module.exports = StripeResource.extend({
       }
     },
   }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{key}',
+  }),
 });
diff --git a/lib/resources/Events.js b/lib/resources/Events.js
index 4c31e5f6..79ee31d4 100644
--- a/lib/resources/Events.js
+++ b/lib/resources/Events.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'events',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/ExchangeRates.js b/lib/resources/ExchangeRates.js
index df3f658a..021e9723 100644
--- a/lib/resources/ExchangeRates.js
+++ b/lib/resources/ExchangeRates.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'exchange_rates',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{rateId}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/FileLinks.js b/lib/resources/FileLinks.js
index 7cdcfec5..7c7a6e11 100644
--- a/lib/resources/FileLinks.js
+++ b/lib/resources/FileLinks.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'file_links',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{link}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{link}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Files.js b/lib/resources/Files.js
index a407079f..5dee82d1 100644
--- a/lib/resources/Files.js
+++ b/lib/resources/Files.js
@@ -9,8 +9,6 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'files',
 
-  includeBasic: ['retrieve', 'list'],
-
   create: stripeMethod({
     method: 'POST',
     headers: {
@@ -19,5 +17,16 @@ module.exports = StripeResource.extend({
     host: 'files.stripe.com',
   }),
 
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{file}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
   requestDataProcessor: multipartRequestDataProcessor,
 });
diff --git a/lib/resources/Identity/VerificationReports.js b/lib/resources/Identity/VerificationReports.js
index bdd300f9..6d78faa7 100644
--- a/lib/resources/Identity/VerificationReports.js
+++ b/lib/resources/Identity/VerificationReports.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'identity/verification_reports',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{report}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Identity/VerificationSessions.js b/lib/resources/Identity/VerificationSessions.js
index fc515b88..02345e12 100644
--- a/lib/resources/Identity/VerificationSessions.js
+++ b/lib/resources/Identity/VerificationSessions.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'identity/verification_sessions',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{session}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{session}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   cancel: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/InvoiceItems.js b/lib/resources/InvoiceItems.js
index b6a36e07..c7cb5586 100644
--- a/lib/resources/InvoiceItems.js
+++ b/lib/resources/InvoiceItems.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'invoiceitems',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{invoiceitem}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{invoiceitem}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{invoiceitem}',
+  }),
 });
diff --git a/lib/resources/Invoices.js b/lib/resources/Invoices.js
index 03bf3f60..489190f8 100644
--- a/lib/resources/Invoices.js
+++ b/lib/resources/Invoices.js
@@ -8,7 +8,31 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'invoices',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{invoice}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{invoice}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{invoice}',
+  }),
 
   finalizeInvoice: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/IssuerFraudRecords.js b/lib/resources/IssuerFraudRecords.js
index b41669c5..c94ee2ff 100644
--- a/lib/resources/IssuerFraudRecords.js
+++ b/lib/resources/IssuerFraudRecords.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'issuer_fraud_records',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{issuerFraudRecord}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Issuing/Authorizations.js b/lib/resources/Issuing/Authorizations.js
index d10043ec..5310d0ea 100644
--- a/lib/resources/Issuing/Authorizations.js
+++ b/lib/resources/Issuing/Authorizations.js
@@ -8,7 +8,21 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'issuing/authorizations',
 
-  includeBasic: ['retrieve', 'update', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{authorization}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{authorization}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   approve: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Issuing/Cardholders.js b/lib/resources/Issuing/Cardholders.js
index 0c4e9682..7b340e69 100644
--- a/lib/resources/Issuing/Cardholders.js
+++ b/lib/resources/Issuing/Cardholders.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'issuing/cardholders',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{cardholder}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{cardholder}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Issuing/Cards.js b/lib/resources/Issuing/Cards.js
index 92594a2f..b94aeaea 100644
--- a/lib/resources/Issuing/Cards.js
+++ b/lib/resources/Issuing/Cards.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'issuing/cards',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{card}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{card}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   retrieveDetails: stripeMethod({
     method: 'GET',
diff --git a/lib/resources/Issuing/Disputes.js b/lib/resources/Issuing/Disputes.js
index a2f169ec..f62a2be9 100644
--- a/lib/resources/Issuing/Disputes.js
+++ b/lib/resources/Issuing/Disputes.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'issuing/disputes',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{dispute}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{dispute}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   submit: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Issuing/Transactions.js b/lib/resources/Issuing/Transactions.js
index f7c15b7b..22b43842 100644
--- a/lib/resources/Issuing/Transactions.js
+++ b/lib/resources/Issuing/Transactions.js
@@ -3,9 +3,24 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'issuing/transactions',
 
-  includeBasic: ['retrieve', 'update', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{transaction}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{transaction}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Mandates.js b/lib/resources/Mandates.js
index 103ebbac..6d9ced9f 100644
--- a/lib/resources/Mandates.js
+++ b/lib/resources/Mandates.js
@@ -3,9 +3,13 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'mandates',
 
-  includeBasic: ['retrieve'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{mandate}',
+  }),
 });
diff --git a/lib/resources/OrderReturns.js b/lib/resources/OrderReturns.js
index b4297f82..4a548183 100644
--- a/lib/resources/OrderReturns.js
+++ b/lib/resources/OrderReturns.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'order_returns',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Orders.js b/lib/resources/Orders.js
index 9033abad..df456424 100644
--- a/lib/resources/Orders.js
+++ b/lib/resources/Orders.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'orders',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   pay: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/PaymentIntents.js b/lib/resources/PaymentIntents.js
index d99b43b1..42f6cddb 100644
--- a/lib/resources/PaymentIntents.js
+++ b/lib/resources/PaymentIntents.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'payment_intents',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{intent}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{intent}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   cancel: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/PaymentMethods.js b/lib/resources/PaymentMethods.js
index f08d8427..bd74b2ee 100644
--- a/lib/resources/PaymentMethods.js
+++ b/lib/resources/PaymentMethods.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'payment_methods',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{paymentMethod}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{paymentMethod}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   attach: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Payouts.js b/lib/resources/Payouts.js
index df134c5a..a3b8e0cd 100644
--- a/lib/resources/Payouts.js
+++ b/lib/resources/Payouts.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'payouts',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{payout}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{payout}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   cancel: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Plans.js b/lib/resources/Plans.js
index 51900780..bd55a157 100644
--- a/lib/resources/Plans.js
+++ b/lib/resources/Plans.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'plans',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{plan}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{plan}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{plan}',
+  }),
 });
diff --git a/lib/resources/Prices.js b/lib/resources/Prices.js
index 59db3192..862e946d 100644
--- a/lib/resources/Prices.js
+++ b/lib/resources/Prices.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'prices',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{price}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{price}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Products.js b/lib/resources/Products.js
index f4af5a20..ba46de32 100644
--- a/lib/resources/Products.js
+++ b/lib/resources/Products.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'products',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{id}',
+  }),
 });
diff --git a/lib/resources/PromotionCodes.js b/lib/resources/PromotionCodes.js
index 9fe56a11..c8afa0e4 100644
--- a/lib/resources/PromotionCodes.js
+++ b/lib/resources/PromotionCodes.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'promotion_codes',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{promotionCode}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{promotionCode}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Quotes.js b/lib/resources/Quotes.js
index e3988122..62aebb65 100644
--- a/lib/resources/Quotes.js
+++ b/lib/resources/Quotes.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'quotes',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{quote}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{quote}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   accept: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Radar/EarlyFraudWarnings.js b/lib/resources/Radar/EarlyFraudWarnings.js
index 33bad28c..9282648d 100644
--- a/lib/resources/Radar/EarlyFraudWarnings.js
+++ b/lib/resources/Radar/EarlyFraudWarnings.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'radar/early_fraud_warnings',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{earlyFraudWarning}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Radar/ValueListItems.js b/lib/resources/Radar/ValueListItems.js
index 908c760e..75bcb894 100644
--- a/lib/resources/Radar/ValueListItems.js
+++ b/lib/resources/Radar/ValueListItems.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'radar/value_list_items',
 
-  includeBasic: ['create', 'retrieve', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{item}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{item}',
+  }),
 });
diff --git a/lib/resources/Radar/ValueLists.js b/lib/resources/Radar/ValueLists.js
index 38192f8b..ace1edaa 100644
--- a/lib/resources/Radar/ValueLists.js
+++ b/lib/resources/Radar/ValueLists.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'radar/value_lists',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{valueList}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{valueList}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{valueList}',
+  }),
 });
diff --git a/lib/resources/Refunds.js b/lib/resources/Refunds.js
index 2ec7b90a..f1c0c130 100644
--- a/lib/resources/Refunds.js
+++ b/lib/resources/Refunds.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'refunds',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{refund}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{refund}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Reporting/ReportRuns.js b/lib/resources/Reporting/ReportRuns.js
index 895a5fda..84eb179f 100644
--- a/lib/resources/Reporting/ReportRuns.js
+++ b/lib/resources/Reporting/ReportRuns.js
@@ -3,9 +3,24 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'reporting/report_runs',
 
-  includeBasic: ['create', 'retrieve', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{reportRun}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Reporting/ReportTypes.js b/lib/resources/Reporting/ReportTypes.js
index bd51d34a..b4b25f80 100644
--- a/lib/resources/Reporting/ReportTypes.js
+++ b/lib/resources/Reporting/ReportTypes.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'reporting/report_types',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{reportType}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Reviews.js b/lib/resources/Reviews.js
index 003368ba..9400e8b8 100644
--- a/lib/resources/Reviews.js
+++ b/lib/resources/Reviews.js
@@ -8,7 +8,16 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'reviews',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{review}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   approve: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/SKUs.js b/lib/resources/SKUs.js
index 83dd16ac..26812405 100644
--- a/lib/resources/SKUs.js
+++ b/lib/resources/SKUs.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'skus',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{id}',
+  }),
 });
diff --git a/lib/resources/SetupAttempts.js b/lib/resources/SetupAttempts.js
index 3d24d9f3..9bb8c1d3 100644
--- a/lib/resources/SetupAttempts.js
+++ b/lib/resources/SetupAttempts.js
@@ -3,9 +3,14 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'setup_attempts',
 
-  includeBasic: ['list'],
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/SetupIntents.js b/lib/resources/SetupIntents.js
index ff22fe5b..0f5ed8ec 100644
--- a/lib/resources/SetupIntents.js
+++ b/lib/resources/SetupIntents.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'setup_intents',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{intent}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{intent}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   cancel: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Sigma/ScheduledQueryRuns.js b/lib/resources/Sigma/ScheduledQueryRuns.js
index 4c8a0999..a8cb8e60 100644
--- a/lib/resources/Sigma/ScheduledQueryRuns.js
+++ b/lib/resources/Sigma/ScheduledQueryRuns.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'sigma/scheduled_query_runs',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{scheduledQueryRun}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Sources.js b/lib/resources/Sources.js
index f1fd8538..5008001c 100644
--- a/lib/resources/Sources.js
+++ b/lib/resources/Sources.js
@@ -8,7 +8,20 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'sources',
 
-  includeBasic: ['create', 'retrieve', 'update'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{source}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{source}',
+  }),
 
   listSourceTransactions: stripeMethod({
     method: 'GET',
diff --git a/lib/resources/SubscriptionItems.js b/lib/resources/SubscriptionItems.js
index 74a90858..62423a22 100644
--- a/lib/resources/SubscriptionItems.js
+++ b/lib/resources/SubscriptionItems.js
@@ -8,7 +8,31 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'subscription_items',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{item}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{item}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{item}',
+  }),
 
   createUsageRecord: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/SubscriptionSchedules.js b/lib/resources/SubscriptionSchedules.js
index 9eabb142..9c9249e3 100644
--- a/lib/resources/SubscriptionSchedules.js
+++ b/lib/resources/SubscriptionSchedules.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'subscription_schedules',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{schedule}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{schedule}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   cancel: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Subscriptions.js b/lib/resources/Subscriptions.js
index b701e45a..03dee403 100644
--- a/lib/resources/Subscriptions.js
+++ b/lib/resources/Subscriptions.js
@@ -8,7 +8,31 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'subscriptions',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{subscriptionExposedId}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{subscriptionExposedId}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{subscriptionExposedId}',
+  }),
 
   deleteDiscount: stripeMethod({
     method: 'DELETE',
diff --git a/lib/resources/TaxCodes.js b/lib/resources/TaxCodes.js
index f63479e2..57bf52ff 100644
--- a/lib/resources/TaxCodes.js
+++ b/lib/resources/TaxCodes.js
@@ -3,9 +3,19 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'tax_codes',
 
-  includeBasic: ['retrieve', 'list'],
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{id}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/TaxRates.js b/lib/resources/TaxRates.js
index 041ac456..e429895f 100644
--- a/lib/resources/TaxRates.js
+++ b/lib/resources/TaxRates.js
@@ -3,9 +3,29 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'tax_rates',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{taxRate}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{taxRate}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 });
diff --git a/lib/resources/Terminal/ConnectionTokens.js b/lib/resources/Terminal/ConnectionTokens.js
index eee06765..2e1a1b51 100644
--- a/lib/resources/Terminal/ConnectionTokens.js
+++ b/lib/resources/Terminal/ConnectionTokens.js
@@ -3,9 +3,13 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'terminal/connection_tokens',
 
-  includeBasic: ['create'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
 });
diff --git a/lib/resources/Terminal/Locations.js b/lib/resources/Terminal/Locations.js
index cb216923..161e0dd0 100644
--- a/lib/resources/Terminal/Locations.js
+++ b/lib/resources/Terminal/Locations.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'terminal/locations',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{location}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{location}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{location}',
+  }),
 });
diff --git a/lib/resources/Terminal/Readers.js b/lib/resources/Terminal/Readers.js
index be5f1348..e0f9d8f5 100644
--- a/lib/resources/Terminal/Readers.js
+++ b/lib/resources/Terminal/Readers.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'terminal/readers',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{reader}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{reader}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{reader}',
+  }),
 });
diff --git a/lib/resources/Tokens.js b/lib/resources/Tokens.js
index 35344090..76e4d337 100644
--- a/lib/resources/Tokens.js
+++ b/lib/resources/Tokens.js
@@ -3,9 +3,18 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'tokens',
 
-  includeBasic: ['create', 'retrieve'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{token}',
+  }),
 });
diff --git a/lib/resources/Topups.js b/lib/resources/Topups.js
index f80dc773..e65fe5d1 100644
--- a/lib/resources/Topups.js
+++ b/lib/resources/Topups.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'topups',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{topup}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{topup}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   cancel: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/Transfers.js b/lib/resources/Transfers.js
index e2a810c4..656128c3 100644
--- a/lib/resources/Transfers.js
+++ b/lib/resources/Transfers.js
@@ -8,7 +8,26 @@ const stripeMethod = StripeResource.method;
 module.exports = StripeResource.extend({
   path: 'transfers',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{transfer}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{transfer}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
 
   createReversal: stripeMethod({
     method: 'POST',
diff --git a/lib/resources/WebhookEndpoints.js b/lib/resources/WebhookEndpoints.js
index e026cf13..2f73d650 100644
--- a/lib/resources/WebhookEndpoints.js
+++ b/lib/resources/WebhookEndpoints.js
@@ -3,9 +3,34 @@
 'use strict';
 
 const StripeResource = require('../StripeResource');
+const stripeMethod = StripeResource.method;
 
 module.exports = StripeResource.extend({
   path: 'webhook_endpoints',
 
-  includeBasic: ['create', 'retrieve', 'update', 'list', 'del'],
+  create: stripeMethod({
+    method: 'POST',
+    path: '',
+  }),
+
+  retrieve: stripeMethod({
+    method: 'GET',
+    path: '/{webhookEndpoint}',
+  }),
+
+  update: stripeMethod({
+    method: 'POST',
+    path: '/{webhookEndpoint}',
+  }),
+
+  list: stripeMethod({
+    method: 'GET',
+    path: '',
+    methodType: 'list',
+  }),
+
+  del: stripeMethod({
+    method: 'DELETE',
+    path: '/{webhookEndpoint}',
+  }),
 });
diff --git a/lib/stripe.js b/lib/stripe.js
index 814c7fea..19ae1b07 100644
--- a/lib/stripe.js
+++ b/lib/stripe.js
@@ -11,17 +11,20 @@ const DEFAULT_TIMEOUT = 80000;
 
 Stripe.PACKAGE_VERSION = require('../package.json').version;
 
+const utils = require('./utils');
+const {determineProcessUserAgentProperties, emitWarning} = utils;
+
 Stripe.USER_AGENT = {
   bindings_version: Stripe.PACKAGE_VERSION,
   lang: 'node',
-  lang_version: process.version,
-  platform: process.platform,
   publisher: 'stripe',
   uname: null,
   typescript: false,
+  ...determineProcessUserAgentProperties(),
 };
 
-Stripe.USER_AGENT_SERIALIZED = null;
+/** @private */
+Stripe._UNAME_CACHE = null;
 
 const MAX_NETWORK_RETRY_DELAY_SEC = 2;
 const INITIAL_NETWORK_RETRY_DELAY_SEC = 0.5;
@@ -32,6 +35,7 @@ const ALLOWED_CONFIG_PROPERTIES = [
   'typescript',
   'maxNetworkRetries',
   'httpAgent',
+  'httpClient',
   'timeout',
   'host',
   'port',
@@ -42,12 +46,14 @@ const ALLOWED_CONFIG_PROPERTIES = [
 ];
 
 const EventEmitter = require('events').EventEmitter;
-const utils = require('./utils');
-const {emitWarning} = utils;
 
 Stripe.StripeResource = require('./StripeResource');
 Stripe.resources = resources;
 
+const {HttpClient, HttpClientResponse} = require('./net/HttpClient');
+Stripe.HttpClient = HttpClient;
+Stripe.HttpClientResponse = HttpClientResponse;
+
 function Stripe(key, config = {}) {
   if (!(this instanceof Stripe)) {
     return new Stripe(key, config);
@@ -78,6 +84,8 @@ function Stripe(key, config = {}) {
     );
   }
 
+  const agent = props.httpAgent || null;
+
   this._api = {
     auth: null,
     host: props.host || DEFAULT_HOST,
@@ -91,7 +99,8 @@ function Stripe(key, config = {}) {
       props.maxNetworkRetries,
       0
     ),
-    agent: props.httpAgent || null,
+    agent: agent,
+    httpClient: props.httpClient || Stripe.createNodeHttpClient(agent),
     dev: false,
     stripeAccount: props.stripeAccount || null,
   };
@@ -102,7 +111,6 @@ function Stripe(key, config = {}) {
     // serializing the user agent involves shelling out to the system,
     // and given some users may instantiate the library many times without switching between TS and non-TS,
     // we only want to incur the performance hit when that actually happens.
-    Stripe.USER_AGENT_SERIALIZED = null;
     Stripe.USER_AGENT.typescript = typescript;
   }
 
@@ -126,6 +134,11 @@ function Stripe(key, config = {}) {
 Stripe.errors = require('./Error');
 Stripe.webhooks = require('./Webhooks');
 
+Stripe.createNodeHttpClient = (agent) => {
+  const {NodeHttpClient} = require('./net/NodeHttpClient');
+  return new NodeHttpClient(agent);
+};
+
 Stripe.prototype = {
   /**
    * @deprecated will be removed in a future major version. Use the config object instead:
@@ -286,9 +299,6 @@ Stripe.prototype = {
       return accum;
     }, undefined);
 
-    // Kill the cached UA string because it may no longer be valid
-    Stripe.USER_AGENT_SERIALIZED = undefined;
-
     this._appInfo = appInfo;
   },
 
@@ -396,6 +406,20 @@ Stripe.prototype = {
     return INITIAL_NETWORK_RETRY_DELAY_SEC;
   },
 
+  /**
+   * @private
+   */
+  getUname(cb) {
+    if (!Stripe._UNAME_CACHE) {
+      Stripe._UNAME_CACHE = new Promise((resolve) => {
+        utils.safeExec('uname -a', (err, uname) => {
+          resolve(uname);
+        });
+      });
+    }
+    Stripe._UNAME_CACHE.then((uname) => cb(uname));
+  },
+
   /**
    * @private
    * Please open or upvote an issue at github.com/stripe/stripe-node
@@ -407,13 +431,7 @@ Stripe.prototype = {
    * speed advantage.
    */
   getClientUserAgent(cb) {
-    if (Stripe.USER_AGENT_SERIALIZED) {
-      return cb(Stripe.USER_AGENT_SERIALIZED);
-    }
-    this.getClientUserAgentSeeded(Stripe.USER_AGENT, (cua) => {
-      Stripe.USER_AGENT_SERIALIZED = cua;
-      cb(Stripe.USER_AGENT_SERIALIZED);
-    });
+    return this.getClientUserAgentSeeded(Stripe.USER_AGENT, cb);
   },
 
   /**
@@ -427,7 +445,7 @@ Stripe.prototype = {
    * fetching a uname from the system.
    */
   getClientUserAgentSeeded(seed, cb) {
-    utils.safeExec('uname -a', (err, uname) => {
+    this.getUname((uname) => {
       const userAgent = {};
       for (const field in seed) {
         userAgent[field] = encodeURIComponent(seed[field]);
@@ -436,6 +454,11 @@ Stripe.prototype = {
       // URI-encode in case there are unusual characters in the system's uname.
       userAgent.uname = encodeURIComponent(uname || 'UNKNOWN');
 
+      const client = this.getApiField('httpClient');
+      if (client) {
+        userAgent.httplib = encodeURIComponent(client.getClientName());
+      }
+
       if (this._appInfo) {
         userAgent.application = this._appInfo;
       }
diff --git a/lib/utils.js b/lib/utils.js
index ae4c0047..7eed41de 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -429,6 +429,15 @@ const utils = (module.exports = {
 
     return n;
   },
+
+  determineProcessUserAgentProperties: () => {
+    return typeof process === 'undefined'
+      ? {}
+      : {
+          lang_version: process.version,
+          platform: process.platform,
+        };
+  },
 });
 
 function emitWarning(warning) {
diff --git a/package.json b/package.json
index 7a696536..7d9ea303 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "stripe",
-  "version": "8.166.0",
+  "version": "8.171.0",
   "description": "Stripe API wrapper",
   "keywords": [
     "stripe",
@@ -38,7 +38,7 @@
     "eslint-plugin-prettier": "^3.0.1",
     "mocha": "^8.3.2",
     "mocha-junit-reporter": "^1.23.1",
-    "nock": "^10.0.6",
+    "nock": "^13.1.1",
     "nyc": "^15.1.0",
     "prettier": "^1.16.4",
     "typescript": "^3.7.2"
diff --git a/types/2020-08-27/BillingPortal/Configurations.d.ts b/types/2020-08-27/BillingPortal/Configurations.d.ts
index 8b53ec9f..6bdfc447 100644
--- a/types/2020-08-27/BillingPortal/Configurations.d.ts
+++ b/types/2020-08-27/BillingPortal/Configurations.d.ts
@@ -126,6 +126,8 @@ declare module 'stripe' {
           }
 
           interface SubscriptionCancel {
+            cancellation_reason?: SubscriptionCancel.CancellationReason;
+
             /**
              * Whether the feature is enabled.
              */
@@ -143,6 +145,30 @@ declare module 'stripe' {
           }
 
           namespace SubscriptionCancel {
+            interface CancellationReason {
+              /**
+               * Whether the feature is enabled.
+               */
+              enabled: boolean;
+
+              /**
+               * Which cancellation reasons will be given as options to the customer.
+               */
+              options: Array<CancellationReason.Option>;
+            }
+
+            namespace CancellationReason {
+              type Option =
+                | 'customer_service'
+                | 'low_quality'
+                | 'missing_features'
+                | 'other'
+                | 'switched_service'
+                | 'too_complex'
+                | 'too_expensive'
+                | 'unused';
+            }
+
             type Mode = 'at_period_end' | 'immediately';
 
             type ProrationBehavior =
@@ -316,6 +342,11 @@ declare module 'stripe' {
           }
 
           interface SubscriptionCancel {
+            /**
+             * Whether the cancellation reasons will be collected in the portal and which options are exposed to the customer
+             */
+            cancellation_reason?: SubscriptionCancel.CancellationReason;
+
             /**
              * Whether the feature is enabled.
              */
@@ -333,6 +364,30 @@ declare module 'stripe' {
           }
 
           namespace SubscriptionCancel {
+            interface CancellationReason {
+              /**
+               * Whether the feature is enabled.
+               */
+              enabled: boolean;
+
+              /**
+               * Which cancellation reasons will be given as options to the customer.
+               */
+              options: Stripe.Emptyable<Array<CancellationReason.Option>>;
+            }
+
+            namespace CancellationReason {
+              type Option =
+                | 'customer_service'
+                | 'low_quality'
+                | 'missing_features'
+                | 'other'
+                | 'switched_service'
+                | 'too_complex'
+                | 'too_expensive'
+                | 'unused';
+            }
+
             type Mode = 'at_period_end' | 'immediately';
 
             type ProrationBehavior =
@@ -518,6 +573,11 @@ declare module 'stripe' {
           }
 
           interface SubscriptionCancel {
+            /**
+             * Whether the cancellation reasons will be collected in the portal and which options are exposed to the customer
+             */
+            cancellation_reason?: SubscriptionCancel.CancellationReason;
+
             /**
              * Whether the feature is enabled.
              */
@@ -535,6 +595,30 @@ declare module 'stripe' {
           }
 
           namespace SubscriptionCancel {
+            interface CancellationReason {
+              /**
+               * Whether the feature is enabled.
+               */
+              enabled: boolean;
+
+              /**
+               * Which cancellation reasons will be given as options to the customer.
+               */
+              options?: Stripe.Emptyable<Array<CancellationReason.Option>>;
+            }
+
+            namespace CancellationReason {
+              type Option =
+                | 'customer_service'
+                | 'low_quality'
+                | 'missing_features'
+                | 'other'
+                | 'switched_service'
+                | 'too_complex'
+                | 'too_expensive'
+                | 'unused';
+            }
+
             type Mode = 'at_period_end' | 'immediately';
 
             type ProrationBehavior =
diff --git a/types/2020-08-27/BillingPortal/Sessions.d.ts b/types/2020-08-27/BillingPortal/Sessions.d.ts
index d6ebca26..8ab93c88 100644
--- a/types/2020-08-27/BillingPortal/Sessions.d.ts
+++ b/types/2020-08-27/BillingPortal/Sessions.d.ts
@@ -37,6 +37,11 @@ declare module 'stripe' {
          */
         livemode: boolean;
 
+        /**
+         * The IETF language tag of the locale Customer Portal is displayed in. If blank or auto, the customer's `preferred_locales` or browser's locale is used.
+         */
+        locale: Session.Locale | null;
+
         /**
          * The account for which the session was created on behalf of. When specified, only subscriptions and invoices with this `on_behalf_of` account appear in the portal. For more information, see the [docs](https://stripe.com/docs/connect/charges-transfers#on-behalf-of). Use the [Accounts API](https://stripe.com/docs/api/accounts/object#account_object-settings-branding) to modify the `on_behalf_of` account's branding settings, which the portal displays.
          */
@@ -53,6 +58,57 @@ declare module 'stripe' {
         url: string;
       }
 
+      namespace Session {
+        type Locale =
+          | 'auto'
+          | 'bg'
+          | 'cs'
+          | 'da'
+          | 'de'
+          | 'el'
+          | 'en'
+          | 'en-AU'
+          | 'en-CA'
+          | 'en-GB'
+          | 'en-IE'
+          | 'en-IN'
+          | 'en-NZ'
+          | 'en-SG'
+          | 'es'
+          | 'es-419'
+          | 'et'
+          | 'fi'
+          | 'fil'
+          | 'fr'
+          | 'fr-CA'
+          | 'hr'
+          | 'hu'
+          | 'id'
+          | 'it'
+          | 'ja'
+          | 'ko'
+          | 'lt'
+          | 'lv'
+          | 'ms'
+          | 'mt'
+          | 'nb'
+          | 'nl'
+          | 'pl'
+          | 'pt'
+          | 'pt-BR'
+          | 'ro'
+          | 'ru'
+          | 'sk'
+          | 'sl'
+          | 'sv'
+          | 'th'
+          | 'tr'
+          | 'vi'
+          | 'zh'
+          | 'zh-HK'
+          | 'zh-TW';
+      }
+
       interface SessionCreateParams {
         /**
          * The ID of an existing customer.
@@ -69,6 +125,11 @@ declare module 'stripe' {
          */
         expand?: Array<string>;
 
+        /**
+         * The IETF language tag of the locale Customer Portal is displayed in. If blank or auto, the customer's `preferred_locales` or browser's locale is used.
+         */
+        locale?: SessionCreateParams.Locale;
+
         /**
          * The `on_behalf_of` account to use for this session. When specified, only subscriptions and invoices with this `on_behalf_of` account appear in the portal. For more information, see the [docs](https://stripe.com/docs/connect/charges-transfers#on-behalf-of). Use the [Accounts API](https://stripe.com/docs/api/accounts/object#account_object-settings-branding) to modify the `on_behalf_of` account's branding settings, which the portal displays.
          */
@@ -80,6 +141,57 @@ declare module 'stripe' {
         return_url?: string;
       }
 
+      namespace SessionCreateParams {
+        type Locale =
+          | 'auto'
+          | 'bg'
+          | 'cs'
+          | 'da'
+          | 'de'
+          | 'el'
+          | 'en'
+          | 'en-AU'
+          | 'en-CA'
+          | 'en-GB'
+          | 'en-IE'
+          | 'en-IN'
+          | 'en-NZ'
+          | 'en-SG'
+          | 'es'
+          | 'es-419'
+          | 'et'
+          | 'fi'
+          | 'fil'
+          | 'fr'
+          | 'fr-CA'
+          | 'hr'
+          | 'hu'
+          | 'id'
+          | 'it'
+          | 'ja'
+          | 'ko'
+          | 'lt'
+          | 'lv'
+          | 'ms'
+          | 'mt'
+          | 'nb'
+          | 'nl'
+          | 'pl'
+          | 'pt'
+          | 'pt-BR'
+          | 'ro'
+          | 'ru'
+          | 'sk'
+          | 'sl'
+          | 'sv'
+          | 'th'
+          | 'tr'
+          | 'vi'
+          | 'zh'
+          | 'zh-HK'
+          | 'zh-TW';
+      }
+
       class SessionsResource {
         /**
          * Creates a session of the customer portal.
diff --git a/types/2020-08-27/Checkout/Sessions.d.ts b/types/2020-08-27/Checkout/Sessions.d.ts
index c2ce48ae..76a44745 100644
--- a/types/2020-08-27/Checkout/Sessions.d.ts
+++ b/types/2020-08-27/Checkout/Sessions.d.ts
@@ -215,7 +215,7 @@ declare module 'stripe' {
 
           interface TaxId {
             /**
-             * The type of the tax ID, one of `eu_vat`, `br_cnpj`, `br_cpf`, `gb_vat`, `nz_gst`, `au_abn`, `in_gst`, `no_vat`, `za_vat`, `ch_vat`, `mx_rfc`, `sg_uen`, `ru_inn`, `ru_kpp`, `ca_bn`, `hk_br`, `es_cif`, `tw_vat`, `th_vat`, `jp_cn`, `jp_rn`, `li_uid`, `my_itn`, `us_ein`, `kr_brn`, `ca_qst`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `my_sst`, `sg_gst`, `ae_trn`, `cl_tin`, `sa_vat`, `id_npwp`, `my_frp`, `il_vat`, or `unknown`
+             * The type of the tax ID, one of `eu_vat`, `br_cnpj`, `br_cpf`, `gb_vat`, `nz_gst`, `au_abn`, `au_arn`, `in_gst`, `no_vat`, `za_vat`, `ch_vat`, `mx_rfc`, `sg_uen`, `ru_inn`, `ru_kpp`, `ca_bn`, `hk_br`, `es_cif`, `tw_vat`, `th_vat`, `jp_cn`, `jp_rn`, `li_uid`, `my_itn`, `us_ein`, `kr_brn`, `ca_qst`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `my_sst`, `sg_gst`, `ae_trn`, `cl_tin`, `sa_vat`, `id_npwp`, `my_frp`, `il_vat`, or `unknown`
              */
             type: TaxId.Type;
 
@@ -229,6 +229,7 @@ declare module 'stripe' {
             type Type =
               | 'ae_trn'
               | 'au_abn'
+              | 'au_arn'
               | 'br_cnpj'
               | 'br_cpf'
               | 'ca_bn'
@@ -282,6 +283,7 @@ declare module 'stripe' {
           | 'es-419'
           | 'et'
           | 'fi'
+          | 'fil'
           | 'fr'
           | 'fr-CA'
           | 'hr'
@@ -341,7 +343,7 @@ declare module 'stripe' {
               custom_mandate_url?: string;
 
               /**
-               * Description of the interval. Only required if 'payment_schedule' parmeter is 'interval' or 'combined'.
+               * Description of the interval. Only required if the 'payment_schedule' parameter is 'interval' or 'combined'.
                */
               interval_description: string | null;
 
@@ -749,6 +751,9 @@ declare module 'stripe' {
          */
         allow_promotion_codes?: boolean;
 
+        /**
+         * Settings for automatic tax lookup for this session and resulting payments, invoices, and subscriptions.
+         */
         automatic_tax?: SessionCreateParams.AutomaticTax;
 
         /**
@@ -1105,6 +1110,7 @@ declare module 'stripe' {
           | 'es-419'
           | 'et'
           | 'fi'
+          | 'fil'
           | 'fr'
           | 'fr-CA'
           | 'hr'
diff --git a/types/2020-08-27/Customers.d.ts b/types/2020-08-27/Customers.d.ts
index e8abe4b7..ae53e124 100644
--- a/types/2020-08-27/Customers.d.ts
+++ b/types/2020-08-27/Customers.d.ts
@@ -420,7 +420,7 @@ declare module 'stripe' {
 
       interface TaxIdDatum {
         /**
-         * Type of the tax ID, one of `ae_trn`, `au_abn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
+         * Type of the tax ID, one of `ae_trn`, `au_abn`, `au_arn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
          */
         type: TaxIdDatum.Type;
 
@@ -434,6 +434,7 @@ declare module 'stripe' {
         type Type =
           | 'ae_trn'
           | 'au_abn'
+          | 'au_arn'
           | 'br_cnpj'
           | 'br_cpf'
           | 'ca_bn'
diff --git a/types/2020-08-27/Identity/VerificationSessions.d.ts b/types/2020-08-27/Identity/VerificationSessions.d.ts
index e388cada..1fd2b8a1 100644
--- a/types/2020-08-27/Identity/VerificationSessions.d.ts
+++ b/types/2020-08-27/Identity/VerificationSessions.d.ts
@@ -68,7 +68,7 @@ declare module 'stripe' {
         type: VerificationSession.Type;
 
         /**
-         * The short-lived URL that you use to redirect a user to Stripe to submit their identity information. This URL expires after 24 hours and can only be used once. Don't store it, log it, send it in emails or expose it to anyone other than the user. Refer to our docs on [verifying identity documents](https://stripe.com/docs/identity/verify-identity-documents?platform=web&type=redirect) to learn how to redirect users to Stripe.
+         * The short-lived URL that you use to redirect a user to Stripe to submit their identity information. This URL expires after 48 hours and can only be used once. Don't store it, log it, send it in emails or expose it to anyone other than the user. Refer to our docs on [verifying identity documents](https://stripe.com/docs/identity/verify-identity-documents?platform=web&type=redirect) to learn how to redirect users to Stripe.
          */
         url: string | null;
 
diff --git a/types/2020-08-27/InvoiceLineItems.d.ts b/types/2020-08-27/InvoiceLineItems.d.ts
index 1dd6ba6c..5df57d08 100644
--- a/types/2020-08-27/InvoiceLineItems.d.ts
+++ b/types/2020-08-27/InvoiceLineItems.d.ts
@@ -178,7 +178,7 @@ declare module 'stripe' {
       customer?: string;
 
       /**
-       * Details about the customer you want to invoice
+       * Details about the customer you want to invoice or overrides for an existing customer.
        */
       customer_details?: InvoiceLineItemListUpcomingParams.CustomerDetails;
 
@@ -347,7 +347,7 @@ declare module 'stripe' {
 
         interface TaxId {
           /**
-           * Type of the tax ID, one of `ae_trn`, `au_abn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
+           * Type of the tax ID, one of `ae_trn`, `au_abn`, `au_arn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
            */
           type: TaxId.Type;
 
@@ -361,6 +361,7 @@ declare module 'stripe' {
           type Type =
             | 'ae_trn'
             | 'au_abn'
+            | 'au_arn'
             | 'br_cnpj'
             | 'br_cpf'
             | 'ca_bn'
diff --git a/types/2020-08-27/Invoices.d.ts b/types/2020-08-27/Invoices.d.ts
index 70485a3b..4ff17043 100644
--- a/types/2020-08-27/Invoices.d.ts
+++ b/types/2020-08-27/Invoices.d.ts
@@ -83,7 +83,7 @@ declare module 'stripe' {
       /**
        * Either `charge_automatically`, or `send_invoice`. When charging automatically, Stripe will attempt to pay this invoice using the default source attached to the customer. When sending an invoice, Stripe will email this invoice to the customer with payment instructions.
        */
-      collection_method: Invoice.CollectionMethod | null;
+      collection_method: Invoice.CollectionMethod;
 
       /**
        * Time at which the object was created. Measured in seconds since the Unix epoch.
@@ -399,7 +399,7 @@ declare module 'stripe' {
 
       interface CustomerTaxId {
         /**
-         * The type of the tax ID, one of `eu_vat`, `br_cnpj`, `br_cpf`, `gb_vat`, `nz_gst`, `au_abn`, `in_gst`, `no_vat`, `za_vat`, `ch_vat`, `mx_rfc`, `sg_uen`, `ru_inn`, `ru_kpp`, `ca_bn`, `hk_br`, `es_cif`, `tw_vat`, `th_vat`, `jp_cn`, `jp_rn`, `li_uid`, `my_itn`, `us_ein`, `kr_brn`, `ca_qst`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `my_sst`, `sg_gst`, `ae_trn`, `cl_tin`, `sa_vat`, `id_npwp`, `my_frp`, `il_vat`, or `unknown`
+         * The type of the tax ID, one of `eu_vat`, `br_cnpj`, `br_cpf`, `gb_vat`, `nz_gst`, `au_abn`, `au_arn`, `in_gst`, `no_vat`, `za_vat`, `ch_vat`, `mx_rfc`, `sg_uen`, `ru_inn`, `ru_kpp`, `ca_bn`, `hk_br`, `es_cif`, `tw_vat`, `th_vat`, `jp_cn`, `jp_rn`, `li_uid`, `my_itn`, `us_ein`, `kr_brn`, `ca_qst`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `my_sst`, `sg_gst`, `ae_trn`, `cl_tin`, `sa_vat`, `id_npwp`, `my_frp`, `il_vat`, or `unknown`
          */
         type: CustomerTaxId.Type;
 
@@ -413,6 +413,7 @@ declare module 'stripe' {
         type Type =
           | 'ae_trn'
           | 'au_abn'
+          | 'au_arn'
           | 'br_cnpj'
           | 'br_cpf'
           | 'ca_bn'
@@ -1310,7 +1311,7 @@ declare module 'stripe' {
       customer?: string;
 
       /**
-       * Details about the customer you want to invoice
+       * Details about the customer you want to invoice or overrides for an existing customer.
        */
       customer_details?: InvoiceRetrieveUpcomingParams.CustomerDetails;
 
@@ -1479,7 +1480,7 @@ declare module 'stripe' {
 
         interface TaxId {
           /**
-           * Type of the tax ID, one of `ae_trn`, `au_abn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
+           * Type of the tax ID, one of `ae_trn`, `au_abn`, `au_arn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
            */
           type: TaxId.Type;
 
@@ -1493,6 +1494,7 @@ declare module 'stripe' {
           type Type =
             | 'ae_trn'
             | 'au_abn'
+            | 'au_arn'
             | 'br_cnpj'
             | 'br_cpf'
             | 'ca_bn'
diff --git a/types/2020-08-27/Issuing/Cardholders.d.ts b/types/2020-08-27/Issuing/Cardholders.d.ts
index 8f8239ac..17ec112c 100644
--- a/types/2020-08-27/Issuing/Cardholders.d.ts
+++ b/types/2020-08-27/Issuing/Cardholders.d.ts
@@ -55,7 +55,7 @@ declare module 'stripe' {
         name: string;
 
         /**
-         * The cardholder's phone number.
+         * The cardholder's phone number. This is required for all cardholders who will be creating EU cards. See the [3D Secure documentation](https://stripe.com/docs/issuing/3d-secure#when-is-3d-secure-applied) for more details.
          */
         phone_number: string | null;
 
@@ -1144,7 +1144,7 @@ declare module 'stripe' {
         metadata?: Stripe.MetadataParam;
 
         /**
-         * The cardholder's phone number. This will be transformed to [E.164](https://en.wikipedia.org/wiki/E.164) if it is not provided in that format already.
+         * The cardholder's phone number. This will be transformed to [E.164](https://en.wikipedia.org/wiki/E.164) if it is not provided in that format already. This is required for all cardholders who will be creating EU cards. See the [3D Secure documentation](https://stripe.com/docs/issuing/3d-secure#when-is-3d-secure-applied) for more details.
          */
         phone_number?: string;
 
@@ -2235,7 +2235,7 @@ declare module 'stripe' {
         metadata?: Stripe.MetadataParam;
 
         /**
-         * The cardholder's phone number.
+         * The cardholder's phone number. This is required for all cardholders who will be creating EU cards. See the [3D Secure documentation](https://stripe.com/docs/issuing/3d-secure) for more details.
          */
         phone_number?: string;
 
diff --git a/types/2020-08-27/Mandates.d.ts b/types/2020-08-27/Mandates.d.ts
index 90380e8c..6ebfde7f 100644
--- a/types/2020-08-27/Mandates.d.ts
+++ b/types/2020-08-27/Mandates.d.ts
@@ -102,7 +102,7 @@ declare module 'stripe' {
       namespace PaymentMethodDetails {
         interface AcssDebit {
           /**
-           * Description of the interval. Only required if 'payment_schedule' parmeter is 'interval' or 'combined'.
+           * Description of the interval. Only required if the 'payment_schedule' parameter is 'interval' or 'combined'.
            */
           interval_description: string | null;
 
diff --git a/types/2020-08-27/PaymentIntents.d.ts b/types/2020-08-27/PaymentIntents.d.ts
index c56ef1f3..189bef84 100644
--- a/types/2020-08-27/PaymentIntents.d.ts
+++ b/types/2020-08-27/PaymentIntents.d.ts
@@ -538,7 +538,7 @@ declare module 'stripe' {
             custom_mandate_url?: string;
 
             /**
-             * Description of the interval. Only required if 'payment_schedule' parmeter is 'interval' or 'combined'.
+             * Description of the interval. Only required if the 'payment_schedule' parameter is 'interval' or 'combined'.
              */
             interval_description: string | null;
 
@@ -1627,7 +1627,7 @@ declare module 'stripe' {
           /**
            * Language shown to the payer on redirect.
            */
-          preferred_language?: Sofort.PreferredLanguage;
+          preferred_language?: Stripe.Emptyable<Sofort.PreferredLanguage>;
         }
 
         namespace Sofort {
@@ -2467,7 +2467,7 @@ declare module 'stripe' {
           /**
            * Language shown to the payer on redirect.
            */
-          preferred_language?: Sofort.PreferredLanguage;
+          preferred_language?: Stripe.Emptyable<Sofort.PreferredLanguage>;
         }
 
         namespace Sofort {
@@ -3421,7 +3421,7 @@ declare module 'stripe' {
           /**
            * Language shown to the payer on redirect.
            */
-          preferred_language?: Sofort.PreferredLanguage;
+          preferred_language?: Stripe.Emptyable<Sofort.PreferredLanguage>;
         }
 
         namespace Sofort {
diff --git a/types/2020-08-27/PaymentMethods.d.ts b/types/2020-08-27/PaymentMethods.d.ts
index 5a1fb45c..4e4c8801 100644
--- a/types/2020-08-27/PaymentMethods.d.ts
+++ b/types/2020-08-27/PaymentMethods.d.ts
@@ -1205,6 +1205,8 @@ declare module 'stripe' {
     class PaymentMethodsResource {
       /**
        * Creates a PaymentMethod object. Read the [Stripe.js reference](https://stripe.com/docs/stripe-js/reference#stripe-create-payment-method) to learn how to create PaymentMethods via Stripe.js.
+       *
+       * Instead of creating a PaymentMethod directly, we recommend using the [PaymentIntents API to accept a payment immediately or the <a href="/docs/payments/save-and-reuse">SetupIntent](https://stripe.com/docs/payments/accept-a-payment) API to collect payment method details ahead of a future payment.
        */
       create(
         params?: PaymentMethodCreateParams,
diff --git a/types/2020-08-27/Prices.d.ts b/types/2020-08-27/Prices.d.ts
index 986c03d3..f8c65de8 100644
--- a/types/2020-08-27/Prices.d.ts
+++ b/types/2020-08-27/Prices.d.ts
@@ -71,7 +71,7 @@ declare module 'stripe' {
       /**
        * Specifies whether the price is considered inclusive of taxes or exclusive of taxes. One of `inclusive`, `exclusive`, or `unspecified`. Once specified as either `inclusive` or `exclusive`, it cannot be changed.
        */
-      tax_behavior?: Price.TaxBehavior | null;
+      tax_behavior: Price.TaxBehavior | null;
 
       /**
        * Each element represents a pricing tier. This parameter requires `billing_scheme` to be set to `tiered`. See also the documentation for `billing_scheme`.
diff --git a/types/2020-08-27/Products.d.ts b/types/2020-08-27/Products.d.ts
index 26b9c905..9a51dc0a 100644
--- a/types/2020-08-27/Products.d.ts
+++ b/types/2020-08-27/Products.d.ts
@@ -86,7 +86,7 @@ declare module 'stripe' {
       /**
        * A [tax code](https://stripe.com/docs/tax/tax-codes) ID.
        */
-      tax_code?: string | Stripe.TaxCode | null;
+      tax_code: string | Stripe.TaxCode | null;
 
       /**
        * The type of the product. The product is either of type `good`, which is eligible for use with Orders and SKUs, or `service`, which is eligible for use with Subscriptions and Plans.
diff --git a/types/2020-08-27/Quotes.d.ts b/types/2020-08-27/Quotes.d.ts
index 9d47b06e..374aa610 100644
--- a/types/2020-08-27/Quotes.d.ts
+++ b/types/2020-08-27/Quotes.d.ts
@@ -36,7 +36,7 @@ declare module 'stripe' {
        */
       application_fee_percent: number | null;
 
-      automatic_tax?: Quote.AutomaticTax;
+      automatic_tax: Quote.AutomaticTax;
 
       /**
        * Either `charge_automatically`, or `send_invoice`. When charging automatically, Stripe will attempt to pay invoices at the end of the subscription cycle or on finalization using the default payment method attached to the subscription or customer. When sending an invoice, Stripe will email your customer an invoice with payment instructions. Defaults to `charge_automatically`.
@@ -1131,7 +1131,7 @@ declare module 'stripe' {
       ): Promise<Stripe.Response<Stripe.Quote>>;
 
       /**
-       * When retrieving a quote, there is an includable upfront.line_items property containing the first handful of those items. There is also a URL where you can retrieve the full (paginated) list of upfront line items.
+       * When retrieving a quote, there is an includable [computed.upfront.line_items](https://stripe.com/docs/api/quotes/object#quote_object-computed-upfront-line_items) property containing the first handful of those items. There is also a URL where you can retrieve the full (paginated) list of upfront line items.
        */
       listComputedUpfrontLineItems(
         id: string,
diff --git a/types/2020-08-27/SetupIntents.d.ts b/types/2020-08-27/SetupIntents.d.ts
index a4c020d2..d5aacd5c 100644
--- a/types/2020-08-27/SetupIntents.d.ts
+++ b/types/2020-08-27/SetupIntents.d.ts
@@ -303,7 +303,7 @@ declare module 'stripe' {
             custom_mandate_url?: string;
 
             /**
-             * Description of the interval. Only required if 'payment_schedule' parmeter is 'interval' or 'combined'.
+             * Description of the interval. Only required if the 'payment_schedule' parameter is 'interval' or 'combined'.
              */
             interval_description: string | null;
 
diff --git a/types/2020-08-27/Subscriptions.d.ts b/types/2020-08-27/Subscriptions.d.ts
index 88d1962b..378438cd 100644
--- a/types/2020-08-27/Subscriptions.d.ts
+++ b/types/2020-08-27/Subscriptions.d.ts
@@ -51,7 +51,7 @@ declare module 'stripe' {
       /**
        * Either `charge_automatically`, or `send_invoice`. When charging automatically, Stripe will attempt to pay this subscription at the end of the cycle using the default source attached to the customer. When sending an invoice, Stripe will email your customer an invoice with payment instructions.
        */
-      collection_method: Subscription.CollectionMethod | null;
+      collection_method: Subscription.CollectionMethod;
 
       /**
        * Time at which the object was created. Measured in seconds since the Unix epoch.
diff --git a/types/2020-08-27/TaxIds.d.ts b/types/2020-08-27/TaxIds.d.ts
index 2a25cdf2..97bb8d98 100644
--- a/types/2020-08-27/TaxIds.d.ts
+++ b/types/2020-08-27/TaxIds.d.ts
@@ -39,7 +39,7 @@ declare module 'stripe' {
       livemode: boolean;
 
       /**
-       * Type of the tax ID, one of `ae_trn`, `au_abn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`. Note that some legacy tax IDs have type `unknown`
+       * Type of the tax ID, one of `ae_trn`, `au_abn`, `au_arn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`. Note that some legacy tax IDs have type `unknown`
        */
       type: TaxId.Type;
 
@@ -58,6 +58,7 @@ declare module 'stripe' {
       type Type =
         | 'ae_trn'
         | 'au_abn'
+        | 'au_arn'
         | 'br_cnpj'
         | 'br_cpf'
         | 'ca_bn'
@@ -140,7 +141,7 @@ declare module 'stripe' {
 
     interface TaxIdCreateParams {
       /**
-       * Type of the tax ID, one of `ae_trn`, `au_abn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
+       * Type of the tax ID, one of `ae_trn`, `au_abn`, `au_arn`, `br_cnpj`, `br_cpf`, `ca_bn`, `ca_gst_hst`, `ca_pst_bc`, `ca_pst_mb`, `ca_pst_sk`, `ca_qst`, `ch_vat`, `cl_tin`, `es_cif`, `eu_vat`, `gb_vat`, `hk_br`, `id_npwp`, `il_vat`, `in_gst`, `jp_cn`, `jp_rn`, `kr_brn`, `li_uid`, `mx_rfc`, `my_frp`, `my_itn`, `my_sst`, `no_vat`, `nz_gst`, `ru_inn`, `ru_kpp`, `sa_vat`, `sg_gst`, `sg_uen`, `th_vat`, `tw_vat`, `us_ein`, or `za_vat`
        */
       type: TaxIdCreateParams.Type;
 
@@ -159,6 +160,7 @@ declare module 'stripe' {
       type Type =
         | 'ae_trn'
         | 'au_abn'
+        | 'au_arn'
         | 'br_cnpj'
         | 'br_cpf'
         | 'ca_bn'
diff --git a/types/2020-08-27/TaxRates.d.ts b/types/2020-08-27/TaxRates.d.ts
index 9c8f9c5a..b9e6bace 100644
--- a/types/2020-08-27/TaxRates.d.ts
+++ b/types/2020-08-27/TaxRates.d.ts
@@ -74,7 +74,7 @@ declare module 'stripe' {
       /**
        * The high-level tax type, such as `vat` or `sales_tax`.
        */
-      tax_type?: TaxRate.TaxType | null;
+      tax_type: TaxRate.TaxType | null;
     }
 
     namespace TaxRate {
diff --git a/types/2020-08-27/index.d.ts b/types/2020-08-27/index.d.ts
index 5c7f8f15..94dd9093 100644
--- a/types/2020-08-27/index.d.ts
+++ b/types/2020-08-27/index.d.ts
@@ -1,6 +1,7 @@
 // File generated from our OpenAPI spec
 
 ///<reference path='../lib.d.ts' />
+///<reference path='../net/net.d.ts' />
 ///<reference path='../shared.d.ts' />
 ///<reference path='../Errors.d.ts' />
 ///<reference path='../OAuth.d.ts' />
diff --git a/types/lib.d.ts b/types/lib.d.ts
index cb090ab5..bb15915c 100644
--- a/types/lib.d.ts
+++ b/types/lib.d.ts
@@ -18,7 +18,9 @@ declare module 'stripe' {
         method: string;
         path?: string;
         fullPath?: string;
-        methodType?: 'list';
+        // Please note, methodType === 'search' is beta functionality and is subject to
+        // change/removal at any time.
+        methodType?: 'list' | 'search';
       }): (...args: any[]) => object; //eslint-disable-line @typescript-eslint/no-explicit-any
       static BASIC_METHODS: {
         create<T>(
@@ -45,6 +47,7 @@ declare module 'stripe' {
     }
     export type LatestApiVersion = '2020-08-27';
     export type HttpAgent = Agent;
+    export type HttpProtocol = 'http' | 'https';
 
     export interface StripeConfig {
       /**
@@ -80,6 +83,13 @@ declare module 'stripe' {
        */
       httpAgent?: HttpAgent;
 
+      /**
+       * Use a custom http client, rather than relying on Node libraries.
+       * Useful for making requests in contexts other than NodeJS (eg. using
+       * `fetch`).
+       */
+      httpClient?: HttpClient;
+
       /**
        * Request timeout in milliseconds.
        * The default is 80000
@@ -90,7 +100,7 @@ declare module 'stripe' {
 
       port?: string | number;
 
-      protocol?: 'https' | 'http';
+      protocol?: HttpProtocol;
 
       /**
        * Pass `telemetry: false` to disable headers that provide Stripe
@@ -156,8 +166,8 @@ declare module 'stripe' {
     }
 
     export type Response<T> = T & {
-      headers: {[key: string]: string};
       lastResponse: {
+        headers: {[key: string]: string};
         requestId: string;
         statusCode: number;
         apiVersion?: string;
@@ -202,6 +212,46 @@ declare module 'stripe' {
       autoPagingToArray(opts: {limit: number}): Promise<Array<T>>;
     }
 
+    /**
+     * A container for paginated lists of search results.
+     * The array of objects is on the `.data` property,
+     * and `.has_more` indicates whether there are additional objects beyond the end of this list.
+     * The `.next_page` field can be used to paginate forwards.
+     *
+     * Please note, ApiSearchResult<T> is beta functionality and is subject to change/removal
+     * at any time.
+     */
+    export interface ApiSearchResult<T> {
+      object: 'search_result';
+
+      data: Array<T>;
+
+      /**
+       * True if this list has another page of items after this one that can be fetched.
+       */
+      has_more: boolean;
+
+      /**
+       * The URL where this list can be accessed.
+       */
+      url: string;
+
+      /**
+       * The page token to use to get the next page of results. If `has_more` is
+       * true, this will be set.
+       */
+      next_page?: string;
+    }
+    export interface ApiSearchResultPromise<T>
+      extends Promise<Response<ApiSearchResult<T>>>,
+        AsyncIterableIterator<T> {
+      autoPagingEach(
+        handler: (item: T) => boolean | void | Promise<boolean | void>
+      ): Promise<void>;
+
+      autoPagingToArray(opts: {limit: number}): Promise<Array<T>>;
+    }
+
     export type StripeStreamResponse = NodeJS.ReadableStream;
 
     /**
diff --git a/types/net/net.d.ts b/types/net/net.d.ts
new file mode 100644
index 00000000..c399f4c6
--- /dev/null
+++ b/types/net/net.d.ts
@@ -0,0 +1,65 @@
+/// <reference types="node" />
+
+import {IncomingMessage} from 'http';
+declare module 'stripe' {
+  namespace Stripe {
+    /**
+     * Encapsulates the logic for issuing a request to the Stripe API. This is
+     * an experimental interface and is not yet stable.
+     */
+    export interface HttpClient<
+      ResponseType extends HttpClientResponse = HttpClientResponse
+    > {
+      /** The client name to use for diagnostics. */
+      getClientName(): string;
+
+      makeRequest(
+        host: string,
+        port: string | number,
+        path: string,
+        method: 'GET' | 'POST' | 'PUT' | 'DELETE',
+        headers: object,
+        requestData: string | null,
+        protocol: Stripe.HttpProtocol,
+        timeout: number
+      ): Promise<ResponseType>;
+    }
+
+    /**
+     * Abstract representation of an HTTP response. This is an experimental
+     * interface and is not yet stable.
+     */
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    interface HttpClientResponse<RawResponseType = any, StreamType = any> {
+      /** The numeric HTTP status code for the response. */
+      getStatusCode(): number;
+
+      /** The response headers. */
+      getHeaders(): {[key: string]: string};
+
+      /** This returns the underlying raw response object for the client. */
+      getRawResponse(): RawResponseType;
+
+      /**
+       * This returns the content as a stream. The expectation is that content
+       * will not have been buffered into memory at this point by the client.
+       *
+       * The streamCompleteCallback should be invoked by the response
+       * implementation when the stream has been consumed.
+       */
+      toStream(streamCompleteCallback: () => void): StreamType;
+
+      /**
+       * Converts the response content into a JSON object, failing if JSON
+       * couldn't be parsed.
+       */
+      toJSON(): Promise<object>;
+    }
+
+    export const createNodeHttpClient: (
+      agent?: HttpAgent | null
+    ) => HttpClient<
+      HttpClientResponse<IncomingMessage, Stripe.StripeStreamResponse>
+    >;
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index 0df47289..719e2f26 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -468,7 +468,7 @@ caching-transform@^4.0.0:
     package-hash "^4.0.0"
     write-file-atomic "^3.0.0"
 
-call-bind@^1.0.0, call-bind@^1.0.2:
+call-bind@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
   integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
@@ -508,18 +508,6 @@ chai-as-promised@~7.1.1:
   dependencies:
     check-error "^1.0.2"
 
-chai@^4.1.2:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
-  integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==
-  dependencies:
-    assertion-error "^1.1.0"
-    check-error "^1.0.2"
-    deep-eql "^3.0.1"
-    get-func-name "^2.0.0"
-    pathval "^1.1.1"
-    type-detect "^4.0.5"
-
 chai@~4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5"
@@ -715,7 +703,7 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
-debug@4.3.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+debug@4.3.1, debug@^4.0.1, debug@^4.1.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
   integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@@ -729,6 +717,13 @@ debug@^2.2.0:
   dependencies:
     ms "2.0.0"
 
+debug@^4.1.0:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
+  dependencies:
+    ms "2.1.2"
+
 decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -746,18 +741,6 @@ deep-eql@^3.0.1:
   dependencies:
     type-detect "^4.0.0"
 
-deep-equal@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
-  integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
-  dependencies:
-    is-arguments "^1.0.4"
-    is-date-object "^1.0.1"
-    is-regex "^1.0.4"
-    object-is "^1.0.1"
-    object-keys "^1.1.1"
-    regexp.prototype.flags "^1.2.0"
-
 deep-is@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@@ -770,13 +753,6 @@ default-require-extensions@^3.0.0:
   dependencies:
     strip-bom "^4.0.0"
 
-define-properties@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
-  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
-  dependencies:
-    object-keys "^1.0.12"
-
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -1242,7 +1218,7 @@ has-flag@^4.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-symbols@^1.0.1, has-symbols@^1.0.2:
+has-symbols@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
   integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
@@ -1343,13 +1319,6 @@ inquirer@^7.0.0:
     strip-ansi "^6.0.0"
     through "^2.3.6"
 
-is-arguments@^1.0.4:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
-  integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==
-  dependencies:
-    call-bind "^1.0.0"
-
 is-binary-path@~2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -1362,11 +1331,6 @@ is-buffer@~1.1.6:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-date-object@^1.0.1:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5"
-  integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==
-
 is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1399,14 +1363,6 @@ is-plain-obj@^2.1.0:
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
   integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
 
-is-regex@^1.0.4:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
-  integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
-  dependencies:
-    call-bind "^1.0.2"
-    has-symbols "^1.0.2"
-
 is-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
@@ -1592,7 +1548,12 @@ lodash.flattendeep@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
   integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
 
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.5:
+lodash.set@^4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
+  integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
+
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -1661,7 +1622,7 @@ minimist@^1.2.5:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
+mkdirp@^0.5.1, mkdirp@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
   integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -1745,20 +1706,15 @@ nice-try@^1.0.4:
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
-nock@^10.0.6:
-  version "10.0.6"
-  resolved "https://registry.yarnpkg.com/nock/-/nock-10.0.6.tgz#e6d90ee7a68b8cfc2ab7f6127e7d99aa7d13d111"
-  integrity sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==
+nock@^13.1.1:
+  version "13.1.1"
+  resolved "https://registry.yarnpkg.com/nock/-/nock-13.1.1.tgz#3c830129d4560957f59b6f480a41ddbaf9cf57af"
+  integrity sha512-YKTR9MjfK3kS9/l4nuTxyYm30cgOExRHzkLNhL8nhEUyU4f8Za/dRxOqjhVT1vGs0svWo3dDnJTUX1qxYeWy5w==
   dependencies:
-    chai "^4.1.2"
     debug "^4.1.0"
-    deep-equal "^1.0.0"
     json-stringify-safe "^5.0.1"
-    lodash "^4.17.5"
-    mkdirp "^0.5.0"
-    propagate "^1.0.0"
-    qs "^6.5.1"
-    semver "^5.5.0"
+    lodash.set "^4.3.2"
+    propagate "^2.0.0"
 
 node-preload@^0.2.1:
   version "0.2.1"
@@ -1816,22 +1772,9 @@ oauth-sign@~0.9.0:
   integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
 
 object-inspect@^1.9.0:
-  version "1.10.3"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
-  integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
-
-object-is@^1.0.1:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
-  integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
-  dependencies:
-    call-bind "^1.0.2"
-    define-properties "^1.1.3"
-
-object-keys@^1.0.12, object-keys@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
-  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
+  integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
 
 once@^1.3.0:
   version "1.4.0"
@@ -1941,7 +1884,7 @@ path-key@^3.1.0:
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
   integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
-pathval@^1.1.0, pathval@^1.1.1:
+pathval@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
   integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
@@ -1992,10 +1935,10 @@ progress@^2.0.0:
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
-propagate@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709"
-  integrity sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=
+propagate@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
+  integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
 
 psl@^1.1.28:
   version "1.8.0"
@@ -2007,7 +1950,7 @@ punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-qs@^6.5.1, qs@^6.6.0:
+qs@^6.6.0:
   version "6.10.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
   integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
@@ -2033,14 +1976,6 @@ readdirp@~3.5.0:
   dependencies:
     picomatch "^2.2.1"
 
-regexp.prototype.flags@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
-  integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==
-  dependencies:
-    call-bind "^1.0.2"
-    define-properties "^1.1.3"
-
 regexpp@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"


Test patch:
diff --git a/test/StripeResource.spec.js b/test/StripeResource.spec.js
index 2f5ec6cc..f3d4600a 100644
--- a/test/StripeResource.spec.js
+++ b/test/StripeResource.spec.js
@@ -1,16 +1,25 @@
 'use strict';
 
 const utils = require('../testUtils');
-
 const nock = require('nock');
 
 const stripe = require('../testUtils').getSpyableStripe();
 const expect = require('chai').expect;
 const testUtils = require('../testUtils');
 
+const {HttpClientResponse} = require('../lib/net/HttpClient');
 const StripeResource = require('../lib/StripeResource');
 const stripeMethod = StripeResource.method;
 
+const {
+  StripeAuthenticationError,
+  StripeIdempotencyError,
+  StripePermissionError,
+  StripeRateLimitError,
+  StripeError,
+  StripeConnectionError,
+} = require('../lib/Error');
+
 describe('StripeResource', () => {
   describe('createResourcePathWithSymbols', () => {
     it('Generates a path', () => {
@@ -208,6 +217,119 @@ describe('StripeResource', () => {
         );
       });
 
+      it('throws an error on invalid JSON', (done) => {
+        return utils.getTestServerStripe(
+          {},
+          (req, res) => {
+            // Write back JSON to close out the server.
+            res.write('invalidjson{}');
+            res.end();
+          },
+          (err, stripe, closeServer) => {
+            if (err) {
+              return done(err);
+            }
+            stripe.charges.create(options.data, (err, result) => {
+              expect(err.message).to.deep.equal(
+                'Invalid JSON received from the Stripe API'
+              );
+              closeServer();
+              done();
+            });
+          }
+        );
+      });
+      it('throws an valid headers but connection error', (done) => {
+        return utils.getTestServerStripe(
+          {},
+          (req, res) => {
+            // Send out valid headers and a partial response. We then interrupt
+            // the response with an error.
+            res.writeHead(200);
+            res.write('{"ab');
+            res.destroy(new Error('something happened'));
+          },
+          (err, stripe, closeServer) => {
+            if (err) {
+              return done(err);
+            }
+            stripe.charges.create(options.data, (err, result) => {
+              expect(err).to.be.an.instanceOf(StripeConnectionError);
+              done();
+            });
+          }
+        );
+      });
+
+      it('throws a StripeAuthenticationError on 401', (done) => {
+        nock(`https://${options.host}`)
+          .post(options.path, options.params)
+          .reply(401, {
+            error: {
+              message: 'message',
+            },
+          });
+
+        realStripe.charges.create(options.data, (err) => {
+          expect(err).to.be.an.instanceOf(StripeAuthenticationError);
+          expect(err.message).to.be.equal('message');
+          done();
+        });
+      });
+
+      it('throws a StripePermissionError on 403', (done) => {
+        nock(`https://${options.host}`)
+          .post(options.path, options.params)
+          .reply(403, {
+            error: {
+              message: 'message',
+            },
+          });
+
+        realStripe.charges.create(options.data, (err) => {
+          expect(err).to.be.an.instanceOf(StripePermissionError);
+          expect(err.message).to.be.equal('message');
+          done();
+        });
+      });
+
+      it('throws a StripeRateLimitError on 429', (done) => {
+        nock(`https://${options.host}`)
+          .post(options.path, options.params)
+          .reply(429, {
+            error: {
+              message: 'message',
+            },
+          });
+
+        realStripe.charges.create(options.data, (err) => {
+          expect(err).to.be.an.instanceOf(StripeRateLimitError);
+          expect(err.message).to.be.equal('message');
+          done();
+        });
+      });
+
+      it('throws a StripeError based on the underlying error type', (done) => {
+        const error = {
+          type: 'idempotency_error',
+        };
+
+        expect(StripeError.generate(error)).to.be.an.instanceOf(
+          StripeIdempotencyError
+        );
+
+        nock(`https://${options.host}`)
+          .post(options.path, options.params)
+          .reply(400, {
+            error,
+          });
+
+        realStripe.charges.create(options.data, (err) => {
+          expect(err).to.be.an.instanceOf(StripeIdempotencyError);
+          done();
+        });
+      });
+
       it('retries connection timeout errors', (done) => {
         let nRequestsReceived = 0;
         return utils.getTestServerStripe(
@@ -557,27 +679,92 @@ describe('StripeResource', () => {
           }
         );
       });
+
+      it('invokes the callback with successful results', (done) => {
+        const returnedCharge = {
+          id: 'ch_123',
+        };
+        return utils.getTestServerStripe(
+          {},
+          (req, res) => {
+            res.write(JSON.stringify(returnedCharge));
+            res.end();
+          },
+          (err, stripe, closeServer) => {
+            if (err) {
+              return done(err);
+            }
+            stripe.charges.create(options.data, (err, result) => {
+              expect(result).to.deep.equal(returnedCharge);
+              closeServer();
+              done();
+            });
+          }
+        );
+      });
+
+      it('returns successful results to await', (done) => {
+        const returnedCharge = {
+          id: 'ch_123',
+        };
+        return utils.getTestServerStripe(
+          {},
+          (req, res) => {
+            res.write(JSON.stringify(returnedCharge));
+            res.end();
+          },
+          async (err, stripe, closeServer) => {
+            if (err) {
+              return done(err);
+            }
+            try {
+              const result = await stripe.charges.create(options.data);
+              expect(result).to.deep.equal(returnedCharge);
+              closeServer();
+              done();
+            } catch (err) {
+              done(err);
+            }
+          }
+        );
+      });
     });
 
     describe('_shouldRetry', () => {
       it("should return false if we've reached maximum retries", () => {
-        const res = stripe.invoices._shouldRetry({statusCode: 409}, 1, 1);
+        const res = stripe.invoices._shouldRetry(
+          new HttpClientResponse(409, {}),
+          1,
+          1
+        );
 
         expect(res).to.equal(false);
       });
 
       it('should return true if we have more retries available', () => {
-        const res = stripe.invoices._shouldRetry({statusCode: 409}, 0, 1);
+        const res = stripe.invoices._shouldRetry(
+          new HttpClientResponse(409, {}),
+          0,
+          1
+        );
 
         expect(res).to.equal(true);
       });
 
       it('should return true if the error code is either 409 or 503', () => {
-        let res = stripe.invoices._shouldRetry({statusCode: 409}, 0, 1);
+        let res = stripe.invoices._shouldRetry(
+          new HttpClientResponse(409, {}),
+          0,
+          1
+        );
 
         expect(res).to.equal(true);
 
-        res = stripe.invoices._shouldRetry({statusCode: 503}, 0, 1);
+        res = stripe.invoices._shouldRetry(
+          new HttpClientResponse(503, {}),
+          0,
+          1
+        );
 
         expect(res).to.equal(true);
       });
@@ -585,10 +772,7 @@ describe('StripeResource', () => {
       it('should return false if the status is 200', () => {
         // mocking that we're on our 2nd request
         const res = stripe.invoices._shouldRetry(
-          {
-            statusCode: 200,
-            req: {_requestEvent: {method: 'POST'}},
-          },
+          new HttpClientResponse(200, {}),
           1,
           2
         );
diff --git a/test/autoPagination.spec.js b/test/autoPagination.spec.js
index f87b93ff..6a8171c4 100644
--- a/test/autoPagination.spec.js
+++ b/test/autoPagination.spec.js
@@ -701,4 +701,115 @@ describe('auto pagination', function() {
       });
     });
   });
+
+  describe('pagination logic using a mock search paginator', () => {
+    const mockPagination = (pages, initialArgs) => {
+      let i = 1;
+      const paramsLog = [];
+      const spec = {
+        method: 'GET',
+        methodType: 'search',
+      };
+
+      const addNextPage = (props) => {
+        let nextPageProperties = {};
+        if (props.has_more) {
+          nextPageProperties = {
+            next_page: `${props.data[props.data.length - 1]}-encoded`,
+          };
+        }
+        return {...props, ...nextPageProperties};
+      };
+
+      const paginator = makeAutoPaginationMethods(
+        {
+          createResourcePathWithSymbols: () => {},
+          createFullPath: () => {},
+          _request: (_1, _2, path, _4, _5, _6, callback) => {
+            paramsLog.push(path);
+
+            callback(
+              null,
+              Promise.resolve(
+                addNextPage({
+                  data: pages[i].map((id) => ({id})),
+                  has_more: i < pages.length - 1,
+                })
+              )
+            );
+            i += 1;
+          },
+        },
+        initialArgs || {},
+        spec,
+        Promise.resolve(
+          addNextPage({
+            data: pages[0].map((id) => ({id})),
+            has_more: pages.length > 1,
+          })
+        )
+      );
+      return {paginator, paramsLog};
+    };
+
+    const testCase = ({
+      pages,
+      limit,
+      expectedIds,
+      expectedParamsLog,
+      initialArgs,
+    }) => {
+      const {paginator, paramsLog} = mockPagination(pages, initialArgs);
+      expect(
+        paginator.autoPagingToArray({limit}).then((result) => ({
+          ids: result.map((x) => x.id),
+          paramsLog,
+        }))
+      ).to.eventually.deep.equal({
+        ids: expectedIds,
+        paramsLog: expectedParamsLog,
+      });
+    };
+
+    it('paginates forwards as expected', () => {
+      testCase({
+        pages: [
+          [1, 2],
+          [3, 4],
+        ],
+        limit: 5,
+        expectedIds: [1, 2, 3, 4],
+        expectedParamsLog: ['?next_page=2-encoded'],
+      });
+
+      testCase({
+        pages: [[1, 2], [3, 4], [5]],
+        limit: 5,
+        expectedIds: [1, 2, 3, 4, 5],
+        expectedParamsLog: ['?next_page=2-encoded', '?next_page=4-encoded'],
+      });
+
+      testCase({
+        pages: [
+          [1, 2],
+          [3, 4],
+          [5, 6],
+        ],
+        limit: 5,
+        expectedIds: [1, 2, 3, 4, 5],
+        expectedParamsLog: ['?next_page=2-encoded', '?next_page=4-encoded'],
+      });
+
+      testCase({
+        pages: [
+          [1, 2],
+          [3, 4],
+          [5, 6],
+        ],
+        limit: 6,
+        expectedIds: [1, 2, 3, 4, 5, 6],
+        expectedParamsLog: ['?next_page=2-encoded', '?next_page=4-encoded'],
+      });
+    });
+  });
 });
diff --git a/test/net/NodeHttpClient.spec.js b/test/net/NodeHttpClient.spec.js
new file mode 100644
index 00000000..84855ee0
--- /dev/null
+++ b/test/net/NodeHttpClient.spec.js
@@ -0,0 +1,215 @@
+'use strict';
+
+const {Readable} = require('stream');
+
+const http = require('http');
+const nock = require('nock');
+const expect = require('chai').expect;
+// const {fail} = require('chai').assert;
+
+const {createNodeHttpClient} = require('../../lib/Stripe');
+const utils = require('../../lib/utils');
+const {fail} = require('assert');
+
+/**
+ * Readable stream which will emit a data event for each value in the array
+ * passed. Readable.from accomplishes this beyond Node 10.17.
+ */
+class ArrayReadable extends Readable {
+  constructor(values) {
+    super();
+    this._index = 0;
+    this._values = values;
+  }
+
+  _read() {
+    if (this._index === this._values.length) {
+      // Destroy the stream once we've read all values.
+      this.push(null);
+    } else {
+      this.push(Buffer.from(this._values[this._index], 'utf8'));
+      this._index += 1;
+    }
+  }
+}
+
+describe('NodeHttpClient', () => {
+  const setupNock = () => {
+    return nock('http://stripe.com').get('/test');
+  };
+
+  const sendRequest = (options) => {
+    options = options || {};
+    return createNodeHttpClient().makeRequest(
+      'stripe.com',
+      options.port || 80,
+      '/test',
+      options.method || 'GET',
+      options.headers || {},
+      options.requestData,
+      'http',
+      options.timeout || 1000
+    );
+  };
+
+  afterEach(() => {
+    nock.cleanAll();
+  });
+
+  describe('makeRequest', () => {
+    it('rejects with a timeout error', async () => {
+      setupNock()
+        .delayConnection(31)
+        .reply(200, 'hello, world!');
+
+      try {
+        await sendRequest({timeout: 30});
+        fail();
+      } catch (e) {
+        expect(e.code).to.be.equal('ETIMEDOUT');
+      }
+    });
+
+    it('forwards any error', async () => {
+      setupNock().replyWithError('sample error');
+
+      try {
+        await sendRequest();
+        fail();
+      } catch (e) {
+        expect(e.message).to.be.equal('sample error');
+      }
+    });
+
+    it('sends request headers', async () => {
+      nock('http://stripe.com', {
+        reqheaders: {
+          sample: 'value',
+        },
+      })
+        .get('/test')
+        .reply(200);
+
+      await sendRequest({headers: {sample: 'value'}});
+    });
+
+    it('sends request data (POST)', (done) => {
+      const expectedData = utils.stringifyRequestData({id: 'test'});
+
+      nock('http://stripe.com')
+        .post('/test')
+        .reply(200, (uri, requestBody) => {
+          expect(requestBody).to.equal(expectedData);
+          done();
+        });
+
+      sendRequest({method: 'POST', requestData: expectedData});
+    });
+
+    it('custom port', async () => {
+      nock('http://stripe.com:1234')
+        .get('/test')
+        .reply(200);
+      await sendRequest({port: 1234});
+    });
+
+    describe('NodeHttpClientResponse', () => {
+      it('getStatusCode()', async () => {
+        setupNock().reply(418, 'hello, world!');
+
+        const response = await sendRequest();
+
+        expect(response.getStatusCode()).to.be.equal(418);
+      });
+
+      it('getHeaders()', async () => {
+        setupNock().reply(200, 'hello, world!', {
+          'X-Header-1': '123',
+          'X-Header-2': 'test',
+        });
+
+        const response = await sendRequest();
+
+        // Headers get transformed into lower case.
+        expect(response.getHeaders()).to.be.deep.equal({
+          'x-header-1': '123',
+          'x-header-2': 'test',
+        });
+      });
+
+      it('getRawResponse()', async () => {
+        setupNock().reply(200);
+
+        const response = await sendRequest();
+
+        expect(response.getRawResponse()).to.be.an.instanceOf(
+          http.IncomingMessage
+        );
+      });
+
+      it('toStream returns a readable stream', async () => {
+        setupNock().reply(200, () => new ArrayReadable(['hello, world!']));
+
+        const response = await sendRequest();
+
+        return new Promise((resolve) => {
+          const stream = response.toStream(() => true);
+
+          let streamedContent = '';
+          stream.on('data', (chunk) => {
+            streamedContent += chunk;
+          });
+          stream.on('end', () => {
+            expect(streamedContent).to.equal('hello, world!');
+            resolve();
+          });
+        });
+      });
+
+      it('toStream invokes the streamCompleteCallback', async () => {
+        setupNock().reply(200, () => new ArrayReadable(['hello, world!']));
+
+        const response = await sendRequest();
+
+        return new Promise((resolve) => {
+          let streamedContent = '';
+
+          const stream = response.toStream(() => {
+            expect(streamedContent).to.equal('hello, world!');
+            resolve();
+          });
+
+          stream.on('data', (chunk) => {
+            streamedContent += chunk;
+          });
+        });
+      });
+
+      it('toJSON accumulates all data chunks in utf-8 encoding', async () => {
+        setupNock().reply(
+          200,
+          () => new ArrayReadable(['{"a', 'bc":', '"∑ 123', '"}'])
+        );
+
+        const response = await sendRequest();
+
+        const json = await response.toJSON();
+
+        expect(json).to.deep.equal({abc: '∑ 123'});
+      });
+
+      it('toJSON throws when JSON parsing fails', async () => {
+        setupNock().reply(200, '{"a');
+
+        const response = await sendRequest();
+
+        try {
+          await response.toJSON();
+          fail();
+        } catch (e) {
+          expect(e.message).to.be.equal('Unexpected end of JSON input');
+        }
+      });
+    });
+  });
+});
diff --git a/test/stripe.spec.js b/test/stripe.spec.js
index 26fcba6b..5751a8b6 100644
--- a/test/stripe.spec.js
+++ b/test/stripe.spec.js
@@ -160,6 +160,21 @@ describe('Stripe Module', function() {
         })
       ).to.eventually.have.property('lang', 'node'));
 
+    it('Should return platform and version in the serialized user agent JSON object', async () => {
+      // Check that the testing environment actually has a process global.
+      expect(process.version).to.not.be.empty;
+      expect(process.platform).to.not.be.empty;
+
+      const userAgent = await new Promise((resolve, reject) => {
+        stripe.getClientUserAgent((c) => {
+          resolve(JSON.parse(c));
+        });
+      });
+
+      expect(userAgent).to.have.property('lang_version', process.version);
+      expect(userAgent).to.have.property('platform', process.platform);
+    });
+
     it('Should include whether typescript: true was passed, respecting reinstantiations', () => {
       return new Promise((resolve) => resolve())
         .then(() => {
@@ -210,8 +225,22 @@ describe('Stripe Module', function() {
       ).to.eventually.have.property('lang', '%C3%AF');
     });
 
+    it('Should URI-encode the HTTP client name', () => {
+      const userAgent = {lang: 'ï'};
+      return expect(
+        new Promise((resolve, reject) => {
+          stripe.getClientUserAgentSeeded(userAgent, (c) => {
+            resolve(JSON.parse(c));
+          });
+        })
+      ).to.eventually.have.property('httplib', 'node');
+    });
+
     describe('uname', () => {
       let origExec;
+      beforeEach(() => {
+        Stripe._UNAME_CACHE = null;
+      });
       beforeEach(() => {
         origExec = utils.safeExec;
       });
@@ -401,6 +430,8 @@ describe('Stripe Module', function() {
                 const headers = customer.lastResponse.headers;
                 expect(headers).to.contain.keys('request-id');
 
+                expect(customer.headers).to.be.undefined;
+
                 resolve('Called!');
               });
             })
@@ -479,7 +510,7 @@ describe('Stripe Module', function() {
       let headers;
       let stripeClient;
       let closeServer;
-      before((callback) => {
+      beforeEach((callback) => {
         testUtils.getTestServerStripe(
           {
             stripeAccount: 'my_stripe_account',
@@ -500,7 +531,7 @@ describe('Stripe Module', function() {
           }
         );
       });
-      after(() => closeServer());
+      afterEach(() => closeServer());
       it('is respected', (callback) => {
         stripeClient.customers.create((err) => {
           closeServer();
diff --git a/testUtils/index.js b/testUtils/index.js
index e2302623..145fc2f4 100644
--- a/testUtils/index.js
+++ b/testUtils/index.js
@@ -10,6 +10,8 @@ const http = require('http');
 
 const ResourceNamespace = require('../lib/ResourceNamespace').ResourceNamespace;
 
+const testingHttpAgent = new http.Agent({keepAlive: false});
+
 const utils = (module.exports = {
   getTestServerStripe: (clientOptions, handler, callback) => {
     const server = http.createServer((req, res) => {
@@ -28,6 +30,7 @@ const utils = (module.exports = {
           host: 'localhost',
           port,
           protocol: 'http',
+          httpAgent: testingHttpAgent,
           ...clientOptions,
         }
       );
diff --git a/types/test/typescriptTest.ts b/types/test/typescriptTest.ts
index 9c763be3..54bf7be2 100644
--- a/types/test/typescriptTest.ts
+++ b/types/test/typescriptTest.ts
@@ -34,6 +34,7 @@ stripe = new Stripe('sk_test_123', {
   host: 'api.example.com',
   port: 123,
   telemetry: true,
+  httpClient: Stripe.createNodeHttpClient(),
 });
 
 stripe.setTimeout(3000);
@@ -144,8 +145,8 @@ stripe.setHost('host', 'port', 'protocol');
     const statusCode: number = lr.statusCode;
     const apiVersion: string | undefined = lr.apiVersion;
     const idempotencyKey: string | undefined = lr.idempotencyKey;
-    const headers = custs.headers;
-    const header: string | undefined = custs.headers['request-id'];
+    const headers = lr.headers;
+    const header: string | undefined = headers['request-id'];
   }
 
   {
@@ -155,8 +156,8 @@ stripe.setHost('host', 'port', 'protocol');
     const statusCode: number = lr.statusCode;
     const apiVersion: string | undefined = lr.apiVersion;
     const idempotencyKey: string | undefined = lr.idempotencyKey;
-    const headers = cust.headers;
-    const header: string | undefined = cust.headers['request-id'];
+    const headers = lr.headers;
+    const header: string | undefined = lr.headers['request-id'];
   }
   {
     const acct = await stripe.accounts.createExternalAccount('foo', {
@@ -184,7 +185,39 @@ Stripe.StripeResource.extend({
     method: 'create',
     fullPath: '/v1/full/path',
   }),
+  search: Stripe.StripeResource.method({
+    method: 'create',
+    fullPath: 'foo',
+    methodType: 'search',
+  }),
 });
 
 const maxBufferedRequestMetrics: number =
   Stripe.StripeResource.MAX_BUFFERED_REQUEST_METRICS;
+
+// Test NodeHttpClient request processing.
+import {Agent} from 'http';
+async (): Promise<void> => {
+  const client = Stripe.createNodeHttpClient(new Agent());
+
+  const response = await client.makeRequest(
+    'api.stripe.com',
+    '443',
+    '/test',
+    'POST',
+    {
+      'Stripe-Account': 'account',
+      'Content-Length': 123,
+    },
+    'requestdata',
+    'https',
+    80000
+  );
+
+  const stream: Stripe.StripeStreamResponse = response.toStream(() => {
+    return;
+  });
+  stream.setEncoding('utf8');
+
+  const jsonResponse: object = await response.toJSON();
+};