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:
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 nowawait 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 currencyresult = 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-readsGET /authorized_payments/{id}and its payment. The first approved charge completes the subscription’s row (payment_completed) and sets the subscriptionACTIVE; each later one saves asubscription_renewalrow keyed by the Mercado Pago payment id (subscription_renewed;PAST_DUEback toACTIVE), once even when two deliveries of the charge race. A charge Mercado Pago is retrying (recycling) setsPAST_DUE(subscription_past_due). The subscription’s ownpaymentnotifications are acknowledged: its charges are recorded only this way.subscription_preapprovalre-readsGET /preapproval/{id}and stores its status (pending->PENDING,authorized->ACTIVE,paused->PAUSED,cancelled->CANCELLED, which is final), amount and next payment date (expires_on), reportingsubscription_updatedorsubscription_cancelledonly 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.
Webhook events
Section titled “Webhook events”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).