Repository: stripe/stripe-python
Issue: v4.0.0 `idempotency_key` parameter unknown

### Describe the bug

Any of our requests to `POST /v1/subscriptions`, `POST /v1/invoices` and `POST /v1/customers/:id` are failing with `Received unknown parameter: idempotency_key`

### To Reproduce

Make a request to create a customer and include the `idempotency_key` parameter
```
stripe.Customer.create(
  name="John Doe",
  email=email,
  idempotency_key="test-key-12345",
)
```

### Expected behavior

`idempotency_key` is an accepted parameter

### Code snippets

_No response_

### OS

macOS

### Language version

Python 3.7.3

### Library version

stripe-python v4.0.0

### API version

2019-05-16

### Additional context

_No response_

Changed files:
stripe/util.py, tests/api_resources/abstract/test_deletable_api_resource.py, stripe/stripe_object.py, tests/api_resources/abstract/test_updateable_api_resource.py, stripe/api_resources/abstract/api_resource.py, tests/api_resources/abstract/test_api_resource.py, tests/api_resources/abstract/test_custom_method.py

diff --git a/stripe/api_resources/abstract/api_resource.py b/stripe/api_resources/abstract/api_resource.py
index 664ea1c9..c2bcd8c6 100644
--- a/stripe/api_resources/abstract/api_resource.py
+++ b/stripe/api_resources/abstract/api_resource.py
@@ -115,10 +115,27 @@ class APIResource(StripeObject):
         stripe_account=None,
         params=None,
     ):
+        params = None if params is None else params.copy()
+        api_key = util.read_special_variable(params, "api_key", api_key)
+        idempotency_key = util.read_special_variable(
+            params, "idempotency_key", idempotency_key
+        )
+        stripe_version = util.read_special_variable(
+            params, "stripe_version", stripe_version
+        )
+        stripe_account = util.read_special_variable(
+            params, "stripe_account", stripe_account
+        )
+        headers = util.read_special_variable(params, "headers", None)
+
         requestor = api_requestor.APIRequestor(
             api_key, api_version=stripe_version, account=stripe_account
         )
-        headers = util.populate_headers(idempotency_key)
+
+        if idempotency_key is not None:
+            headers = {} if headers is None else headers.copy()
+            headers.update(util.populate_headers(idempotency_key))
+
         response, api_key = requestor.request(method_, url_, params, headers)
         return util.convert_to_stripe_object(
             response, api_key, stripe_version, stripe_account, params
@@ -137,9 +154,26 @@ class APIResource(StripeObject):
         stripe_account=None,
         params=None,
     ):
+        params = None if params is None else params.copy()
+        api_key = util.read_special_variable(params, "api_key", api_key)
+        idempotency_key = util.read_special_variable(
+            params, "idempotency_key", idempotency_key
+        )
+        stripe_version = util.read_special_variable(
+            params, "stripe_version", stripe_version
+        )
+        stripe_account = util.read_special_variable(
+            params, "stripe_account", stripe_account
+        )
+        headers = util.read_special_variable(params, "headers", None)
+
         requestor = api_requestor.APIRequestor(
             api_key, api_version=stripe_version, account=stripe_account
         )
-        headers = util.populate_headers(idempotency_key)
+
+        if idempotency_key is not None:
+            headers = {} if headers is None else headers.copy()
+            headers.update(util.populate_headers(idempotency_key))
+
         response, _ = requestor.request_stream(method_, url_, params, headers)
         return response
diff --git a/stripe/stripe_object.py b/stripe/stripe_object.py
index a06cedd2..80ca5d66 100644
--- a/stripe/stripe_object.py
+++ b/stripe/stripe_object.py
@@ -250,6 +250,19 @@ class StripeObject(dict):
         headers=None,
         params=None,
     ):
+        params = None if params is None else params.copy()
+        api_key = util.read_special_variable(params, "api_key", api_key)
+        idempotency_key = util.read_special_variable(
+            params, "idempotency_key", idempotency_key
+        )
+        stripe_version = util.read_special_variable(
+            params, "stripe_version", stripe_version
+        )
+        stripe_account = util.read_special_variable(
+            params, "stripe_account", stripe_account
+        )
+        headers = util.read_special_variable(params, "headers", headers)
+
         stripe_account = stripe_account or self.stripe_account
         stripe_version = stripe_version or self.stripe_version
         api_key = api_key or self.api_key
diff --git a/stripe/util.py b/stripe/util.py
index 619f9cb1..d4c55392 100644
--- a/stripe/util.py
+++ b/stripe/util.py
@@ -210,6 +210,20 @@ def populate_headers(idempotency_key):
     return None
 
 
+def read_special_variable(params, key_name, default_value):
+    value = default_value
+    params_value = None
+
+    if params is not None and key_name in params:
+        params_value = params[key_name]
+        del params[key_name]
+
+    if value is None:
+        value = params_value
+
+    return value
+
+
 def merge_dicts(x, y):
     z = x.copy()
     z.update(y)
diff --git a/tests/api_resources/abstract/test_api_resource.py b/tests/api_resources/abstract/test_api_resource.py
index 9bed856b..13794611 100644
--- a/tests/api_resources/abstract/test_api_resource.py
+++ b/tests/api_resources/abstract/test_api_resource.py
@@ -38,6 +38,25 @@ class TestAPIResource(object):
         with pytest.raises(KeyError):
             res["bobble"]
 
+    def test_request_with_special_fields_prefers_explicit(self, request_mock):
+        url = "/v1/myresources/foo"
+        request_mock.stub_request(
+            "get",
+            url,
+            {"id": "foo2", "bobble": "scrobble"},
+        )
+
+        self.MyResource._static_request(
+            "get",
+            "/v1/myresources/foo",
+            idempotency_key="explicit",
+            params={"idempotency_key": "params", "bobble": "scrobble"},
+        )
+
+        request_mock.assert_requested(
+            "get", url, {"bobble": "scrobble"}, {"Idempotency-Key": "explicit"}
+        )
+
     def test_convert_to_stripe_object(self):
         sample = {
             "foo": "bar",
diff --git a/tests/api_resources/abstract/test_custom_method.py b/tests/api_resources/abstract/test_custom_method.py
index f02a87e9..f3471ebf 100644
--- a/tests/api_resources/abstract/test_custom_method.py
+++ b/tests/api_resources/abstract/test_custom_method.py
@@ -31,6 +31,37 @@ class TestCustomMethod(object):
             headers = util.populate_headers(idempotency_key)
             return self.request_stream("post", url, params, headers)
 
+        @classmethod
+        def _cls_do_stuff_new_codegen(
+            cls,
+            id,
+            api_key=None,
+            stripe_version=None,
+            stripe_account=None,
+            **params
+        ):
+            return cls._static_request(
+                "post",
+                "/v1/myresources/{id}/do_the_thing".format(
+                    id=util.sanitize_id(id)
+                ),
+                api_key=api_key,
+                stripe_version=stripe_version,
+                stripe_account=stripe_account,
+                params=params,
+            )
+
+        @util.class_method_variant("_cls_do_stuff_new_codegen")
+        def do_stuff_new_codegen(self, idempotency_key=None, **params):
+            return self._request(
+                "post",
+                "/v1/myresources/{id}/do_the_thing".format(
+                    id=util.sanitize_id(self.get("id"))
+                ),
+                idempotency_key=idempotency_key,
+                params=params,
+            )
+
     def test_call_custom_method_class(self, request_mock):
         request_mock.stub_request(
             "post",
@@ -193,3 +224,83 @@ class TestCustomMethod(object):
             body_content = body_content.decode("utf-8")
 
         assert body_content == "response body"
+
+    def test_call_custom_method_class_special_fields(self, request_mock):
+        request_mock.stub_request(
+            "post",
+            "/v1/myresources/mid/do_the_thing",
+            {"id": "mid", "thing_done": True},
+            rheaders={"request-id": "req_id"},
+        )
+
+        self.MyResource.do_stuff(
+            "mid",
+            foo="bar",
+            stripe_version="2017-08-15",
+            api_key="APIKEY",
+            idempotency_key="IdempotencyKey",
+            stripe_account="Acc",
+        )
+
+        request_mock.assert_requested(
+            "post",
+            "/v1/myresources/mid/do_the_thing",
+            {"foo": "bar"},
+            {"Idempotency-Key": "IdempotencyKey"},
+        )
+        request_mock.assert_api_version("2017-08-15")
+
+    def test_call_custom_method_class_newcodegen_special_fields(
+        self, request_mock
+    ):
+        request_mock.stub_request(
+            "post",
+            "/v1/myresources/mid/do_the_thing",
+            {"id": "mid", "thing_done": True},
+            rheaders={"request-id": "req_id"},
+        )
+
+        self.MyResource.do_stuff_new_codegen(
+            "mid",
+            foo="bar",
+            stripe_version="2017-08-15",
+            api_key="APIKEY",
+            idempotency_key="IdempotencyKey",
+            stripe_account="Acc",
+        )
+
+        request_mock.assert_requested(
+            "post",
+            "/v1/myresources/mid/do_the_thing",
+            {"foo": "bar"},
+            {"Idempotency-Key": "IdempotencyKey"},
+        )
+        request_mock.assert_api_version("2017-08-15")
+
+    def test_call_custom_method_instance_newcodegen_special_fields(
+        self, request_mock
+    ):
+        request_mock.stub_request(
+            "post",
+            "/v1/myresources/mid/do_the_thing",
+            {"id": "mid", "thing_done": True},
+            rheaders={"request-id": "req_id"},
+        )
+
+        obj = self.MyResource.construct_from({"id": "mid"}, "mykey")
+        obj.do_stuff_new_codegen(
+            foo="bar",
+            stripe_version="2017-08-15",
+            api_key="APIKEY",
+            idempotency_key="IdempotencyKey",
+            stripe_account="Acc",
+            headers={"extra_header": "val"},
+        )
+
+        request_mock.assert_requested(
+            "post",
+            "/v1/myresources/mid/do_the_thing",
+            {"foo": "bar"},
+            {"Idempotency-Key": "IdempotencyKey", "extra_header": "val"},
+        )
+        request_mock.assert_api_version("2017-08-15")
diff --git a/tests/api_resources/abstract/test_deletable_api_resource.py b/tests/api_resources/abstract/test_deletable_api_resource.py
index e8a452c8..532bd493 100644
--- a/tests/api_resources/abstract/test_deletable_api_resource.py
+++ b/tests/api_resources/abstract/test_deletable_api_resource.py
@@ -60,3 +60,28 @@ class TestDeletableAPIResource(object):
 
         assert obj.last_response is not None
         assert obj.last_response.request_id == "req_id"
+
+    def test_delete_with_all_special_fields(self, request_mock):
+        request_mock.stub_request(
+            "delete",
+            "/v1/mydeletables/foo",
+            {"id": "foo", "bobble": "new_scrobble"},
+            {"Idempotency-Key": "IdempotencyKey"},
+        )
+
+        self.MyDeletable.delete(
+            "foo",
+            stripe_version="2017-08-15",
+            api_key="APIKEY",
+            idempotency_key="IdempotencyKey",
+            stripe_account="Acc",
+            bobble="new_scrobble",
+        )
+
+        request_mock.assert_requested(
+            "delete",
+            "/v1/mydeletables/foo",
+            {"bobble": "new_scrobble"},
+            {"Idempotency-Key": "IdempotencyKey"},
+        )
+        request_mock.assert_api_version("2017-08-15")
diff --git a/tests/api_resources/abstract/test_updateable_api_resource.py b/tests/api_resources/abstract/test_updateable_api_resource.py
index 21c57745..16196156 100644
--- a/tests/api_resources/abstract/test_updateable_api_resource.py
+++ b/tests/api_resources/abstract/test_updateable_api_resource.py
@@ -346,3 +346,29 @@ class TestUpdateableAPIResource(object):
         res.save()
 
         request_mock.assert_api_version("2017-08-15")
+
+    def test_modify_with_all_special_fields(self, request_mock, obj):
+        request_mock.stub_request(
+            "post",
+            "/v1/myupdateables/foo",
+            {"id": "foo", "bobble": "new_scrobble"},
+            {"Idempotency-Key": "IdempotencyKey"},
+        )
+
+        self.MyUpdateable.modify(
+            "foo",
+            stripe_version="2017-08-15",
+            api_key="APIKEY",
+            idempotency_key="IdempotencyKey",
+            stripe_account="Acc",
+            bobble="new_scrobble",
+            headers={"extra_header": "val"},
+        )
+
+        request_mock.assert_requested(
+            "post",
+            "/v1/myupdateables/foo",
+            {"bobble": "new_scrobble"},
+            {"Idempotency-Key": "IdempotencyKey", "extra_header": "val"},
+        )
+        request_mock.assert_api_version("2017-08-15")