PagBank 3DS Integration
Summary
3D Secure (3DS) is a fraud prevention protocol for credit card payments. This integration uses the PagBank browser SDK to authenticate the cardholder before processing a charge, reducing chargebacks and enabling liability shift.
Motivation
- Fraud reduction: 3DS validates the cardholder's identity through the issuing bank.
- Liability shift: Authenticated transactions shift chargeback liability from the merchant to the card issuer.
- Conditional activation: The feature is toggled per event via
event.is3DSActive, allowing gradual rollout.
Architecture
Backend
Session endpoint
POST /order/{id}/3ds-session — creates a PagBank 3DS session for the given order.
| File | Path |
|---|---|
| Handler | apps/app-api/src/order/handleOrder3dsSessionPost.ts |
| PagBank client | packages/pagbank/src/threeds/pagbank3dsSessionPost.ts |
| Route registration | apps/app-api/src/order/orderRoutes.ts |
Flow:
Validations:
- Order must belong to the authenticated user (
userIdmatch). - Order must have
status: PENDING_PAYMENT. - Order must not be soft-deleted (
removedAt: null).
Environment: Returns PROD when NODE_ENV === 'production', otherwise SANDBOX.
Authentication ID in payment
When the frontend obtains an authenticationId from 3DS, it passes it to the credit card payment endpoint.
| File | Path |
|---|---|
| Handler | apps/app-api/src/order/handleOrderPayCreditCardPost.ts |
| BAAS type | packages/baas/src/creditCard/BaasCreditCardCreate.ts |
| PagBank adapter | packages/pagbank/src/creditCard/adapters/creditCardOrderToPagbankOrder.ts |
The authenticationId field is optional in BaasCreditCardCreateArgs. When present, the PagBank adapter includes it in the charge as:
{
"authentication_method": {
"type": "THREEDS",
"id": "<authenticationId>"
}
}
Frontend
SDK setup
The PagBank browser SDK is loaded via a <script> tag in _document.tsx. Global types are declared in:
| File | Purpose |
|---|---|
apps/app-console/src/types/pagseguro.d.ts | Global types for window.PagSeguro |
Key types: PagSeguroSDK, PagSeguro3DSAuthRequest, PagSeguro3DSAuthResult, PagSeguroErrorDetail.
Authentication flow
The authenticate3DS function in CartPaymentCreditCardForm.tsx orchestrates the frontend flow:
- Calls
POST /order/{id}/3ds-sessionto get a session and environment. - Initializes the SDK:
window.PagSeguro.setUp({ session, env }). - Builds the
PagSeguro3DSAuthRequestwith customer, payment method, amount, and billing address data. - Calls
window.PagSeguro.authenticate3DS(request). - Handles the result based on
PAGBANK_THREE_DS_STATUS_ENUM.
Conditional activation
3DS is only triggered when event.is3DSActive === true:
if (event?.is3DSActive) {
setIs3dsLoading(true);
const result = await authenticate3DS(data);
setIs3dsLoading(false);
if (
result === PAGBANK_THREE_DS_STATUS_ENUM.CHANGE_PAYMENT_METHOD ||
result === PAGBANK_THREE_DS_STATUS_ENUM.ERROR
) {
return; // halt payment
}
authenticationId = result;
}
When is3DSActive is false, the payment proceeds directly with card encryption (no 3DS).
Status enum
Defined in packages/enum/src/pagbankThreeDSStatusEnum.ts:
| Status | Meaning | Action |
|---|---|---|
AUTH_FLOW_COMPLETED | Cardholder authenticated | Proceed with authenticationId |
AUTH_NOT_SUPPORTED | Issuer does not support 3DS | Proceed without authenticationId |
CHANGE_PAYMENT_METHOD | Authentication denied | Halt payment, show toast |
REQUIRE_CHALLENGE | Challenge presented to user | SDK handles automatically |
ERROR | SDK or network error | Halt payment, show toast |
Sandbox testing
PagBank sandbox requires specific test card numbers paired with exact amount.value (in cents):
| Card number | Amount (cents) | Expected result |
|---|---|---|
4000000000002503 | 2503 | Authenticated (no challenge) |
4000000000002370 | 2370 | Authenticated (with challenge) |
4000000000002602 | 2602 | CHANGE_PAYMENT_METHOD |
Reference: PagBank 3DS sandbox docs
Corner cases
- Missing user context: The
handleOrder3dsSessionPosthandler does not guard against unauthenticated requests — ifuserCtxisundefined, it crashes with a 500 instead of returning a 404. - Phone formatting: The frontend strips non-digits from
user.phoneand splits intoarea(first 2 digits) andnumber(remaining). If the phone is missing, defaults to11/999999999. - Holder name: The Zod schema enforces at least two words (first + last name). PagBank rejects single-word names.
- Cart expiration: Even if 3DS succeeds, the payment endpoint checks cart expiration before processing.
Issues and improvements
- Missing user guard: Both
handleOrder3dsSessionPostandhandleOrderPayCreditCardPostshould checkif (!userCtx)and return a 404 before accessinguserCtx._id. - Import path:
handleOrderPayCreditCardPostimportsPAGBANK_PROVIDER_CODEusing a relative workspace path (packages/pagbank/src/pagbankProviderInfo.js) instead of@nittio/pagbank. This breaks Vitest resolution and should be refactored to use the package alias. - Error detail logging: The frontend logs
pagSeguroError.detailwithJSON.stringifyfor debugging, but this is only visible in the browser console. Consider sending error details to a monitoring service.
References
- Source files:
apps/app-api/src/order/handleOrder3dsSessionPost.tsapps/app-api/src/order/handleOrderPayCreditCardPost.tsapps/app-console/src/components/cart/CartPaymentCreditCardForm.tsxapps/app-console/src/types/pagseguro.d.tspackages/pagbank/src/threeds/pagbank3dsSessionPost.tspackages/pagbank/src/creditCard/adapters/creditCardOrderToPagbankOrder.tspackages/baas/src/creditCard/BaasCreditCardCreate.tspackages/enum/src/pagbankThreeDSStatusEnum.ts
- Related docs:
- External: