SDKs & Exemplos
React / Next.js
Hook React pronto para uso com suporte a cartão simples e 3DS Cielo.
React / Next.js
Hook useCaktoCheckout
Hook completo que encapsula todo o fluxo de pagamento — detecção de fraude, 3DS Cielo e submissão do checkout.
// hooks/useCaktoCheckout.ts
import { useState, useRef, useEffect, useCallback } from 'react'
const BASE_URL = process.env.NEXT_PUBLIC_CAKTO_BASE_URL!
const MERCHANT_ID = Number(process.env.NEXT_PUBLIC_CAKTO_MERCHANT_ID)
const IS_PROD = process.env.NEXT_PUBLIC_ENV === 'production'
const NETHONE_URL = process.env.NEXT_PUBLIC_NETHONE_SCRIPT_URL!
// ⚠️ Em produção, use o BFF — nunca exponha o token no bundle do cliente
const API_TOKEN = process.env.NEXT_PUBLIC_CAKTO_API_TOKEN!
const BPMPI_URL = IS_PROD
? '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'
// Helpers
async function loadScript(src: string): Promise<void> {
if (document.querySelector(`script[src="${src}"]`)) return
return new Promise((res, rej) => {
const s = document.createElement('script')
s.src = src; s.async = true
s.onload = () => res()
s.onerror = () => rej(new Error(`Falha ao carregar ${src}`))
document.head.appendChild(s)
})
}
async function getUserIp(): Promise<string> {
try {
const r = await fetch('https://api.ipify.org?format=json', {
signal: AbortSignal.timeout(3_000),
})
return (await r.json()).ip
} catch { return '' }
}
async function getThreeDsToken(): Promise<string> {
const r = await fetch(`${BASE_URL}/api/3ds/token/?provider=cielo`, {
signal: AbortSignal.timeout(10_000),
})
if (!r.ok) throw new Error(`Falha ao obter token 3DS: ${r.status}`)
return (await r.json()).access_token
}
type ThreeDSData = {
Cavv: string; Xid: string; Eci: string
Version: string; ReferenceId: string; DataOnly: boolean
}
type ThreeDSResult = { ok: true; data: ThreeDSData } | { ok: false; error: string }
function runThreeDsAuth(params: {
accessToken: string; amount: number; installments: number
cardNumber: string; expMonth: string; expYear: string; deviceIp: string
}): Promise<ThreeDSResult> {
return new Promise((resolve) => {
const timeout = setTimeout(() => resolve({ ok: false, error: '3DS timeout' }), 90_000)
let activated = false
const activate = () => { if (!activated) { activated = true; window.bpmpi_authenticate?.() } }
const watchdog = setTimeout(activate, 5_000)
const observer = new MutationObserver(() => {
const f = document.querySelector<HTMLIFrameElement>('iframe[name="bpmpi_frame"]')
if (!f || f.dataset.styled) return
f.dataset.styled = 'true'
Object.assign(f.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 = (r: ThreeDSResult) => {
clearTimeout(timeout); clearTimeout(watchdog); observer.disconnect(); resolve(r)
}
window.bpmpi_config = () => ({
Environment: IS_PROD ? 'PRD' : 'SDB', Debug: false,
onReady: () => { clearTimeout(watchdog); activate() },
onSuccess: (e: any) => done({ ok: true, data: {
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' }),
onUnsupportedBrand: () => done({ ok: false, error: 'Bandeira não suportada pelo 3DS' }),
onError: (e: any) => done({ ok: false, error: e?.ReturnMessage ?? 'Erro SDK 3DS' }),
})
document.getElementById('bpmpi-form')?.remove()
const form = document.createElement('form')
form.id = 'bpmpi-form'; form.style.display = 'none'
const yr = String(params.expYear)
;[
['bpmpi_auth', 'true'],
['bpmpi_accesstoken', params.accessToken],
['bpmpi_ordernumber', `ORDER-${Date.now()}`],
['bpmpi_currency', '986'],
['bpmpi_totalamount', String(Math.round(params.amount * 100))],
['bpmpi_installments', String(params.installments)],
['bpmpi_paymentmethod', 'credit'],
['bpmpi_cardnumber', params.cardNumber.replace(/\D/g, '')],
['bpmpi_cardexpirationmonth', String(params.expMonth).padStart(2, '0')],
['bpmpi_cardexpirationyear', yr.length === 2 ? `20${yr}` : yr],
['bpmpi_device_ipaddress', params.deviceIp || '127.0.0.1'],
['bpmpi_device_1_channel', 'Browser'],
].forEach(([cls, val]) => {
const i = document.createElement('input'); i.className = cls; i.value = val; form.appendChild(i)
})
document.body.appendChild(form)
})
}
// Tipos
type CheckoutStatus = 'idle' | 'loading' | 'success' | 'error'
interface CheckoutInput {
use3DS: boolean
card: {
number: string; holderName: string
expMonth: string; expYear: string; cvv: string
}
customer: {
name: string; email: string; phone: string
docType: 'cpf' | 'cnpj'; docNumber: string; birthDate?: string
}
address: {
street: string; number: string; complement?: string
neighborhood: string; city: string; state: string; zipcode: string
}
order: {
amount: number; installments: number
items: Array<{
title: string; unitPrice: number
externalRef?: string; quantity?: number; tangible?: boolean
}>
webhookUrl: string
}
}
// Hook
export function useCaktoCheckout() {
const [status, setStatus] = useState<CheckoutStatus>('idle')
const [error, setError] = useState<string | null>(null)
const fraudSessionId = useRef(crypto.randomUUID())
useEffect(() => {
loadScript(NETHONE_URL).then(() => {
window.dftp?.init({ attemptReference: fraudSessionId.current })
})
}, [])
const preload3DS = useCallback(() => {
loadScript(CARDINAL_URL)
loadScript(BPMPI_URL)
}, [])
const submit = useCallback(async (input: CheckoutInput) => {
setStatus('loading')
setError(null)
try {
await window.dftp?.profileCompleted()
const ip = await getUserIp()
let threeDSecure: ThreeDSData | undefined
if (input.use3DS) {
const [accessToken] = await Promise.all([
getThreeDsToken(),
loadScript(CARDINAL_URL),
loadScript(BPMPI_URL),
])
const authResult = await runThreeDsAuth({
accessToken, amount: input.order.amount,
installments: input.order.installments,
cardNumber: input.card.number,
expMonth: input.card.expMonth, expYear: input.card.expYear,
deviceIp: ip,
})
if (!authResult.ok) throw new Error(authResult.error)
threeDSecure = authResult.data
}
const payload: Record<string, unknown> = {
user: MERCHANT_ID,
paymentMethod: input.use3DS ? 'threeDs' : 'credit_card',
amount: input.order.amount.toFixed(2),
installments: input.order.installments,
antifraud_profiling_attempt_reference: fraudSessionId.current,
card: {
number: input.card.number.replace(/\D/g, ''),
holderName: input.card.holderName.toUpperCase(),
expMonth: input.card.expMonth,
expYear: input.card.expYear,
cvv: input.card.cvv,
},
customer: {
name: input.customer.name,
email: input.customer.email,
phone: input.customer.phone.replace(/\D/g, ''),
docType: input.customer.docType,
docNumber: input.customer.docNumber.replace(/\D/g, ''),
birthDate: input.customer.birthDate ?? null,
ip: ip || null,
},
address: { ...input.address, country: 'BR' },
items: input.order.items.map(i => ({
title: i.title,
unitPrice: i.unitPrice.toFixed(2),
quantity: i.quantity ?? 1,
tangible: i.tangible ?? false,
externalRef: i.externalRef ?? null,
})),
postbackUrl: input.order.webhookUrl,
checkoutUrl: typeof window !== 'undefined' ? window.location.href : '',
}
if (threeDSecure) payload.threeDSecure = threeDSecure
const res = await fetch(`${BASE_URL}/api/checkout/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// ⚠️ Use BFF em produção — nunca exponha o token no bundle do cliente
'Authorization': `Token ${API_TOKEN}`,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30_000),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error ?? JSON.stringify(data.errors ?? data))
setStatus('success')
return data
} catch (e: any) {
setStatus('error')
setError(e.message)
throw e
}
}, [])
return { submit, preload3DS, status, error, fraudSessionId: fraudSessionId.current }
}Uso no componente
export function CheckoutPage() {
const { submit, preload3DS, status, error } = useCaktoCheckout()
const USE_3DS = true
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
try {
const payment = await submit({
use3DS: USE_3DS,
card: {
number: formData.get('cardNumber') as string,
holderName: formData.get('holderName') as string,
expMonth: formData.get('expMonth') as string,
expYear: formData.get('expYear') as string,
cvv: formData.get('cvv') as string,
},
customer: {
name: formData.get('name') as string,
email: formData.get('email') as string,
phone: formData.get('phone') as string,
docType: 'cpf',
docNumber: formData.get('cpf') as string,
},
address: {
street: formData.get('street') as string,
number: formData.get('addrNumber') as string,
complement: formData.get('complement') as string || undefined,
neighborhood: formData.get('neighborhood') as string,
city: formData.get('city') as string,
state: formData.get('state') as string,
zipcode: (formData.get('zipcode') as string).replace(/\D/g, ''),
},
order: {
amount: 149.90,
installments: 1,
items: [{
title: 'Curso de Marketing Digital',
externalRef: 'PROD-001',
unitPrice: 149.90,
}],
webhookUrl: 'https://sua-loja.com/webhooks/cakto',
},
})
if (payment.status === 'paid') {
window.location.href = '/obrigado'
} else {
alert('Pagamento não aprovado. Verifique os dados do cartão.')
}
} catch {
alert('Não foi possível processar o pagamento. Tente novamente.')
}
}
return (
<form onSubmit={handleSubmit}>
{/* Seus campos de formulário */}
<button
type="submit"
disabled={status === 'loading'}
onMouseEnter={USE_3DS ? preload3DS : undefined}
>
{status === 'loading' ? 'Processando...' : 'Pagar'}
</button>
{error && <p>Pagamento não aprovado. Tente novamente.</p>}
</form>
)
}Next.js BFF
Para não expor credenciais no bundle do cliente, crie rotas de API no Next.js:
// app/api/cakto/3ds-token/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const base = process.env.CAKTO_BASE_URL!
const r = await fetch(`${base}/api/3ds/token/?provider=cielo`)
return NextResponse.json(await r.json(), { status: r.status })
}// app/api/cakto/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
const base = process.env.CAKTO_BASE_URL!
const token = process.env.CAKTO_API_TOKEN!
export async function POST(req: NextRequest) {
const r = await fetch(`${base}/api/checkout/`, {
method: 'POST',
headers: {
'Authorization': `Token ${token}`,
'Content-Type': 'application/json',
},
body: await req.text(),
})
return NextResponse.json(await r.json(), { status: r.status })
}No hook, substitua as URLs:
${BASE_URL}/api/3ds/token/...→/api/cakto/3ds-token${BASE_URL}/api/checkout/→/api/cakto/checkout- Remova o header
Authorizationdas chamadas client-side
Declaração de tipos globais
// types/global.d.ts
declare global {
interface Window {
dftp?: {
init: (options: { attemptReference: string }) => void
profileCompleted: () => Promise<void>
}
bpmpi_authenticate?: () => void
bpmpi_config?: () => {
Environment: string
Debug: boolean
onReady?: () => void
onSuccess?: (e: any) => void
onFailure?: () => void
onUnenrolled?: () => void
onDisabled?: () => void
onUnsupportedBrand?: () => void
onError?: (e: any) => void
}
}
}
export {}