Cartão + 3DS Cielo
Pagamento com cartão de crédito e autenticação 3D Secure via Cielo/Braspag BP.MPI.
Cartão + 3DS Cielo
Fluxo com autenticação 3D Secure (3DS 2.x) via BP.MPI da Braspag. Oferece liability shift: em caso de chargeback por não-reconhecimento, a responsabilidade é do emissor do cartão, não da sua loja.
Resumo
1. [mount] Carregar scripts 3DS + iniciar detecção de fraude
2. [submit] Fechar detecção de fraude
3. [submit] GET /api/3ds/token/ ← token Cielo (público, sem auth)
4. [submit] bpmpi_authenticate() ← SDK 3DS no browser do usuário
5. [submit] POST /api/checkout/ ← com threeDSecure preenchidoDiagrama de fluxo
Passo 1 — Scripts a carregar no mount
const IS_PRODUCTION = process.env.NEXT_PUBLIC_ENV === 'production'
const BPMPI_URL = IS_PRODUCTION
? 'https://mpi.braspag.com.br/Scripts/BP.Mpi.3ds20.min.js'
: 'https://mpisandbox.braspag.com.br/Scripts/BP.Mpi.3ds20.min.js'
const CARDINAL_URL = 'https://songbird.cardinalcommerce.com/edge/v1/songbird.js'Carregue no mount, não no submit — o SDK precisa de tempo para inicializar.
Passo 2 — Obter token 3DS
GET {BASE_URL}/api/3ds/token/?provider=cieloEndpoint público — sem header Authorization.
async function getThreeDsToken() {
const r = await fetch(`${BASE_URL}/api/3ds/token/?provider=cielo`)
if (!r.ok) throw new Error(`Erro ao obter token 3DS: ${r.status}`)
const { access_token } = await r.json()
return access_token
}Resposta:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}Gere o token imediatamente antes de chamar bpmpi_authenticate(). TTL é ~1h — não armazene em estado persistente.
Passo 3 — Executar autenticação 3DS (BP.MPI)
O SDK roda no browser do usuário. Ele lê dados do cartão via inputs HTML com classes específicas e retorna o criptograma via callback.
function runThreeDsAuth({ accessToken, amount, installments, card, deviceIp }) {
return new Promise((resolve) => {
const timeout = setTimeout(
() => resolve({ ok: false, error: '3DS timeout — tente novamente' }),
90_000
)
let activated = false
const activate = () => {
if (!activated) { activated = true; window.bpmpi_authenticate?.() }
}
// Watchdog: algumas versões do SDK não disparam onReady
const watchdog = setTimeout(activate, 5_000)
// Tornar o iframe do challenge visível (nasce invisível sem isso)
const observer = new MutationObserver(() => {
const iframe = document.querySelector('iframe[name="bpmpi_frame"]')
if (!iframe || iframe.dataset.styled) return
iframe.dataset.styled = 'true'
Object.assign(iframe.style, {
position: 'fixed', top: '0', left: '0',
width: '100vw', height: '100vh',
zIndex: '99999', border: 'none',
})
})
observer.observe(document.body, { childList: true, subtree: true })
const done = (result) => {
clearTimeout(timeout)
clearTimeout(watchdog)
observer.disconnect()
resolve(result)
}
// Registrar callbacks ANTES de criar o form
window.bpmpi_config = () => ({
Environment: IS_PRODUCTION ? 'PRD' : 'SDB',
Debug: false,
onReady: () => { clearTimeout(watchdog); activate() },
onSuccess: (e) => done({
ok: true,
threeDSecure: {
Cavv: e.Cavv,
Xid: e.Xid,
Eci: e.Eci,
Version: e.Version,
ReferenceId: e.ReferenceId,
DataOnly: false,
},
}),
onFailure: () => done({ ok: false, error: 'Autenticação 3DS falhou' }),
onUnenrolled: () => done({ ok: false, error: 'Cartão não cadastrado no 3DS' }),
onDisabled: () => done({ ok: false, error: '3DS desabilitado para este cartão' }),
onUnsupportedBrand: () => done({ ok: false, error: 'Bandeira não suportada pelo 3DS' }),
onError: (e) => done({ ok: false, error: e?.ReturnMessage ?? 'Erro no SDK 3DS' }),
})
// Form oculto — o SDK BP.MPI lê os campos por classe CSS
document.getElementById('bpmpi-form')?.remove()
const form = document.createElement('form')
form.id = 'bpmpi-form'
form.style.display = 'none'
const amountCents = Math.round(amount * 100)
const expYear = String(card.expYear)
const fields = [
['bpmpi_auth', 'true'],
['bpmpi_accesstoken', accessToken],
['bpmpi_ordernumber', `ORDER-${Date.now()}`],
['bpmpi_currency', '986'], // BRL = 986
['bpmpi_totalamount', String(amountCents)],
['bpmpi_installments', String(installments)],
['bpmpi_paymentmethod', 'credit'],
['bpmpi_cardnumber', card.number.replace(/\D/g, '')],
['bpmpi_cardexpirationmonth', String(card.expMonth).padStart(2, '0')],
['bpmpi_cardexpirationyear', expYear.length === 2 ? `20${expYear}` : expYear],
['bpmpi_device_ipaddress', deviceIp || '127.0.0.1'],
['bpmpi_device_1_channel', 'Browser'],
]
fields.forEach(([cls, value]) => {
const input = document.createElement('input')
input.className = cls
input.value = value
form.appendChild(input)
})
document.body.appendChild(form)
})
}Passo 4 — Request do checkout com 3DS
O campo threeDSecure recebe exatamente o que o onSuccess retornou — chaves PascalCase, sem transformação.
{
"user": 42,
"paymentMethod": "threeDs",
"amount": "149.90",
"installments": 1,
"antifraud_profiling_attempt_reference": "550e8400-e29b-41d4-a716-446655440000",
"threeDSecure": {
"Cavv": "AAIBBYNoEwAAACcKhAJkdQAAAAA=",
"Xid": "5f3be6de-71cd-4b17-8e3f-000000000001",
"Eci": "05",
"Version": "2.2.0",
"ReferenceId": "a24a5d87-b1a1-4aef-a31b-52d1e47b6eb1",
"DataOnly": false
},
"card": { ... },
"customer": { ... },
"address": { ... },
"items": [ ... ],
"postbackUrl": "https://sua-loja.com/webhooks/cakto"
}Hard-fail: Se Cavv, Eci ou Version estiverem vazios, a API retorna HTTP 400. Nunca chame /api/checkout/ se o callback onSuccess não foi disparado.
Resposta — 3DS aprovado com liability shift
{
"id": "9a1b2c3d-ef45-6789-abcd-000000",
"status": "paid",
"amount": 149.9,
"paidAmount": 149.9,
"liquidAmount": 143.52,
"fee": 6.38,
"installments": 1,
"tid": "0505096727000195830101",
"return_code": "4",
"acquirerType": "cielo",
"currency": "BRL",
"threeDs": {
"eci": "05",
"cavv": "AAIBBYNoEwAAACcKhAJkdQAAAAA=",
"version": "2.2.0",
"liability_shift": true
},
"card": {
"lastDigits": "1111",
"holderName": "JOAO DA SILVA",
"brand": "visa",
"token": null
}
}ECI e liability shift
| ECI | Bandeira | Liability shift |
|---|---|---|
05 | Visa | ✅ Sim (emissor responsável) |
06 | Visa | ✅ Sim |
07 | Visa | ❌ Não |
02 | Mastercard | ✅ Sim |
01 | Mastercard | ✅ Sim |
00 | Mastercard | ❌ Não |
liability_shift: true na resposta confirma que o chargeback por não-reconhecimento é responsabilidade do emissor.
Checklist de implementação
- Scripts BPMPI e Cardinal carregados no mount (não no submit)
- Token gerado imediatamente antes de
bpmpi_authenticate() -
bpmpi_configregistrado antes de criar o form -
bpmpi_formremovido e recriado a cada tentativa -
threeDSecurecom chaves PascalCase exatamente como retornado peloonSuccess -
paymentMethod: "threeDs"(com capital D e S) - Não chamar
/api/checkout/seonSuccessnão disparou - Tratamento de todos os callbacks:
onFailure,onUnenrolled,onDisabled,onError