Skip to content

Mercado Pago

Setup guide for the mercadopago payments provider. The shared API (initiate_payment, verify_payment, refund_payment, webhooks, hooks, saved methods) is documented in Payments.

The mercadopago provider supports one-time payments through Checkout Pro (initiate_payment, verify_payment, with cards, Pix, boleto, OXXO and the other methods your account offers), refunds (refund_payment), subscriptions (type: "subscription") with cancel, plan change (amount) and pause/resume, reading chargebacks, and webhooks, in the countries Mercado Pago serves (BRL, MXN, ARS, CLP, COP, PEN, UYU). GET /payments/providers reports dispute_read, pause, subscription_cancel, subscription_lifecycle, subscription_update and subscriptions.

Feature Status
One-time payments (Checkout Pro), verify, refunds Supported
Subscriptions (type: "subscription") Supported (a Mercado Pago preapproval plan id); first charge and renewals recorded
Cancel Supported, immediately only (at_period_end: True is NOT_SUPPORTED, 501)
update_subscription Supported: another plan’s amount with the same billing cycle and currency
Pause / resume Supported
get_dispute, chargeback webhooks Supported
submit_dispute_evidence Not supported (NOT_SUPPORTED, 501): answer chargebacks in the Mercado Pago dashboard
Billing portal Not offered by Mercado Pago (NOT_SUPPORTED, 501)
Saved cards, charge_off_session Not supported (NOT_SUPPORTED, 501): a saved Mercado Pago card needs its security code again for every charge, so nothing can be charged without the customer

It needs no extra install: it calls https://api.mercadopago.com directly over HTTP, following Mercado Pago’s developer documentation as of September 2026 (Checkout Pro, Subscriptions, Payments v1, Chargebacks, Webhooks).

Why plain HTTP (no Mercado Pago SDK). Mercado Pago’s Python package, mercadopago, needed Python >= 3.10 (since version 3.5.0) when Kirak still supported 3.9, and the few endpoints Kirak calls are simple REST.

1. Get credentials. In Your integrations, open (or create) the application and copy its Access Token (test or production credentials; test payments use Mercado Pago test users).

2. Set the webhook. In the application’s Webhooks settings:

  • set the URL to https://your-domain.com/payments/webhook/mercadopago (/payments/webhook/<instance> for another instance name), for test and production mode;
  • select the events Payments, Plans and subscriptions (subscription preapproval and authorized payment) and Chargebacks;
  • copy the secret signature Mercado Pago generates.

Use Webhooks, not the older IPN notifications: IPN deliveries are not signed, so Kirak rejects them (400).

3. Configure the instance and secrets. In kirak.json:

"mercadopago": {
"type": "mercadopago",
"environment": "sandbox",
"success_url": "https://your-domain.com/payment/success"
}

environment is sandbox (the default) or production; in sandbox the checkout link is Mercado Pago’s sandbox_init_point when it returns one. success_url is sent as the buyer’s return URL (back_urls, with auto_return for approved payments) when absolute (http(s)://); subscriptions need an absolute one (Mercado Pago requires a back_url), else CONFIGURATION_ERROR (500) before anything is saved. In the environment:

Terminal window
KIRAK_PAYMENT_MERCADOPAGO_ACCESS_TOKEN=TEST-...
KIRAK_PAYMENT_MERCADOPAGO_WEBHOOK_SECRET=<the secret signature>

For an instance with another name the variables are KIRAK_PAYMENT_<INSTANCE>_ACCESS_TOKEN and KIRAK_PAYMENT_<INSTANCE>_WEBHOOK_SECRET (see docs/reference/configuration.md). Both are required.

4. Use it:

# One-time payment -- send the buyer to data["url"] (Checkout Pro)
result = await kirak.payments.initiate_payment({
"provider": "mercadopago",
"user_id": "42", "amount": 4990, "currency": "BRL", # centavos: R$ 49.90
"name": "Pro Plan", "type": "one_time",
"payment_email": "buyer@example.com", # optional, prefills the payer
})
reference = result["data"]["reference"] # external_reference, kirak-<32 hex>
# On the return page (?payment_id=...&external_reference=...): read the payment now
await kirak.payments.verify_payment({
"provider": "mercadopago", "payment_id": reference,
"mercadopago_payment_id": payment_id, # optional, from the return URL
})
# Subscription -- subscription_plan_id is a Mercado Pago preapproval plan id,
# created in Mercado Pago first, in the same currency
result = await kirak.payments.initiate_payment({
"provider": "mercadopago", "user_id": "42", "amount": 4990, "currency": "BRL",
"type": "subscription", "subscription_plan_id": "2c938084...",
"payment_email": "buyer@example.com", # required for a subscription
})
subscription_id = result["data"]["subscription_id"] # the preapproval id
await kirak.payments.pause_subscription({"provider": "mercadopago", "subscription_id": subscription_id})
await kirak.payments.update_subscription({
"provider": "mercadopago", "subscription_id": subscription_id, "new_plan_id": "2c938084...",
})
await kirak.payments.cancel_subscription({"provider": "mercadopago", "subscription_id": subscription_id})

Payments. initiate_payment stores a random external_reference (kirak-<32 hex>) on the PENDING row before creating the Checkout Pro preference (one item: name, quantity, the unit amount; metadata naming the transaction and instance) and returns its link as url. The reference is never derived from the idempotency_key, which another Kirak database on the same account could repeat. A 4xx answer marks the row FAILED; a timeout, 5xx or 429 leaves it PENDING (the preference may exist).

Mercado Pago’s notifications carry only a type and an id, so every change comes from re-reading the object. A payment notification re-reads GET /v1/payments/{id} and completes the row once (payment_completed, with amount and currency) only when the payment is approved, carries the row’s external_reference, the row’s currency, a transaction_amount of at least amount x quantity, and metadata that, where present, names this transaction and instance; a payment that does not match writes nothing. A buyer can try again on the same checkout after a rejection, so a rejected or cancelled payment keeps the row PENDING (noted in ipn_dump.last_failed_attempt, no event); Pix, boleto and OXXO payments stay pending for hours or days and complete on a later notification. A second approved payment for a completed checkout is logged as a possible double payment. A payment Mercado Pago does not return yet, or a read that times out, fails the webhook (500) so Mercado Pago retries (every 15 minutes until it gets a 200 or 201).

verify_payment (payment_id = the reference; optional mercadopago_payment_id from the return URL, else a search by external_reference) sets PROCESSING for a confirmed approved payment and never fails the row; it returns outcome: "not_confirmed" for a payment that does not match and "not_found" when there is none yet.

Refunds. refund_payment (payment_id = the reference, or a renewal’s Mercado Pago payment id; optional amount in minor units of the payment’s currency) calls POST /v1/payments/{id}/refunds. Its X-Idempotency-Key is derived from the caller’s idempotency_key and the transaction’s random reference, so retrying with the same key after a timeout never refunds twice (and never matches a refund of another Kirak database on the account); without an idempotency_key it is random. An approved refund is recorded at once, keyed by its refund id with a running total (PARTIALLY_REFUNDED, then REFUNDED, which never moves back); one still in_process is recorded when a later payment notification shows it approved in the payment’s refunds, reporting refund_completed. Refunds made in the Mercado Pago dashboard are recorded the same way, a renewal’s on its subscription_renewal row.

Subscriptions. A Mercado Pago preapproval created with a plan id needs a card token (no hosted page), so initiate_payment with type: "subscription" reads the preapproval plan (subscription_plan_id) and creates a preapproval on its terms instead: its frequency, amount and currency, status: "pending", the row’s external_reference, the payment_email (required) and success_url as back_url. The plan must be active and in the requested currency (else PLAN_NOT_ACTIVE or PLAN_CURRENCY_MISMATCH, 400, nothing saved); the row’s amount is the plan’s and quantity 1. The preapproval is Kirak’s own, so it is bound at once: a subscriptions row (PENDING) and the row’s subscription_id; the buyer authorizes it on the returned url.

  • subscription_authorized_payment (a subscription charge) re-reads GET /authorized_payments/{id} and its payment. The first approved charge completes the subscription’s row (payment_completed) and sets the subscription ACTIVE; each later one saves a subscription_renewal row keyed by the Mercado Pago payment id (subscription_renewed; PAST_DUE back to ACTIVE), once even when two deliveries of the charge race. A charge Mercado Pago is retrying (recycling) sets PAST_DUE (subscription_past_due). The subscription’s own payment notifications are acknowledged: its charges are recorded only this way.
  • subscription_preapproval re-reads GET /preapproval/{id} and stores its status (pending -> PENDING, authorized -> ACTIVE, paused -> PAUSED, cancelled -> CANCELLED, which is final), amount and next payment date (expires_on), reporting subscription_updated or subscription_cancelled only when the status changed.

cancel_subscription (immediately; at_period_end is NOT_SUPPORTED), pause_subscription and resume_subscription set the preapproval’s status; update_subscription (new_plan_id, optional quantity) moves it to another preapproval plan’s amount x quantity from the next charge, only when that plan has the same frequency and currency (PLAN_INTERVAL_MISMATCH or PLAN_CURRENCY_MISMATCH, 400), and stores the new plan_id on the subscription. The subscription_preapproval notification records the new amount.

Disputes (read-only). get_dispute reads GET /v1/chargebacks/{id}. A topic_chargebacks_wh notification re-reads the chargeback and reports it for a payment that completed a row of the instance (dispute_created when the notification’s action is a creation, else dispute_updated), with its amount; the row is not changed. submit_dispute_evidence is NOT_SUPPORTED: send documentation in the Mercado Pago dashboard.

Webhook security. The x-signature header (ts=...,v1=...) is checked before anything else: v1 must be the hex HMAC-SHA256, with the secret signature, of id:<data.id>;request-id:<x-request-id>;ts:<ts>;, where data.id comes from the notification URL’s query string (lower-cased) and a part whose value is absent is left out, as Mercado Pago signs it; the body’s data.id must match it. A missing header is MISSING_WEBHOOK_SIGNATURE, a wrong or malformed one INVALID_WEBHOOK_SIGNATURE (400). Kirak sets no time window on ts: Mercado Pago retries every 15 minutes and does not document whether a retry is signed again. Deliveries are deduplicated on the sha256 of the raw body.

Currencies. Mercado Pago amounts are major-unit decimals, sent as JSON numbers built from the exact decimal (never rounded through a float). COP must be a whole number of pesos at Mercado Pago, so a COP amount with centavos is rejected with AMOUNT_NOT_REPRESENTABLE (400) before anything is saved; CLP has no minor unit already.

Known limits.

  • Saved cards and off-session charges are not supported (the security code is needed for every charge with a saved card).
  • A rejected or expired payment (for example an unpaid boleto) keeps the row PENDING; Kirak never fails a checkout.
  • IPN notifications and unsigned QR-code notifications are rejected (400).
  • A subscription can change only its amount, not its billing cycle.
  • Disputes are read-only.

after_webhook receives these provider-neutral event names (see Webhook Hooks):

Mercado Pago reports payment_completed (a payment notification whose re-read payment is approved and matches the row, and a subscription’s first approved charge from subscription_authorized_payment, with event_type payment.completed; a rejected attempt reports none, with event_type payment.failed_attempt, and an unmatched payment none, with payment.unconfirmed), refund_completed (a payment notification showing a newly approved refund, event_type payment.refunded), subscription_renewed (a later approved subscription charge, subscription.renewed), subscription_past_due (a charge Mercado Pago is retrying, subscription.past_due), subscription_updated and subscription_cancelled (subscription_preapproval, when the status changed), and dispute_created/dispute_updated (topic_chargebacks_wh).