Skip to main content

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.

FilePath
Handlerapps/app-api/src/order/handleOrder3dsSessionPost.ts
PagBank clientpackages/pagbank/src/threeds/pagbank3dsSessionPost.ts
Route registrationapps/app-api/src/order/orderRoutes.ts

Flow:

Validations:

  • Order must belong to the authenticated user (userId match).
  • 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.

FilePath
Handlerapps/app-api/src/order/handleOrderPayCreditCardPost.ts
BAAS typepackages/baas/src/creditCard/BaasCreditCardCreate.ts
PagBank adapterpackages/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:

FilePurpose
apps/app-console/src/types/pagseguro.d.tsGlobal types for window.PagSeguro

Key types: PagSeguroSDK, PagSeguro3DSAuthRequest, PagSeguro3DSAuthResult, PagSeguroErrorDetail.

Authentication flow

The authenticate3DS function in CartPaymentCreditCardForm.tsx orchestrates the frontend flow:

  1. Calls POST /order/{id}/3ds-session to get a session and environment.
  2. Initializes the SDK: window.PagSeguro.setUp({ session, env }).
  3. Builds the PagSeguro3DSAuthRequest with customer, payment method, amount, and billing address data.
  4. Calls window.PagSeguro.authenticate3DS(request).
  5. 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:

StatusMeaningAction
AUTH_FLOW_COMPLETEDCardholder authenticatedProceed with authenticationId
AUTH_NOT_SUPPORTEDIssuer does not support 3DSProceed without authenticationId
CHANGE_PAYMENT_METHODAuthentication deniedHalt payment, show toast
REQUIRE_CHALLENGEChallenge presented to userSDK handles automatically
ERRORSDK or network errorHalt payment, show toast

Sandbox testing

PagBank sandbox requires specific test card numbers paired with exact amount.value (in cents):

Card numberAmount (cents)Expected result
40000000000025032503Authenticated (no challenge)
40000000000023702370Authenticated (with challenge)
40000000000026022602CHANGE_PAYMENT_METHOD

Reference: PagBank 3DS sandbox docs

Corner cases

  • Missing user context: The handleOrder3dsSessionPost handler does not guard against unauthenticated requests — if userCtx is undefined, it crashes with a 500 instead of returning a 404.
  • Phone formatting: The frontend strips non-digits from user.phone and splits into area (first 2 digits) and number (remaining). If the phone is missing, defaults to 11 / 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 handleOrder3dsSessionPost and handleOrderPayCreditCardPost should check if (!userCtx) and return a 404 before accessing userCtx._id.
  • Import path: handleOrderPayCreditCardPost imports PAGBANK_PROVIDER_CODE using 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.detail with JSON.stringify for 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.ts
    • apps/app-api/src/order/handleOrderPayCreditCardPost.ts
    • apps/app-console/src/components/cart/CartPaymentCreditCardForm.tsx
    • apps/app-console/src/types/pagseguro.d.ts
    • packages/pagbank/src/threeds/pagbank3dsSessionPost.ts
    • packages/pagbank/src/creditCard/adapters/creditCardOrderToPagbankOrder.ts
    • packages/baas/src/creditCard/BaasCreditCardCreate.ts
    • packages/enum/src/pagbankThreeDSStatusEnum.ts
  • Related docs:
  • External: