Mercado Pago: cobrando no gateway mais usado do Brasil¶
Pix, cartão, boleto e presencial, com a superfície inteira já gerada da especificação oficial do provedor.
Instalando e conectando¶
from tempest_fastapi_sdk import HTTPClient
from tempest_fastapi_sdk.integrations.payment.mercado_pago import (
DEFAULT_BASE_URL,
MercadoPagoClient,
)
http: HTTPClient = HTTPClient(
base_url=DEFAULT_BASE_URL,
default_headers={"Authorization": "Bearer <seu access token>"},
)
client: MercadoPagoClient = MercadoPagoClient(http)
Ou pelo mixin de settings, que já resolve o prefixo:
from tempest_fastapi_sdk import HTTPClient, MercadoPagoSettings
from tempest_fastapi_sdk.integrations.payment.mercado_pago import MercadoPagoClient
def build_client(settings: MercadoPagoSettings) -> MercadoPagoClient:
"""Build the client from configuration.
Args:
settings (MercadoPagoSettings): The loaded settings.
Returns:
MercadoPagoClient: The configured client.
"""
return MercadoPagoClient(HTTPClient(**settings.mercado_pago_kwargs()))
Não existe host de sandbox
Medido na especificação pinada: servers tem uma entrada,
https://api.mercadopago.com. O que separa uma cobrança de teste de uma
real é qual token você está segurando, não qual host você chama.
É o oposto do OpenPix, onde o ambiente troca o domínio. Aqui um token de produção apontado para essa mesma URL move dinheiro de verdade, e não há configuração que o impeça.
Dinheiro é em reais, não em centavos¶
from tempest_fastapi_sdk.integrations.payment.mercado_pago import (
from_cents,
to_cents,
)
def exemplo() -> tuple[int, str]:
"""Convert both ways.
Returns:
tuple[int, str]: Cents parsed from reais, and reais rendered back.
"""
cents: int = to_cents(19.9)
return cents, str(from_cents(cents))
A armadilha de fator 100
Mercado Pago tipa dinheiro como number e o declara em reais — 39
propriedades monetárias na especificação, entre elas
transaction_amount, unit_price e Refund.amount.
O OpenPix também usa number, mas declara em centavos. Mesmo tipo
errado, unidade diferente. Trocar um pelo outro cobra R$ 1.990,00 por um
item de R$ 19,90 — e o erro só aparece no extrato do cliente.
Por isso to_cents recusa fração de centavo em vez de arredondar:
arredondar esconderia a divergência atrás de um número plausível.
Checkout Pro: a preferência¶
O comprador é redirecionado para uma tela do Mercado Pago:
from tempest_fastapi_sdk.integrations.payment.mercado_pago import (
MercadoPagoClient,
PreferenceItem,
PreferenceRequest,
)
async def criar_preferencia(client: MercadoPagoClient) -> str | None:
"""Create a Checkout Pro preference and return where to send the buyer.
Args:
client (MercadoPagoClient): The configured client.
Returns:
str | None: The ``init_point`` URL, when the provider returned one.
"""
preference = await client.create_preference(
body=PreferenceRequest(
items=[
PreferenceItem(
title="Pedido 1042",
quantity=1,
unit_price=19.9,
)
],
external_reference="pedido-1042",
)
)
return preference.init_point
Checkout Transparente: cobrando sem redirect¶
Pix e boleto são inteiramente server-side — nenhum redirecionamento:
import uuid
from tempest_fastapi_sdk.integrations.payment.mercado_pago import (
MercadoPagoClient,
PaymentPayer,
PaymentRequest,
)
async def cobrar_pix(client: MercadoPagoClient) -> str | None:
"""Charge over Pix without sending the buyer anywhere.
Args:
client (MercadoPagoClient): The configured client.
Returns:
str | None: The payment URL for the offline method, when present.
"""
payment = await client.create_payment(
body=PaymentRequest(
transaction_amount=19.9,
payment_method_id="pix",
payer=PaymentPayer(email="comprador@example.com"),
external_reference="pedido-1042",
),
x_idempotency_key=uuid.uuid4(),
)
details = payment.transaction_details
return details.external_resource_url if details is not None else None
x_idempotency_key é argumento da chamada
Uma chave por tentativa. Se a rede cair depois de o Mercado Pago receber a requisição, repetir com a mesma chave devolve o pagamento original em vez de criar um segundo.
Ela é argumento — e não header default do HTTPClient — justamente por
isso: um header default mandaria a mesma chave em toda cobrança, e a
segunda venda seria deduplicada em cima da primeira.
Cartão tem uma parte obrigatória no cliente
create_payment recebe o cartão como token, nunca como número. Quem
emite esse token é POST /v1/card_tokens, que a especificação declara
com security: publicKey — chave pública, feita para rodar no
browser ou no app.
Chamar essa rota do servidor é tecnicamente possível e coloca o seu serviço no escopo do PCI DSS. Use o SDK JavaScript ou mobile do Mercado Pago para obter o token, e mande só o token para o seu backend.
O QR do Pix, e o motivo de ele desaparecer¶
O create_payment gerado devolve o Payment que a especificação declara — e
a especificação não declara point_of_interaction, que é exatamente onde
o copia-e-cola e a imagem do QR chegam. Como o BaseSchema do SDK é
extra="ignore", o objeto é descartado na validação: o QR chega no corpo
HTTP e some no modelo, sem erro e sem log.
Por isso existe create_pix_payment, que faz a mesma requisição e
devolve um modelo que tem onde guardar o QR:
import uuid
from tempest_fastapi_sdk import HTTPClient
from tempest_fastapi_sdk.integrations.payment.mercado_pago import (
DEFAULT_BASE_URL,
PixPayment,
create_pix_payment,
)
async def cobrar_pix_com_qr(access_token: str) -> PixPayment:
"""Charge over Pix and keep the QR the generated model drops.
Args:
access_token (str): The Mercado Pago access token.
Returns:
PixPayment: The pending payment, carrying ``qr_code`` and
``qr_code_base64``.
"""
http: HTTPClient = HTTPClient(
base_url=DEFAULT_BASE_URL,
default_headers={"Authorization": f"Bearer {access_token}"},
)
return await create_pix_payment(
http,
body={
"transaction_amount": 19.9,
"payment_method_id": "pix",
"payer": {"email": "comprador@example.com"},
"external_reference": "pedido-1042",
},
idempotency_key=uuid.uuid4(),
)
O que o PixPayment devolvido carrega:
payment.qr_code "00020126580014br.gov.bcb.pix0136..." o copia-e-cola
payment.qr_code_base64 "iVBORw0KGgoAAAANSUhEUg..." PNG, para <img src="data:...">
payment.ticket_url "https://www.mercadopago.com.br/..." página que já desenha o QR
payment.status "pending" até o pagador pagar
As três são propriedades None-safe: pagamento de cartão, ou Pix já pago,
devolve None em vez de estourar — é a forma como o provedor responde
depois da liquidação.
Já tem o corpo em mãos? Use parse_pix_payment
Um webhook manda você buscar o pagamento; se você já chamou pelo cliente
gerado e guardou o JSON cru, parse_pix_payment(payload) monta o mesmo
PixPayment sem repetir a requisição. Para reler pelo id existe
get_pix_payment(http, payment_id).
De onde vêm esses nomes de campo
Não da especificação, que os omite: do SDK Node oficial do Mercado Pago
(mercadopago/sdk-nodejs, src/clients/payment/commonTypes.ts, commit
c2d3c6ae), onde PointOfInteraction e TransactionData estão
modelados. O conjunto de campos é fixado por teste, então uma mudança lá
aparece aqui como falha e não como valor que sumiu.
PixPayment é uma vista, não um substituto
Para tudo que a especificação declara, use o Payment gerado. O
PixPayment carrega só o que um fluxo Pix lê — id, status, valor,
expiração — mais o objeto do QR. Ele não importa os schemas gerados, de
propósito: ler um QR não paga os 0,76 s que construir os 324 modelos
custa.
A rota alternativa: Orders API¶
A especificação modela o QR num lugar só, OrderTransactionPayment, da API
de Orders — lá qr_code, qr_code_base64, digitable_line e e2e_id são
declarados de verdade:
from tempest_fastapi_sdk.integrations.payment.mercado_pago import MercadoPagoClient
async def qr_do_pedido(client: MercadoPagoClient, order_id: str) -> object:
"""Read the Pix QR data of an order.
Args:
client (MercadoPagoClient): The configured client.
order_id (str): The order identifier.
Returns:
object: The order, whose transactions carry ``qr_code`` and
``qr_code_base64``.
"""
return await client.get_order(order_id)
Use Orders quando a integração é nova — é a recomendação do próprio
provedor, e o caminho tipado direto pela especificação. Use
create_pix_payment quando a cobrança já roda em /v1/payments e trocar de
API não está em discussão.
Verificando o webhook¶
from tempest_fastapi_sdk.integrations.payment.mercado_pago import verify_signature
def notificacao_e_autentica(
secret: str, signature: str, data_id: str, request_id: str
) -> bool:
"""Check that a notification really came from Mercado Pago.
Args:
secret (str): The webhook secret from the dashboard.
signature (str): The ``x-signature`` header.
data_id (str): The ``data.id`` query parameter.
request_id (str): The ``x-request-id`` header.
Returns:
bool: Whether the signature matches.
"""
return verify_signature(
secret=secret,
signature_header=signature,
data_id=data_id,
request_id=request_id,
tolerance_seconds=300.0,
)
O algoritmo é portado do validador do próprio Mercado Pago
(mercadopago/sdk-nodejs, src/utils/webhook/index.ts, commit 99857f33),
que é o módulo para o qual a documentação deles aponta o integrador. A
especificação vendorizada não descreve nada disso —
grep -c "x-signature" vendor/mercadopago-openapi.yaml devolve 0.
O manifesto assinado omite par ausente. Não é template fixo:
tudo presente id:<data.id>;request-id:<x-request-id>;ts:<ts>;
sem data.id request-id:<x-request-id>;ts:<ts>;
sem os dois ts:<ts>;
Isto era um defeito até a v0.250.0
Até então este módulo renderizava um template fixo, então uma entrega sem
data.id assinava id:;request-id:...;ts:...; — e nenhuma entrega desse
tipo verificava. Se você tratava a rejeição como "notificação inválida",
estava descartando notificação legítima.
build_manifest está exportado para você conferir o que seria assinado:
from tempest_fastapi_sdk.integrations.payment.mercado_pago import build_manifest
def manifesto_da_entrega(data_id: str, request_id: str, ts: str) -> str:
"""Show the exact string the signature covers.
Args:
data_id (str): The ``data.id`` query parameter, empty when absent.
request_id (str): The ``x-request-id`` header, empty when absent.
ts (str): The ``ts`` component of ``x-signature``.
Returns:
str: The manifest, with absent pairs left out.
"""
return build_manifest(data_id=data_id, request_id=request_id, timestamp=ts)
Ligue a janela de tolerância
Sem tolerance_seconds, uma entrega capturada do fio verifica para
sempre: a assinatura cobre um timestamp que ninguém confere. O upstream
deixa a janela opcional e nós também, mas 300.0 é o que faz o ts do
manifesto trabalhar. A unidade do ts é lida pela magnitude — os próprios
artefatos do provedor discordam entre segundos e milissegundos, e a
issue #458 deles
foi exatamente essa confusão.
Migração para v2 não precisa de release
O header pode carregar mais de um hash (ts=..,v1=..,v2=..). O verificador
usa a primeira versão que você aceitar, então
versions=("v2", "v1") adota a nova antes de este pacote mudar. O default
é ("v1",) — falhar fechado é o comportamento certo para versão que o
provedor ainda não mandou.
Ainda não foi medido contra uma entrega real
Portado da implementação do provedor não é o mesmo que verificado contra
notificação que o provedor mandou. O que está medido: os manifestos, byte
a byte, contra as regras que o upstream codifica; e os digests, contra
vetores calculados com openssl dgst -sha256 -hmac, que é outra
implementação de HMAC que não a do Python.
O que continua sem medição: se as entregas reais seguem o SDK deles. Passe
uma notificação real por verify_signature antes de isso guardar
dinheiro, e abra uma issue se ela for rejeitada.
Notificação de QR Code não é assinada
O upstream diz isso explicitamente: essas entregas não carregam assinatura e vão falhar sempre. Não passe QR Code por aqui — proteja essa rota de outra forma.
Recapitulando¶
- Um único host: o que separa teste de produção é o token.
- Dinheiro em reais; converta na fronteira com
to_cents/from_cents. - Pix e boleto são server-side; cartão exige tokenização no cliente.
x_idempotency_keyé argumento por chamada, nunca header default.- O
Paymentgerado descarta o QR do Pix em silêncio; usecreate_pix_payment/parse_pix_payment, ou a API de Orders. - A verificação de webhook é portada do validador do provedor, com o
manifesto omitindo par ausente e digests conferidos contra
openssl; falta só uma entrega real para confirmar. Liguetolerance_seconds. - Notificação de QR Code não é assinada — não passe por
verify_signature.