# MD for: https://www.mercadopago.com.ar/developers/es/docs/automatic-payments/card-updater-notifications.md \# Configure \_Card Updater\_ notifications \_Card Updater\_ is a Mercado Pago feature that automatically retrieves and updates stored card data, ensuring the continuity of recurring payments without CVV when a card expires, is replaced, or undergoes any change in its lifecycle. Whenever a change occurs in a card's lifecycle — expiration, loss, theft, category upgrade, or data correction — Mercado Pago performs direct synchronization with card networks and issuers. Once the database is updated, it triggers a Webhook notification to your application so you can update your records asynchronously and automatically. sequenceDiagram participant E as Issuer / Card Network participant MP as Mercado Pago participant App as Your application E->>MP: Card lifecycle change MP->>MP: Syncs credentials in the database MP->>App: Webhook: card.updated App-->>MP: HTTP 200 / 201 App->>MP: GET /v1/customers/{id}/cards (optional) MP-->>App: New card data App->>App: Updates card_id for upcoming charges The \_Card Updater\_ notification topic mitigates rejected payments caused by outdated data, resolving errors such as: - \*\*Data entry errors\*\*: \`Bad\_Filled\_Card\_Number\`, \`Bad\_Filled\_Card\_Date\`, \`Bad\_Filled\_Security\_Code\`. - \*\*Card status restrictions\*\*: \`Card\_Disabled\`, \`Blacklist\`, \`Call\_For\_Authorized\`, \`Other\_Reason\`. - \*\*Credential replacement\*\*: transitions from expired cards or category migration (for example, from Gold to Black). By processing the \`card.updated\` event, your application ensures billing continuity without manual intervention from the end customer. ## How does it work? Depending on the change made by the card network or issuer, the card identifier (\`card\_id\`) can undergo two types of modifications: - \*\*\`card\_id\` change\*\*: occurs when a new card number (PAN) is generated. The Webhook notification sent by Mercado Pago will include the \`new\_card\_id\` field, which replaces the previous identifier. - \*\*Silent update\*\*: for minor corrections, the \`card\_id\` remains the same and the update occurs transparently in Mercado Pago's database. Additionally, all cards generated by \_Card Updater\_ are \*\*automatically added to the respective Customer\*\* stored in Mercado Pago, with a \*\*limit of up to 20 cards\*\*. To validate the details of the new card or list all active cards for a customer, use the :TagComponent{tag="GET" text="/v1/customers/{id}/cards" href="/developers/en/reference/online-payments/automatic-payments/cards/get-customer-cards/get"} endpoint. Always keep the \`card\_id\` reference in your database updated after receiving each \`card.updated\` notification. When an ID change occurs, ensure that \*\*all upcoming charges use the \`new\_card\_id\`\*\*. Using an outdated \`card\_id\` after the update will result in a high probability of payment rejection. ## Configure Webhooks Follow the steps below to configure your endpoints and start receiving \`card.updated\` events. 1\. Access the \[Developer Panel\](https://www.mercadopago.com/developers/panel) and select the application that will receive \_Card Updater\_ updates. 2\. In the left menu, select \*\*Notifications > Webhooks\*\*. 3\. Configure the URLs that will receive notifications. We recommend using separate URLs for test and production environments: - \*\*Test URL:\*\* use during development, exclusively with :toolTipComponent\[test credentials\]{content="Unique credentials that identify your integration in test environments." link="/developers/en/docs/your-integrations/credentials"}. - \*\*Production URL:\*\* use with your integration already in production, configured with :toolTipComponent\[production credentials\]{content="Unique credentials that identify your integration in production environments." link="/developers/en/docs/your-integrations/credentials"}. > NOTE > > If your integration manages multiple sellers, you can append query parameters to the URL to facilitate internal routing. For example: \`https://your-endpoint.com/webhook?client\_id=SELLER\_ID\`. 4\. Under \*\*Recommended events for Checkout API integrations\*\*, select the \*\*Card Updater\*\* option. 5\. Finally, click \*\*Save configuration\*\*. This will generate a :toolTipComponent\[secret key\]{content="Key generated in the Developer Panel to validate the authenticity of webhook notifications." link="/developers/en/docs/your-integrations/notifications/webhooks"} for your application. Note that this key has no expiration date and periodic renewal is not mandatory, though recommended. To do so, click the \*\*Reset\*\* button. ## Simulate receiving the notification To ensure notifications are configured correctly, simulate receiving them by following the steps below. 1\. After configuring the URL and event, click \*\*Save configuration\*\*. 2\. Then click \*\*Simulate notification\*\* to check if the indicated URL is receiving notifications correctly. 3\. On the simulation screen, select the URL to test. 4\. Choose the \*\*Card Updater\*\* event type and enter the \*\*notification ID\*\* to be sent in the notification body (\`Data ID\`). 5\. Finally, click \*\*Send test\*\* to verify the request, the server response, and the event description. ## Validate the notification origin Validating the origin of each request is essential to ensure the authenticity of received notifications and prevent fraud. Mercado Pago will send your server a notification similar to the example below for a \_Card Updater\_ topic alert. \`\`\`json { "id": "evt\_123456789", "action": "card.updated", "type": "automatic-payments", "api\_version": "v1", "application\_id": 8339021212080291, "user\_id": 1197520450, "date\_created": "2024-01-28T15:00:00-03:00", "data": { "customer\_id": "cust\_987654321", "new\_card\_id": 50000102202, "old\_card\_id": 50000006036 } } \`\`\` | Field | Type | Description | |---|---|---| | \`id\` | string | Notification identifier. Use it for idempotency control. | | \`action\` | string | Event action. Always \`card.updated\`. | | \`type\` | string | Event origin. Always \`automatic-payments\`. | | \`application\_id\` | long | Your application identifier in Mercado Pago. | | \`user\_id\` | long | Seller identifier. | | \`date\_created\` | string | Notification creation date (ISO 8601). | | \`data.customer\_id\` | string | Identifier of the customer who owns the card. | | \`data.old\_card\_id\` | long | Previous identifier of the replaced card. | | \`data.new\_card\_id\` | long | New identifier of the updated card. \*\*Present only in PAN change cases\*\*. | The secret key generated when saving the configuration is sent in the \`x-signature\` \_header\_ of each request, with the following format: \`\`\` ts=1742505638683,v1=ced36ab6d33566bb1e16c125819b8d840d6b8ef136b0b9127c76064466f5229b \`\`\` To confirm the validation, extract the key from the \_header\_ and compare it with the key provided for your application in \[Your integrations\](https://www.mercadopago.com.ar/developers/panel/app). To confirm the validation, it is necessary to extract the key from the \_header\_ and compare it with the key provided for your application in \[Your integrations\](https://www.mercadopago.com.ar/developers/panel/app). Follow one of the approaches below to validate the authenticity of the notification. ::::TabsComponent :::TabComponent{title="With SDKs"} The official SDK implements HMAC-based Webhook Signature Verification to authenticate the origin of each received notification. To get your secret key (\`secret\`), select the application in \[Your integrations\](https://www.mercadopago.com.ar/developers/panel/app), click \*\*Webhooks > Configure notification\*\*, and reveal the generated key. * [csharp ](#editor%5F5) * [go ](#editor%5F4) * [java ](#editor%5F6) * [javascript ](#editor%5F2) * [php ](#editor%5F1) * [python ](#editor%5F3) * [ruby ](#editor%5F7) php javascript python go csharp java ruby ``` NOTE > > If any of the values (\`data.id\`, \`x-request-id\`) are not present in the received notification, you must remove them from the manifest before computing the \`HMAC\`. 3\. In \[Your integrations\](https://www.mercadopago.com.ar/developers/panel/app), select the integrated application, click \*\*Webhooks > Configure notification\*\* and reveal the generated secret key. 4\. Generate the counter-key for validation. To do this, compute an \[HMAC\](https://en.wikipedia.org/wiki/HMAC) with the \`SHA256 hash\` function in hexadecimal base, using the secret key as the key and the \_template\_ with the values as the message. * [java ](#editor%5F10) * [node ](#editor%5F9) * [php ](#editor%5F8) * [python ](#editor%5F11) php node java python ``` $cyphedSignature = hash_hmac('sha256', $data, $key); ``` Copiar ``` const crypto = require('crypto'); const cyphedSignature = crypto .createHmac('sha256', secret) .update(signatureTemplateParsed) .digest('hex'); ``` Copiar ``` String cyphedSignature = new HmacUtils("HmacSHA256", secret).hmacHex(signedTemplate); ``` Copiar ``` import hashlib, hmac, binascii cyphedSignature = binascii.hexlify(hmac.new(secret.encode(), signedTemplate.encode(), hashlib.sha256).digest()) ``` Copiar 5\. Finally, compare the generated key with the key extracted from the \_header\_, ensuring they match exactly. Additionally, you can use the \_timestamp\_ extracted from the \_header\_ to compare it with a \_timestamp\_ generated at the time of receipt, in order to establish a delay tolerance in receiving the message. Here are complete code examples: * [csharp ](#editor%5F16) * [go ](#editor%5F15) * [java ](#editor%5F17) * [javascript ](#editor%5F13) * [php ](#editor%5F12) * [python ](#editor%5F14) * [ruby ](#editor%5F18) php javascript python go csharp java ruby ``` (); if (!string.IsNullOrEmpty(dataId)) parts.Add(\[\[\[ \`\`\`php (); if (!dataId.isEmpty()) parts.add("id:" + dataId); if (!xRequestId.isEmpty()) parts.add("request-id:" + xRequestId); parts.add("ts:" + ts); String manifest = String.join(";", parts) + ";"; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF\_8), "HmacSHA256")); byte\[\] bytes = mac.doFinal(manifest.getBytes(StandardCharsets.UTF\_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) sb.append(String.format("%02x", b & 0xff)); String computed = sb.toString(); if (!MessageDigest.isEqual( computed.getBytes(StandardCharsets.UTF\_8), hash.getBytes(StandardCharsets.UTF\_8))) { response.setStatus(401); return; } response.setStatus(200); \`\`\` \`\`\`ruby require 'openssl' x\_signature = request.headers\['x-signature'\] || '' x\_request\_id = request.headers\['x-request-id'\] || '' data\_id = (params\['data.id'\] || '').downcase ts = hash\_value = nil x\_signature.split(',').each do |part| key, value = part.split('=', 2) next unless key && value ts = value.strip if key.strip == 'ts' hash\_value = value.strip if key.strip == 'v1' end parts = \[\] parts << "id:#{data\_id}" unless data\_id.empty? parts << "request-id:#{x\_request\_id}" unless x\_request\_id.empty? parts << "ts:#{ts}" manifest = "#{parts.join(';')};" computed = OpenSSL::HMAC.hexdigest('SHA256', secret, manifest) unless OpenSSL.fixed\_length\_secure\_compare(computed, hash\_value) head :unauthorized return end head :ok \`\`\` \]\]\]quot;id:{dataId}"); if (!string.IsNullOrEmpty(xRequestId)) parts.Add(\[\[\[ \`\`\`php (); if (!dataId.isEmpty()) parts.add("id:" + dataId); if (!xRequestId.isEmpty()) parts.add("request-id:" + xRequestId); parts.add("ts:" + ts); String manifest = String.join(";", parts) + ";"; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF\_8), "HmacSHA256")); byte\[\] bytes = mac.doFinal(manifest.getBytes(StandardCharsets.UTF\_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) sb.append(String.format("%02x", b & 0xff)); String computed = sb.toString(); if (!MessageDigest.isEqual( computed.getBytes(StandardCharsets.UTF\_8), hash.getBytes(StandardCharsets.UTF\_8))) { response.setStatus(401); return; } response.setStatus(200); \`\`\` \`\`\`ruby require 'openssl' x\_signature = request.headers\['x-signature'\] || '' x\_request\_id = request.headers\['x-request-id'\] || '' data\_id = (params\['data.id'\] || '').downcase ts = hash\_value = nil x\_signature.split(',').each do |part| key, value = part.split('=', 2) next unless key && value ts = value.strip if key.strip == 'ts' hash\_value = value.strip if key.strip == 'v1' end parts = \[\] parts << "id:#{data\_id}" unless data\_id.empty? parts << "request-id:#{x\_request\_id}" unless x\_request\_id.empty? parts << "ts:#{ts}" manifest = "#{parts.join(';')};" computed = OpenSSL::HMAC.hexdigest('SHA256', secret, manifest) unless OpenSSL.fixed\_length\_secure\_compare(computed, hash\_value) head :unauthorized return end head :ok \`\`\` \]\]\]quot;request-id:{xRequestId}"); parts.Add(\[\[\[ \`\`\`php (); if (!dataId.isEmpty()) parts.add("id:" + dataId); if (!xRequestId.isEmpty()) parts.add("request-id:" + xRequestId); parts.add("ts:" + ts); String manifest = String.join(";", parts) + ";"; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF\_8), "HmacSHA256")); byte\[\] bytes = mac.doFinal(manifest.getBytes(StandardCharsets.UTF\_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) sb.append(String.format("%02x", b & 0xff)); String computed = sb.toString(); if (!MessageDigest.isEqual( computed.getBytes(StandardCharsets.UTF\_8), hash.getBytes(StandardCharsets.UTF\_8))) { response.setStatus(401); return; } response.setStatus(200); \`\`\` \`\`\`ruby require 'openssl' x\_signature = request.headers\['x-signature'\] || '' x\_request\_id = request.headers\['x-request-id'\] || '' data\_id = (params\['data.id'\] || '').downcase ts = hash\_value = nil x\_signature.split(',').each do |part| key, value = part.split('=', 2) next unless key && value ts = value.strip if key.strip == 'ts' hash\_value = value.strip if key.strip == 'v1' end parts = \[\] parts << "id:#{data\_id}" unless data\_id.empty? parts << "request-id:#{x\_request\_id}" unless x\_request\_id.empty? parts << "ts:#{ts}" manifest = "#{parts.join(';')};" computed = OpenSSL::HMAC.hexdigest('SHA256', secret, manifest) unless OpenSSL.fixed\_length\_secure\_compare(computed, hash\_value) head :unauthorized return end head :ok \`\`\` \]\]\]quot;ts:{ts}"); var manifest = string.Join(";", parts) + ";"; using var hmacSha = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var computed = BitConverter .ToString(hmacSha.ComputeHash(Encoding.UTF8.GetBytes(manifest))) .Replace("-", "").ToLowerInvariant(); if (!CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(computed), Encoding.UTF8.GetBytes(hash))) { return Unauthorized(); } return Ok(); Copiar ``` import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; String xSignature = request.getHeader("x-signature") != null ? request.getHeader("x-signature") : ""; String xRequestId = request.getHeader("x-request-id") != null ? request.getHeader("x-request-id") : ""; String dataId = request.getParameter("data.id") != null ? request.getParameter("data.id").toLowerCase() : ""; String ts = null, hash = null; for (String part : xSignature.split(",")) { String[] kv = part.split("=", 2); if (kv.length != 2) continue; String key = kv[0].trim(); String val = kv[1].trim(); if ("ts".equals(key)) ts = val; if ("v1".equals(key)) hash = val; } List parts = new ArrayList<>(); if (!dataId.isEmpty()) parts.add("id:" + dataId); if (!xRequestId.isEmpty()) parts.add("request-id:" + xRequestId); parts.add("ts:" + ts); String manifest = String.join(";", parts) + ";"; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] bytes = mac.doFinal(manifest.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) sb.append(String.format("%02x", b & 0xff)); String computed = sb.toString(); if (!MessageDigest.isEqual( computed.getBytes(StandardCharsets.UTF_8), hash.getBytes(StandardCharsets.UTF_8))) { response.setStatus(401); return; } response.setStatus(200); ``` Copiar ``` require 'openssl' x_signature = request.headers['x-signature'] || '' x_request_id = request.headers['x-request-id'] || '' data_id = (params['data.id'] || '').downcase ts = hash_value = nil x_signature.split(',').each do |part| key, value = part.split('=', 2) next unless key && value ts = value.strip if key.strip == 'ts' hash_value = value.strip if key.strip == 'v1' end parts = [] parts << "id:#{data_id}" unless data_id.empty? parts << "request-id:#{x_request_id}" unless x_request_id.empty? parts << "ts:#{ts}" manifest = "#{parts.join(';')};" computed = OpenSSL::HMAC.hexdigest('SHA256', secret, manifest) unless OpenSSL.fixed_length_secure_compare(computed, hash_value) head :unauthorized return end head :ok ``` Copiar ::: :::: ## Actions needed after receiving the notification When you receive a notification on your platform, Mercado Pago expects a response to validate that the receipt was correct. To do so, return an \`HTTP STATUS 200\` or \`201\` within 22 seconds of receipt. We recommend that you first respond with a \`200\` or \`201\`, and then process the notification on the server, to avoid duplicate notifications. If this response is not sent, the system will make new delivery attempts every 15 minutes. After the first failures, the interval progressively increases, but deliveries continue until the notification is confirmed. After confirming receipt, process the event asynchronously: - If \`data.new\_card\_id\` is present in the notification, update the reference in your database and use that identifier for all future charges for that customer. Using an outdated \`card\_id\` after the update will result in a high probability of rejection. - If you need the full details of the new card, query the API by sending a request to :TagComponent{tag="GET" text="/v1/customers/{id}/cards" href="/developers/en/reference/online-payments/automatic-payments/cards/get-customer-cards/get"}.