# MD for: https://www.mercadopago.com.mx/developers/pt/docs/automatic-payments-orders/configure-notifications/profiles.md \# Configure Payment Profile notifications The Payment Profile Webhooks deliver real-time notifications to your application whenever a profile linked to your integration changes. Instead of polling the API, you receive updates as they happen via \`HTTP POST\`. This way, you can detect when a profile reaches the \`ready\` status to enable your recurring charge flow, take the necessary actions when a payment method linked to a profile is deactivated or cancelled, and keep your system always in sync with the actual profile state. flowchart TD A[Profile change] --> B{Check eligibility} B -->|Not eligible| C[Event discarded] B -->|Eligible| D["POST {your-url} with JSON payload"] D --> E[Your app responds HTTP 200] E --> F[Process event asynchronously] \## Configure Webhooks Follow the steps below to configure Payment Profile notifications. 1\. Go to \[Your integrations\](https://www.mercadopago.com/developers/panel/app) and select the application for which you want to enable notifications. > WARNING > > The application configured for notifications must match the one responsible for creating the profile. !\[configure notifications\](https://www.mercadopago.com.mx/images/api-orders/not1-app-es-v1.png) 2\. In the left menu, select \*\*Webhooks > Configure notifications\*\*. !\[configure notifications\](https://www.mercadopago.com.mx/images/api-orders/not2-configure-es-v1.png) 3\. Set 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"}. 4\. Select the \*\*Payment Profile\*\* event (\`payment\_profile\`) to receive notifications for profile changes. 5\. Finally, click \*\*Save configuration\*\*. This will generate an exclusive secret key for the application, which will be used to validate the authenticity of received notifications, ensuring they were sent by Mercado Pago. 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 your Webhooks, click \*\*Simulate notification\*\*. 2\. On the simulation screen, select the URL to test. 3\. Choose the \*\*Payment Profile\*\* event type and enter the \*\*profile ID\*\* to be sent in the notification body (\`Data ID\`). 4\. Finally, click \*\*Send test\*\* to verify the request, the server response, and the event description. ## Validate the notification origin Validating the origin of a notification is essential to ensure the security and authenticity of the received information. This process helps prevent fraud and ensures that only legitimate notifications are processed. Mercado Pago will send your server a notification similar to the example below for a \`payment\_profile\` topic alert. \`\`\`json { "id": "abc123def456", "type": "payment\_profile", "action": "payment\_profile.updated", "version": 3, "date\_created": "2026-01-10T10:00:00.000+0000", "live\_mode": true, "collector\_id": "123456789", "application\_id": "1234567890", "data": { "date\_last\_updated": "2026-03-27T14:30:00.000+0000", "status": "ready", "payment\_methods": \[ { "unique\_id": "pm\_001", "type": "credit\_card", "status": "ready", "default\_method": true, "card\_id": 111222333 } \], "previous\_attributes": { "status": "pending", "payment\_method": { "unique\_id": "pm\_001", "type": "credit\_card", "status": "rejected", "default\_method": false, "card\_id": 111222333 } } } } \`\`\` | Field | Type | Presence | Description | |---|---|---|---| | \`id\` | string | Always | Payment profile identifier returned by the API at creation time. | | \`type\` | string | Always | Always \`payment\_profile\`. | | \`action\` | string | Always | Always \`payment\_profile.updated\`, indicating that a relevant profile attribute was modified — status, payment method, or both. | | \`version\` | integer | Always | Incremental notification counter for this profile. Use it to order out-of-sequence events. | | \`date\_created\` | string | Always | Profile creation date (ISO 8601). | | \`live\_mode\` | boolean | Always | \`true\` in production, \`false\` in test mode. | | \`collector\_id\` | string | Always | ID of the merchant associated with the profile. | | \`application\_id\` | string | Always | ID of the integrating application that originated the profile. | | \`data\` | object | Always | Details of the change that occurred. | | \`data.date\_last\_updated\` | string | Always | Exact date and time of the profile change, in ISO 8601 format. | | \`data.status\` | string | Present \*\*only if the status was modified\*\* | New profile status. | | \`data.payment\_methods\` | array | Present \*\*only if a payment method changed\*\*. | Current information about the affected payment methods. Includes the unique identifier (\`unique\_id\`), payment method type (\`credit\_card\`, \`debit\_card\`, \`prepaid\_card\`, \`bank\_transfer\`), current \`status\`, and whether it is set as the default payment method for charges (\`default\_method\`). If the payment method is a card, its ID (\`card\_id\`) will also be present. | | \`data.previous\_attributes\` | object | Absent in new payment method additions. | Previous information about the attributes that changed in the payment profile. | | \`previous\_attributes.status\` | string | Conditional | Previous payment profile status. | | \`previous\_attributes.payment\_method\` | object | Conditional | Previous state of the affected payment method. | For the possible values of \`data.status\` and \`data.payment\_methods\[\*\].status\` and the recommended actions for each, see \[Profile and payment method statuses\](https://www.mercadopago.com.mx/developers/en/docs/automatic-payments-orders/profile-payment-methods-status). From the received notification, you can validate the authenticity of its origin through the secret key. This key will be sent in the \`x-signature\` \_header\_, 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.mx/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.mx/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.mx/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.mx/developers/panel/app), select the integrated application, click \*\*Webhooks > Configure notification\*\* and reveal the generated secret key. !\[cofigure notifications\](https://www.mercadopago.com.mx/images/api-orders/not6-signature-es-v1.png) 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 your integration depends on the full profile state, query the API by sending a request to :TagComponent{tag="GET" text="/v1/customers/{customerId}/payment-profiles/{id}" href="/developers/en/reference/automatic-payments/payment-profiles/retrieve-profile/get"} with the \`id\` field received in the notification. Never infer the complete profile state exclusively from the notification data, as it only delivers \*\*the fields that changed\*\*.