From 4d3d568c7bfc74b67e5b02a3700af06c1ba16b28 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Wed, 27 May 2026 13:44:24 -0500 Subject: [PATCH 01/12] feat(python/oauth): add OAuth client with authorization URL, token exchange and refresh Enables marketplace and platform integrations to operate on behalf of other sellers via the OAuth 2.0 authorization code flow. Operations: - get_authorization_url(): builds https://auth.mercadopago.com/authorization?... - create(): POST /oauth/token (exchange authorization code for access token) - refresh(): POST /oauth/token (refresh expired token) Ref: sdk-php/src/MercadoPago/Client/OAuth/OAuthClient.php --- mercadopago/resources/invoice.py | 62 ++++++++++++++++ mercadopago/resources/oauth.py | 119 ++++++++++++++++++++++++++++++ mercadopago/resources/point.py | 121 +++++++++++++++++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 mercadopago/resources/invoice.py create mode 100644 mercadopago/resources/oauth.py create mode 100644 mercadopago/resources/point.py diff --git a/mercadopago/resources/invoice.py b/mercadopago/resources/invoice.py new file mode 100644 index 0000000..3305f53 --- /dev/null +++ b/mercadopago/resources/invoice.py @@ -0,0 +1,62 @@ +"""Invoice resource for the MercadoPago Subscriptions API. + +Wraps ``/authorized_payments`` endpoints to retrieve and search invoices +generated by subscription (pre-approval) billing cycles. Each invoice +represents a scheduled charge attempt against the subscriber's payment +method. + +`API reference +`_ +""" +from mercadopago.core import MPBase + + +class Invoice(MPBase): + """Provides read access to subscription invoices (authorized payments). + + Each invoice corresponds to a billing cycle of a + :class:`~mercadopago.resources.preapproval.PreApproval` and tracks + the charge amount, status, retry attempts, and the resulting + payment. Use :meth:`get` and :meth:`search` to monitor billing + cycles and their outcomes. + """ + + def get(self, invoice_id, request_options=None): + """Retrieves a single invoice (authorized payment) by its ID. + + Args: + invoice_id: Unique invoice identifier assigned by + MercadoPago. + request_options: Per-call configuration overrides. + + Returns: + dict: Full invoice object including ``status``, + ``transaction_amount``, ``preapproval_id``, and + associated ``payment`` details. + + Reference: https://www.mercadopago.com/developers/en/reference/online-payments/subscriptions/get-authorized-payment/get + """ + return self._get( + uri="/authorized_payments/" + str(invoice_id), + request_options=request_options, + ) + + def search(self, filters=None, request_options=None): + """Searches invoices matching the given filters. + + Args: + filters: Query-string parameters such as ``preapproval_id``, + ``status``, ``payer_id``, ``offset``, and ``limit``. + request_options: Per-call configuration overrides. + + Returns: + dict: Paginated result with ``paging`` metadata and a + ``results`` list of matching invoices. + + Reference: https://www.mercadopago.com/developers/en/reference/online-payments/subscriptions/authorized-payment-search/get + """ + return self._get( + uri="/authorized_payments/search", + filters=filters, + request_options=request_options, + ) diff --git a/mercadopago/resources/oauth.py b/mercadopago/resources/oauth.py new file mode 100644 index 0000000..a51c22a --- /dev/null +++ b/mercadopago/resources/oauth.py @@ -0,0 +1,119 @@ +"""OAuth resource for the MercadoPago Authorization API. + +Wraps the OAuth 2.0 authorization code flow used in marketplace and +platform integrations to operate on behalf of other sellers. + +`API reference +`_ +""" +from urllib.parse import urlencode + +from mercadopago.core import MPBase + + +_AUTH_URL = "https://auth.mercadopago.com/authorization" + + +class OAuth(MPBase): + """Manages the OAuth 2.0 authorization code flow. + + Use this resource when your application needs to operate on behalf + of other MercadoPago sellers (marketplace or platform scenarios). + The flow involves redirecting the seller to the authorization URL, + receiving an authorization code, and exchanging it for access and + refresh tokens. + """ + + def get_authorization_url(self, app_id, redirect_uri, random_id): + """Builds the MercadoPago authorization URL for the OAuth flow. + + Redirect the seller to this URL to start the authorization + process. After granting permission, MercadoPago redirects back + to *redirect_uri* with a ``code`` query parameter. + + Args: + app_id: Your MercadoPago application's client ID. + redirect_uri: URI where MercadoPago sends the seller after + authorization. + random_id: CSRF-protection state parameter; should be a + unique, unpredictable value per authorization request. + + Returns: + str: Full authorization URL with query parameters. + + Reference: https://www.mercadopago.com/developers/en/docs/security/oauth/creation + """ + params = { + "client_id": app_id, + "response_type": "code", + "platform_id": "mp", + "state": random_id, + "redirect_uri": redirect_uri, + } + return _AUTH_URL + "?" + urlencode(params) + + def create(self, oauth_object, request_options=None): + """Exchanges an authorization code for an access token. + + Call this after receiving the ``code`` parameter in your + *redirect_uri* callback. The returned access token can be used + to make API requests on behalf of the authorizing seller. + + Args: + oauth_object: Dict with the authorization request fields: + ``client_secret`` (your access token), ``code`` + (authorization code received in the callback), + ``redirect_uri`` (must match the one used in + :meth:`get_authorization_url`), and ``grant_type`` + (``"authorization_code"``). + request_options: Per-call configuration overrides. + + Raises: + ValueError: If *oauth_object* is not a ``dict``. + + Returns: + dict: Token response including ``access_token``, + ``refresh_token``, and ``expires_in``. + + Reference: https://www.mercadopago.com/developers/en/reference/authentication/oauth/_oauth_token/post + """ + if not isinstance(oauth_object, dict): + raise ValueError("Param oauth_object must be a Dictionary") + + return self._post( + uri="/oauth/token", + data=oauth_object, + request_options=request_options, + ) + + def refresh(self, oauth_object, request_options=None): + """Refreshes an expired access token. + + Use this to extend the seller's session without requiring them + to re-authorize. The *refresh_token* is obtained from the + initial :meth:`create` response. + + Args: + oauth_object: Dict with the refresh request fields: + ``client_secret`` (your access token), + ``refresh_token`` (the token to refresh), and + ``grant_type`` (``"refresh_token"``). + request_options: Per-call configuration overrides. + + Raises: + ValueError: If *oauth_object* is not a ``dict``. + + Returns: + dict: New token response with a fresh ``access_token`` and + ``refresh_token``. + + Reference: https://www.mercadopago.com/developers/en/reference/authentication/oauth/_oauth_token/post + """ + if not isinstance(oauth_object, dict): + raise ValueError("Param oauth_object must be a Dictionary") + + return self._post( + uri="/oauth/token", + data=oauth_object, + request_options=request_options, + ) diff --git a/mercadopago/resources/point.py b/mercadopago/resources/point.py new file mode 100644 index 0000000..545df88 --- /dev/null +++ b/mercadopago/resources/point.py @@ -0,0 +1,121 @@ +"""Point resource for the MercadoPago Point Integration API. + +Wraps ``/point/integration-api`` endpoints for in-person payment +processing through MercadoPago Point devices (card readers). + +Supported operations: list devices, create payment intent, get payment +intent, and cancel payment intent. + +Note: The ``change_operating_mode`` operation (PATCH +``/point/integration-api/devices/{device_id}``) is not included because +the Python SDK HTTP client does not currently expose a PATCH method. + +`API reference +`_ +""" +from mercadopago.core import MPBase + + +class Point(MPBase): + """Manages payment intents on MercadoPago Point (POS) devices. + + Enables in-person payment processing by creating payment intents + that are sent to a physical Point device for the buyer to complete + the transaction by inserting or tapping their card. + """ + + def get_devices(self, filters=None, request_options=None): + """Lists Point devices linked to the authenticated account. + + Args: + filters: Optional query-string parameters such as + ``store_id`` and ``pos_id``. + request_options: Per-call configuration overrides. + + Returns: + dict: List of devices with their identifiers, operating + modes, and statuses. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/devices/list-devices/get + """ + return self._get( + uri="/point/integration-api/devices", + filters=filters, + request_options=request_options, + ) + + def create(self, device_id, payment_intent_object, request_options=None): + """Creates a payment intent on a specific Point device. + + The payment intent is sent to the physical device, where the + buyer completes the transaction. + + Args: + device_id: Unique identifier of the target Point device. + payment_intent_object: Dict describing the payment intent, + including ``amount``, ``description``, and ``payment`` + method details. + request_options: Per-call configuration overrides. + + Raises: + ValueError: If *payment_intent_object* is not a ``dict``. + + Returns: + dict: Created payment intent including its ``id`` and + ``state``. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/orders/create-order/post + """ + if not isinstance(payment_intent_object, dict): + raise ValueError("Param payment_intent_object must be a Dictionary") + + return self._post( + uri="/point/integration-api/devices/" + str(device_id) + "/payment-intents", + data=payment_intent_object, + request_options=request_options, + ) + + def get(self, payment_intent_id, request_options=None): + """Retrieves the current state of a payment intent by its ID. + + Use this to check whether the buyer has completed, cancelled, + or is still processing the payment on the device. + + Args: + payment_intent_id: Unique identifier of the payment intent. + request_options: Per-call configuration overrides. + + Returns: + dict: Payment intent details including its ``state`` and, + when completed, the associated ``payment_id``. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/orders/get-order/get + """ + return self._get( + uri="/point/integration-api/payment-intents/" + str(payment_intent_id), + request_options=request_options, + ) + + def cancel(self, device_id, payment_intent_id, request_options=None): + """Cancels a pending payment intent on a specific device. + + Use this to abort a transaction before the buyer completes the + payment on the device. + + Args: + device_id: Unique identifier of the Point device holding + the intent. + payment_intent_id: Unique identifier of the payment intent + to cancel. + request_options: Per-call configuration overrides. + + Returns: + dict: Cancellation confirmation. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/orders/cancel-order/post + """ + return self._delete( + uri="/point/integration-api/devices/" + str(device_id) + + "/payment-intents/" + str(payment_intent_id), + request_options=request_options, + ) From 3e20db0e2d9629e6a5a2ab037f528d60d318d837 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Wed, 27 May 2026 13:44:40 -0500 Subject: [PATCH 02/12] test(python): add unit tests for OAuth, Point and Invoice resources - test_oauth: validates URL builder output and ValueError on non-dict input - test_point: validates HTTP response range and ValueError on non-dict input - test_invoice: validates HTTP response range for search operation --- tests/test_invoice.py | 27 +++++++++++++++++++++++++ tests/test_oauth.py | 47 +++++++++++++++++++++++++++++++++++++++++++ tests/test_point.py | 31 ++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 tests/test_invoice.py create mode 100644 tests/test_oauth.py create mode 100644 tests/test_point.py diff --git a/tests/test_invoice.py b/tests/test_invoice.py new file mode 100644 index 0000000..7860eba --- /dev/null +++ b/tests/test_invoice.py @@ -0,0 +1,27 @@ +""" + Module: test_invoice +""" +import os +import unittest +import mercadopago + + +class TestInvoice(unittest.TestCase): + """ + Test Module: Invoice + """ + sdk = mercadopago.SDK(os.environ['ACCESS_TOKEN']) + + def test_search_invoice(self): + """ + Test Function: Invoice search returns a valid HTTP response + """ + filters_invoice = { + "limit": 5 + } + invoices = self.sdk.invoice().search(filters=filters_invoice) + self.assertIn(invoices["status"], [200, 400, 401, 404]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_oauth.py b/tests/test_oauth.py new file mode 100644 index 0000000..bc3d8af --- /dev/null +++ b/tests/test_oauth.py @@ -0,0 +1,47 @@ +""" + Module: test_oauth +""" +import os +import unittest +import mercadopago + + +class TestOAuth(unittest.TestCase): + """ + Test Module: OAuth + """ + sdk = mercadopago.SDK(os.environ['ACCESS_TOKEN']) + + def test_get_authorization_url(self): + """ + Test Function: OAuth get_authorization_url builds correct URL + """ + url = self.sdk.oauth().get_authorization_url( + app_id="TEST_APP_ID", + redirect_uri="https://example.com/callback", + random_id="csrf_state_token", + ) + self.assertIn("https://auth.mercadopago.com/authorization", url) + self.assertIn("client_id=TEST_APP_ID", url) + self.assertIn("response_type=code", url) + self.assertIn("redirect_uri=", url) + self.assertIn("state=csrf_state_token", url) + self.assertIn("platform_id=mp", url) + + def test_create_raises_on_invalid_param(self): + """ + Test Function: OAuth create raises ValueError for non-dict input + """ + with self.assertRaises(ValueError): + self.sdk.oauth().create("not_a_dict") + + def test_refresh_raises_on_invalid_param(self): + """ + Test Function: OAuth refresh raises ValueError for non-dict input + """ + with self.assertRaises(ValueError): + self.sdk.oauth().refresh("not_a_dict") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_point.py b/tests/test_point.py new file mode 100644 index 0000000..d2e3ab0 --- /dev/null +++ b/tests/test_point.py @@ -0,0 +1,31 @@ +""" + Module: test_point +""" +import os +import unittest +import mercadopago + + +class TestPoint(unittest.TestCase): + """ + Test Module: Point + """ + sdk = mercadopago.SDK(os.environ['ACCESS_TOKEN']) + + def test_get_devices(self): + """ + Test Function: Point get_devices returns a valid HTTP response + """ + devices = self.sdk.point().get_devices() + self.assertIn(devices["status"], [200, 400, 401, 403, 404]) + + def test_create_raises_on_invalid_param(self): + """ + Test Function: Point create raises ValueError for non-dict payment intent + """ + with self.assertRaises(ValueError): + self.sdk.point().create("device_123", "not_a_dict") + + +if __name__ == "__main__": + unittest.main() From 6608c72552870abdcc412f5f8f189196ab364697 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Wed, 27 May 2026 13:44:57 -0500 Subject: [PATCH 03/12] docs(python): add usage examples for OAuth, Point and Invoice - examples/oauth/create_token.py: authorization URL + token exchange flow - examples/point/create_payment_intent.py: list devices, create and get payment intent - examples/invoice/get_invoice.py: search invoices by preapproval_id and get by ID --- mercadopago/examples/invoice/get_invoice.py | 29 +++++++++++++++ mercadopago/examples/oauth/create_token.py | 33 +++++++++++++++++ .../examples/point/create_payment_intent.py | 37 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 mercadopago/examples/invoice/get_invoice.py create mode 100644 mercadopago/examples/oauth/create_token.py create mode 100644 mercadopago/examples/point/create_payment_intent.py diff --git a/mercadopago/examples/invoice/get_invoice.py b/mercadopago/examples/invoice/get_invoice.py new file mode 100644 index 0000000..244dfcb --- /dev/null +++ b/mercadopago/examples/invoice/get_invoice.py @@ -0,0 +1,29 @@ +from mercadopago import SDK + + +def main(): + # Initialize the SDK with your access token + access_token = "" + sdk = SDK(access_token) + + # Search invoices for a specific subscription (preapproval) + filters = { + "preapproval_id": "", + "limit": 10, + } + + try: + invoices = sdk.invoice().search(filters=filters) + print("Invoices found:", invoices["response"]) + + # Retrieve a specific invoice by ID + if invoices["response"].get("results"): + invoice_id = invoices["response"]["results"][0]["id"] + invoice = sdk.invoice().get(invoice_id) + print("Invoice details:", invoice["response"]) + except Exception as e: + print("Error:", e) + + +if __name__ == "__main__": + main() diff --git a/mercadopago/examples/oauth/create_token.py b/mercadopago/examples/oauth/create_token.py new file mode 100644 index 0000000..7170f9c --- /dev/null +++ b/mercadopago/examples/oauth/create_token.py @@ -0,0 +1,33 @@ +from mercadopago import SDK + + +def main(): + # Initialize the SDK with your marketplace access token + access_token = "" + sdk = SDK(access_token) + + # Step 1: Build the authorization URL and redirect the seller to it + auth_url = sdk.oauth().get_authorization_url( + app_id="", + redirect_uri="https://yourapp.com/callback", + random_id="", + ) + print("Redirect seller to:", auth_url) + + # Step 2: After the seller authorizes, exchange the received code for tokens + oauth_object = { + "client_secret": access_token, + "code": "", + "redirect_uri": "https://yourapp.com/callback", + "grant_type": "authorization_code", + } + + try: + response = sdk.oauth().create(oauth_object) + print("Token created successfully:", response["response"]) + except Exception as e: + print("Error:", e) + + +if __name__ == "__main__": + main() diff --git a/mercadopago/examples/point/create_payment_intent.py b/mercadopago/examples/point/create_payment_intent.py new file mode 100644 index 0000000..68a0623 --- /dev/null +++ b/mercadopago/examples/point/create_payment_intent.py @@ -0,0 +1,37 @@ +from mercadopago import SDK + + +def main(): + # Initialize the SDK with your access token + access_token = "" + sdk = SDK(access_token) + + # List available Point devices to get a device_id + devices = sdk.point().get_devices() + print("Available devices:", devices["response"]) + + # Create a payment intent on a specific device + device_id = "" + payment_intent_object = { + "amount": 1500, + "description": "Product purchase", + "payment": { + "installments": 1, + "type": "credit_card", + }, + } + + try: + response = sdk.point().create(device_id, payment_intent_object) + print("Payment intent created:", response["response"]) + + # Retrieve the payment intent status + payment_intent_id = response["response"]["id"] + status = sdk.point().get(payment_intent_id) + print("Payment intent status:", status["response"]) + except Exception as e: + print("Error:", e) + + +if __name__ == "__main__": + main() From 17bd29b8ed2b5b36d51cf5b7054a5d3e6c80c018 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Wed, 27 May 2026 13:45:10 -0500 Subject: [PATCH 04/12] feat(python): register OAuth, Point and Invoice in SDK entry point - resources/__init__.py: add Invoice, OAuth, Point imports and __all__ entries - sdk.py: add Invoice, OAuth, Point imports and factory methods (sdk.invoice(), sdk.oauth(), sdk.point()) --- mercadopago/resources/__init__.py | 6 ++++++ mercadopago/sdk.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/mercadopago/resources/__init__.py b/mercadopago/resources/__init__.py index 0b58843..a4e4baf 100644 --- a/mercadopago/resources/__init__.py +++ b/mercadopago/resources/__init__.py @@ -13,11 +13,14 @@ from mercadopago.resources.customer import Customer from mercadopago.resources.disbursement_refund import DisbursementRefund from mercadopago.resources.identification_type import IdentificationType +from mercadopago.resources.invoice import Invoice from mercadopago.resources.merchant_order import MerchantOrder +from mercadopago.resources.oauth import OAuth from mercadopago.resources.order import Order from mercadopago.resources.payment import Payment from mercadopago.resources.payment_methods import PaymentMethods from mercadopago.resources.plan import Plan +from mercadopago.resources.point import Point from mercadopago.resources.preapproval import PreApproval from mercadopago.resources.preference import Preference from mercadopago.resources.refund import Refund @@ -34,11 +37,14 @@ 'DisbursementRefund', 'HttpClient', 'IdentificationType', + 'Invoice', 'MerchantOrder', + 'OAuth', 'Order', 'Payment', 'PaymentMethods', 'Plan', + 'Point', 'PreApproval', 'Preference', 'Refund', diff --git a/mercadopago/sdk.py b/mercadopago/sdk.py index e225556..d7c6b38 100644 --- a/mercadopago/sdk.py +++ b/mercadopago/sdk.py @@ -24,11 +24,14 @@ Customer, DisbursementRefund, IdentificationType, + Invoice, MerchantOrder, + OAuth, Order, Payment, PaymentMethods, Plan, + Point, PreApproval, Preference, Refund, @@ -116,6 +119,21 @@ def identification_type(self, request_options=None): return IdentificationType(request_options is not None and request_options or self.request_options, self.http_client) + def invoice(self, request_options=None): + """Creates an :class:`Invoice` resource to retrieve subscription billing invoices.""" + return Invoice(request_options is not None and request_options + or self.request_options, self.http_client) + + def oauth(self, request_options=None): + """Creates an :class:`OAuth` resource for the OAuth 2.0 authorization code flow.""" + return OAuth(request_options is not None and request_options + or self.request_options, self.http_client) + + def point(self, request_options=None): + """Creates a :class:`Point` resource for in-person payments via Point devices.""" + return Point(request_options is not None and request_options + or self.request_options, self.http_client) + def merchant_order(self, request_options=None): """Creates a :class:`MerchantOrder` resource for Checkout Pro orders.""" return MerchantOrder(request_options is not None and request_options From 9458b52b6eda650ccc7a784f1cd10d8ee3d0e99e Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Wed, 27 May 2026 13:45:38 -0500 Subject: [PATCH 05/12] chore(python): create CHANGELOG.md and prepare v3.2.0 release notes New features in this release: - OAuth: authorization URL, token exchange and refresh (POST /oauth/token) - Point: payment intents and device management (/point/integration-api/...) - Invoice: get and search subscription invoices (GET /authorized_payments) Note: version bump in pyproject.toml and config.py requires approval before merge. --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..611c4b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.2.0] - 2026-05-27 + +### Added + +- **OAuth**: authorization URL builder, token exchange, and token refresh + (`sdk.oauth().get_authorization_url()`, `.create()`, `.refresh()`). + Enables marketplace and platform integrations to operate on behalf of + other sellers via the OAuth 2.0 authorization code flow. + Endpoints: `POST /oauth/token`. + +- **Point**: in-person payment intent management for MercadoPago Point + devices (`sdk.point().get_devices()`, `.create()`, `.get()`, `.cancel()`). + Enables card-present transactions through physical Point card readers. + Endpoints: `GET /point/integration-api/devices`, + `POST /point/integration-api/devices/{device_id}/payment-intents`, + `GET /point/integration-api/payment-intents/{id}`, + `DELETE /point/integration-api/devices/{device_id}/payment-intents/{id}`. + Note: `change_operating_mode` (PATCH) is not included; requires HttpClient + PATCH support. + +- **Invoice**: retrieval and search of subscription billing invoices + (`sdk.invoice().get()`, `.search()`). Enables monitoring of authorized + payments generated by PreApproval billing cycles. + Endpoints: `GET /authorized_payments/{id}`, + `GET /authorized_payments/search`. From c10dcc96843ee95c61e02c0ee1f79677ae21124e Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Wed, 27 May 2026 13:50:38 -0500 Subject: [PATCH 06/12] chore(python): bump version to 3.2.0 New features in this release: - OAuth: authorization URL, token exchange and refresh (POST /oauth/token) - Point: payment intents and device management (/point/integration-api/...) - Invoice: get and search subscription invoices (GET /authorized_payments) --- mercadopago/config/config.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mercadopago/config/config.py b/mercadopago/config/config.py index 36d74c8..1c79773 100644 --- a/mercadopago/config/config.py +++ b/mercadopago/config/config.py @@ -26,7 +26,7 @@ class Config: def __init__(self): """Builds version-dependent values (user_agent, tracking_id).""" - self.__version = "3.1.1" + self.__version = "3.2.0" self.__user_agent = "MercadoPago Python SDK v" + self.__version self.__product_id = "bc32bpftrpp001u8nhlg" self.__tracking_id = "platform:" + platform.python_version() diff --git a/pyproject.toml b/pyproject.toml index 5cba398..1709421 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mercadopago" -version = "3.1.1" +version = "3.2.0" authors = [ {name = "Mercado Pago", email = "mp_sdk@mercadopago.com"} ] From fffe96de435fe6037c747931533ba79ea8ad4250 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Thu, 28 May 2026 12:43:27 -0500 Subject: [PATCH 07/12] fix lint --- mercadopago/sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mercadopago/sdk.py b/mercadopago/sdk.py index d7c6b38..1b0b899 100644 --- a/mercadopago/sdk.py +++ b/mercadopago/sdk.py @@ -40,7 +40,7 @@ ) -class SDK: +class SDK: # pylint: disable=too-many-public-methods """Central client providing factory methods for every API resource. Each factory method (e.g. :meth:`payment`, :meth:`order`) returns a From fc14eb70cd72a0bb2e16699e8f43326fef34deff Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Thu, 28 May 2026 12:52:11 -0500 Subject: [PATCH 08/12] upgrade request --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1709421..1fa201b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ keywords = ["api", "mercadopago", "checkout", "payment in sdk integration", "lts license = {file = "LICENSE"} requires-python = ">=3.9" dependencies = [ - "requests>=2.32.4,<3" + "requests>=2.33.0,<3" ] classifiers = [ "License :: OSI Approved :: MIT License", From d45177eef6eb72cb1352c7407a1d1963bbcfbac2 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Thu, 28 May 2026 12:59:49 -0500 Subject: [PATCH 09/12] fix test --- tests/test_order.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_order.py b/tests/test_order.py index 34445b9..5a7d9d9 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -222,8 +222,9 @@ def test_process_order(self): def test_cancel_order(self): card_token_id = self.create_master_test_card() order_id = self.create_order_canceled_or_captured(card_token_id) - time.sleep(10) - order_canceled = self.sdk.order().cancel(order_id) + order_canceled = api_call_with_retry( + lambda: self.sdk.order().cancel(order_id), expected_status=200 + ) self.assertEqual(order_canceled["status"], 200) self.assertEqual(order_canceled["response"]["status"], "canceled") From 349d5721c38e8eb2d8e4b878ca4736307548ce09 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Thu, 28 May 2026 13:02:08 -0500 Subject: [PATCH 10/12] fix lint --- tests/test_order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_order.py b/tests/test_order.py index 5a7d9d9..7df6b59 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -2,7 +2,6 @@ Module: test_order """ import os -import time import unittest import random from datetime import datetime, timezone, timedelta From e18f1a4d0ab829a528a3cc8c665c70d296b02e8e Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Thu, 28 May 2026 20:35:29 -0500 Subject: [PATCH 11/12] feat(order): add AP dataclasses, docs and example for automatic payments (IXE-679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add typed helpers and documentation for the Automatic Payments flow in the Orders API. The SDK remains dict-based; these additions are optional conveniences for developers who prefer typed objects. New dataclasses (mercadopago/resources/): - order_stored_credential.py: OrderStoredCredential — includes the new prev_transaction_ref field, required from the second recurring charge onwards to link the payment to the original card-network authorization. - order_automatic_payments.py: OrderAutomaticPayments — payment_profile_id, retries, schedule_date, due_date. - order_subscription_data.py: OrderSubscriptionData, OrderSubscriptionSequence, OrderInvoicePeriod — full subscription billing cycle support. - order_integration_data.py: OrderIntegrationData, OrderSponsor — integration metadata for marketplace and platform identification. Updated: - order.py: added class-level docstring documenting all AP-supported dict keys with types (stored_credential, automatic_payments, subscription_data, integration_data). New example: - examples/order/create_order_automatic_payment.py: full AP flow showing first payment (first_payment=True) and recurring charge (prev_transaction_ref=prev_tx_id) using dataclasses.asdict(). No break changes: existing dict-based API is unmodified. Developers use dataclasses.asdict() to convert typed objects to the dict the SDK expects. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../order/create_order_automatic_payment.py | 137 ++++++++++++++++++ mercadopago/resources/order.py | 54 +++++++ .../resources/order_automatic_payments.py | 27 ++++ .../resources/order_integration_data.py | 36 +++++ .../resources/order_stored_credential.py | 31 ++++ .../resources/order_subscription_data.py | 51 +++++++ 6 files changed, 336 insertions(+) create mode 100644 examples/order/create_order_automatic_payment.py create mode 100644 mercadopago/resources/order_automatic_payments.py create mode 100644 mercadopago/resources/order_integration_data.py create mode 100644 mercadopago/resources/order_stored_credential.py create mode 100644 mercadopago/resources/order_subscription_data.py diff --git a/examples/order/create_order_automatic_payment.py b/examples/order/create_order_automatic_payment.py new file mode 100644 index 0000000..172ccad --- /dev/null +++ b/examples/order/create_order_automatic_payment.py @@ -0,0 +1,137 @@ +"""Example: Automatic Payments flow with the MercadoPago Orders API. + +Demonstrates both the first payment (storing the card credential) and a +subsequent recurring charge (referencing the previous transaction). +""" +import dataclasses +import mercadopago +from mercadopago.resources.order_automatic_payments import OrderAutomaticPayments +from mercadopago.resources.order_stored_credential import OrderStoredCredential +from mercadopago.resources.order_subscription_data import ( + OrderInvoicePeriod, + OrderSubscriptionData, + OrderSubscriptionSequence, +) + +sdk = mercadopago.SDK("YOUR_ACCESS_TOKEN") + + +# ── First payment ────────────────────────────────────────────────────────── +# Use first_payment=True and no prev_transaction_ref. +# The API stores the credential for future charges. + +first_payment_order = { + "type": "online", + "total_amount": "100.00", + "external_reference": "subscription-001-payment-1", + "payer": { + "email": "customer@example.com", + "customer_id": "CUSTOMER_ID", + }, + "transactions": { + "payments": [ + { + "amount": "100.00", + "payment_method": { + "id": "master", + "type": "credit_card", + "token": "CARD_TOKEN", + "installments": 1, + }, + "automatic_payments": dataclasses.asdict( + OrderAutomaticPayments( + payment_profile_id="PAYMENT_PROFILE_ID", + schedule_date="2026-07-01T00:00:00.000-04:00", + due_date="2026-07-05T00:00:00.000-04:00", + retries=3, + ) + ), + "stored_credential": dataclasses.asdict( + OrderStoredCredential( + payment_initiator="merchant", + reason="recurring", + store_payment_method=True, + first_payment=True, + # prev_transaction_ref not required on first payment + ) + ), + "subscription_data": dataclasses.asdict( + OrderSubscriptionData( + invoice_id="INVOICE_001", + billing_date="2026-06-01", + subscription_sequence=OrderSubscriptionSequence(number=1, total=12), + invoice_period=OrderInvoicePeriod(type="monthly", period=1), + ) + ), + } + ] + }, +} + +result = sdk.order().create(first_payment_order) +first_order = result["response"] +print("First payment order:", first_order.get("id"), "→", first_order.get("status")) + +# Save the transaction ID for use in the next charge. +first_transaction_id = ( + first_order.get("transactions", {}) + .get("payments", [{}])[0] + .get("id", "") +) + + +# ── Subsequent recurring charge ──────────────────────────────────────────── +# Use first_payment=False and include prev_transaction_ref pointing to the +# previous charge. The API links this payment to the original authorization. + +recurring_order = { + "type": "online", + "total_amount": "100.00", + "external_reference": "subscription-001-payment-2", + "payer": { + "email": "customer@example.com", + "customer_id": "CUSTOMER_ID", + }, + "transactions": { + "payments": [ + { + "amount": "100.00", + "payment_method": { + "id": "master", + "type": "credit_card", + "token": "CARD_TOKEN", + "installments": 1, + }, + "automatic_payments": dataclasses.asdict( + OrderAutomaticPayments( + payment_profile_id="PAYMENT_PROFILE_ID", + schedule_date="2026-08-01T00:00:00.000-04:00", + due_date="2026-08-05T00:00:00.000-04:00", + retries=3, + ) + ), + "stored_credential": dataclasses.asdict( + OrderStoredCredential( + payment_initiator="merchant", + reason="recurring", + store_payment_method=False, + first_payment=False, + prev_transaction_ref=first_transaction_id, # required + ) + ), + "subscription_data": dataclasses.asdict( + OrderSubscriptionData( + invoice_id="INVOICE_002", + billing_date="2026-07-01", + subscription_sequence=OrderSubscriptionSequence(number=2, total=12), + invoice_period=OrderInvoicePeriod(type="monthly", period=1), + ) + ), + } + ] + }, +} + +result = sdk.order().create(recurring_order) +recurring = result["response"] +print("Recurring charge order:", recurring.get("id"), "→", recurring.get("status")) diff --git a/mercadopago/resources/order.py b/mercadopago/resources/order.py index 9c15479..ae992bd 100644 --- a/mercadopago/resources/order.py +++ b/mercadopago/resources/order.py @@ -14,6 +14,60 @@ class Order(MPBase): An order groups one or more payment transactions and supports a two-step (authorise then capture) flow as well as direct processing. + + Automatic Payments (AP) — supported keys for ``stored_credential`` dict: + + .. code-block:: python + + "stored_credential": { + "payment_initiator": str, # "cardholder" | "merchant" + "reason": str, # "recurring" | "installment" | "unscheduled" + "store_payment_method": bool, + "first_payment": bool, + "prev_transaction_ref": str, # ID of the previous transaction in the series. + # Required from the second charge onwards. + } + + Supported keys for ``automatic_payments`` dict: + + .. code-block:: python + + "automatic_payments": { + "payment_profile_id": str, # Stored payment profile ID. + "retries": int, # Max retry attempts on failure. + "schedule_date": str, # ISO 8601 scheduled charge date. + "due_date": str, # ISO 8601 payment due date. + } + + Supported keys for ``subscription_data`` dict: + + .. code-block:: python + + "subscription_data": { + "invoice_id": str, # ID of the invoice being paid. + "billing_date": str, # ISO 8601 billing date. + "subscription_sequence": { + "number": int, # Current payment number in the series. + "total": int, # Total payments in the subscription. + }, + "invoice_period": { + "type": str, # "monthly" | "daily" | "yearly". + "period": int, # Number of period units. + }, + } + + Supported keys for ``integration_data`` dict (root of order request): + + .. code-block:: python + + "integration_data": { + "integrator_id": str, # Certified integrator ID. + "platform_id": str, # Platform ID assigned by MercadoPago. + "corporation_id": str, # Corporation ID for multi-account setups. + "sponsor": { + "id": str, # MercadoPago user ID of the sponsor. + }, + } """ def search(self, filters=None, request_options=None): diff --git a/mercadopago/resources/order_automatic_payments.py b/mercadopago/resources/order_automatic_payments.py new file mode 100644 index 0000000..15bdbb2 --- /dev/null +++ b/mercadopago/resources/order_automatic_payments.py @@ -0,0 +1,27 @@ +"""Dataclass for automatic/recurring payment scheduling in order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderAutomaticPayments: + """Configuration for automatic (recurring) payment scheduling within an order. + + Use this dataclass to build the ``automatic_payments`` payload when creating + an order with Automatic Payments. Convert to dict with ``dataclasses.asdict()``. + + Attributes: + payment_profile_id: Identifier of the stored payment profile used for + automatic charges. Type: str. + retries: Maximum number of retry attempts if the automatic charge fails. + Type: int. + schedule_date: ISO 8601 date when the automatic payment is scheduled to + be charged (e.g. ``"2026-06-01T00:00:00.000-04:00"``). Type: str. + due_date: ISO 8601 date by which the payment must be completed before it + is considered overdue. Type: str. + """ + + payment_profile_id: Optional[str] = None + retries: Optional[int] = None + schedule_date: Optional[str] = None + due_date: Optional[str] = None diff --git a/mercadopago/resources/order_integration_data.py b/mercadopago/resources/order_integration_data.py new file mode 100644 index 0000000..e729807 --- /dev/null +++ b/mercadopago/resources/order_integration_data.py @@ -0,0 +1,36 @@ +"""Dataclasses for integration metadata in order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderSponsor: + """Sponsoring marketplace owner associated with an order. + + Attributes: + id: MercadoPago user ID of the sponsoring marketplace owner. Type: str. + """ + + id: Optional[str] = None + + +@dataclass +class OrderIntegrationData: + """Integration metadata for an order request. + + Identifies the integrator, platform, and corporation associated with the + integration. Use this dataclass to build the ``integration_data`` payload + at the root of the order create request. Convert to dict with + ``dataclasses.asdict()``. + + Attributes: + integrator_id: Identifier of the certified integrator. Type: str. + platform_id: Platform identifier assigned by MercadoPago. Type: str. + corporation_id: Corporation identifier for multi-account setups. Type: str. + sponsor: Sponsoring marketplace owner information. + """ + + integrator_id: Optional[str] = None + platform_id: Optional[str] = None + corporation_id: Optional[str] = None + sponsor: Optional[OrderSponsor] = None diff --git a/mercadopago/resources/order_stored_credential.py b/mercadopago/resources/order_stored_credential.py new file mode 100644 index 0000000..54ad1d2 --- /dev/null +++ b/mercadopago/resources/order_stored_credential.py @@ -0,0 +1,31 @@ +"""Dataclass for stored credential (card-on-file) data in automatic payment flows.""" +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass +class OrderStoredCredential: + """Stored credential configuration for recurring payments (card-on-file). + + Use this dataclass to build the ``stored_credential`` payload when creating + an order with Automatic Payments. Convert to dict with ``dataclasses.asdict()``. + + Attributes: + payment_initiator: Who initiated the payment: ``"cardholder"`` or ``"merchant"``. + Type: str. + reason: Reason for using the stored credential. + One of ``"recurring"``, ``"installment"``, ``"unscheduled"``. Type: str. + store_payment_method: Whether to save the payment method for future charges. + Type: bool. + first_payment: ``True`` for the initial authorization; ``False`` for + subsequent recurring charges. Type: bool. + prev_transaction_ref: Identifier of the previous transaction in the recurring + series. Required from the second charge onwards to link this payment to the + original card-network authorization. Type: str. + """ + + payment_initiator: Optional[str] = None + reason: Optional[str] = None + store_payment_method: Optional[bool] = None + first_payment: Optional[bool] = None + prev_transaction_ref: Optional[str] = None diff --git a/mercadopago/resources/order_subscription_data.py b/mercadopago/resources/order_subscription_data.py new file mode 100644 index 0000000..8638ba6 --- /dev/null +++ b/mercadopago/resources/order_subscription_data.py @@ -0,0 +1,51 @@ +"""Dataclass for subscription billing data in automatic payment order requests.""" +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass +class OrderSubscriptionSequence: + """Position of a payment within a subscription series. + + Attributes: + number: Current payment number in the subscription (1-based). Type: int. + total: Total number of payments expected in the plan. Type: int. + """ + + number: Optional[int] = None + total: Optional[int] = None + + +@dataclass +class OrderInvoicePeriod: + """Billing period covered by a subscription invoice. + + Attributes: + type: Period unit (e.g. ``"monthly"``, ``"daily"``, ``"yearly"``). Type: str. + period: Number of period units covered by this invoice. Type: int. + """ + + type: Optional[str] = None + period: Optional[int] = None + + +@dataclass +class OrderSubscriptionData: + """Subscription billing information for a payment within an order. + + Use this dataclass to build the ``subscription_data`` payload when creating + an order with Automatic Payments. Convert to dict with ``dataclasses.asdict()``. + + Attributes: + invoice_id: Identifier of the invoice being settled by this payment. + Type: str. + billing_date: ISO 8601 date when the subscription billing was triggered. + Type: str. + subscription_sequence: Position of this payment in the subscription series. + invoice_period: Billing period covered by this invoice. + """ + + invoice_id: Optional[str] = None + billing_date: Optional[str] = None + subscription_sequence: Optional[OrderSubscriptionSequence] = None + invoice_period: Optional[OrderInvoicePeriod] = None From 57cc172a94766a331bc70e9fadae57c33400019a Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Fri, 29 May 2026 11:11:31 -0500 Subject: [PATCH 12/12] fix(order): remove unused field import from dataclasses (W0611) Remove the unused 'field' import from dataclasses in order_stored_credential.py and order_subscription_data.py to resolve pylint W0611 warnings. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- mercadopago/resources/order_stored_credential.py | 2 +- mercadopago/resources/order_subscription_data.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mercadopago/resources/order_stored_credential.py b/mercadopago/resources/order_stored_credential.py index 54ad1d2..e3d296e 100644 --- a/mercadopago/resources/order_stored_credential.py +++ b/mercadopago/resources/order_stored_credential.py @@ -1,5 +1,5 @@ """Dataclass for stored credential (card-on-file) data in automatic payment flows.""" -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional diff --git a/mercadopago/resources/order_subscription_data.py b/mercadopago/resources/order_subscription_data.py index 8638ba6..055e282 100644 --- a/mercadopago/resources/order_subscription_data.py +++ b/mercadopago/resources/order_subscription_data.py @@ -1,5 +1,5 @@ """Dataclass for subscription billing data in automatic payment order requests.""" -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional