3D Secure Integration
Present 3D Secure challenges at checkout with the Preczn browser SDK, so card payments that need cardholder authentication complete instead of declining.
3D Secure (3DS) lets a card issuer ask the cardholder to verify a payment before it is approved, for example with a one-time code or a banking app prompt. Without 3DS, a card that requires this verification is declined. With 3DS, Preczn returns a challenge. Your page presents it in a Preczn-hosted lightbox, and the payment then completes.
Transactions remain server-to-server: your server creates the transaction with your private API key, exactly as it does today. The browser presents the challenge and never creates a transaction.
ℹ️ Supported connections
3D Secure challenges are currently supported on Stripe and Stripe Connect connections. Card payments only: Apple Pay and Google Pay payments are processed without a challenge.
How It Works
Eligibility
A challenge can only happen when payment is a single-use token created by the Preczn browser SDK (PaymentElements or PaymentFields).
A challenge needs the cardholder at a live checkout, on a page that can show it. A single-use token minted by the browser SDK moments ago is how Preczn knows that is true. The following are processed as none, whatever requestThreeDSecure value you send:
- Multi-use tokens (card-on-file, recurring). The cardholder is usually not present to complete a challenge.
- Raw card data sent from your server. It was not captured at a live checkout by the Preczn SDK.
These requests are not rejected. They run as they do today, so a card that requires a challenge declines. To make a raw-card integration eligible, tokenize with the Preczn browser SDK.
Integration Sequence
- Your page tokenizes the card with the Preczn browser SDK and sends the single-use token to your server.
- Your server calls
POST /v1/transactionswith the token andrequestThreeDSecure. - If the issuer requires a challenge, the transaction is returned with
authorization.statusP(pending) and anauthorization.threeDSecureChallengeobject withstatusCHALLENGE_REQUIRED. - Your server passes the transaction
idandauthorization.threeDSecureChallengeto your page, in whatever response your checkout endpoint already returns. - Your page calls
Preczn.ThreeDSecure.handleChallenge(id, threeDSecureChallenge.challengeKey, callback). The SDK opens a lightbox where the cardholder completes the issuer's challenge. - The SDK closes the lightbox and invokes your callback with the outcome.
- Your server confirms the final result with
GET /v1/transactions/:id.
sequenceDiagram
autonumber
actor Cardholder
participant Page as Your Page
participant SDK as Preczn SDK
participant Server as Your Server
participant Preczn as Preczn API
participant Issuer as Card Issuer
Cardholder->>Page: Enters card details
Page->>SDK: Tokenize card
SDK-->>Page: Single-use token (tkn_...)
Page->>Server: Submit checkout with token
Server->>Preczn: POST /v1/transactions<br/>requestThreeDSecure: "automatic"
Preczn-->>Server: authorization.status "P"<br/>threeDSecureChallenge: CHALLENGE_REQUIRED
Server-->>Page: Transaction id + threeDSecureChallenge
Page->>SDK: handleChallenge(id, challengeKey, callback)
SDK->>Cardholder: Opens challenge lightbox
Cardholder->>Issuer: Completes challenge
SDK-->>Page: callback({ transactionId, status, threeDSecureStatus })
Page->>Server: Confirm order
Server->>Preczn: GET /v1/transactions/:id
Preczn-->>Server: Final transaction (approved or declined)
If the issuer does not require a challenge, step 3 returns a final status (A or D) directly, and steps 4 to 6 are skipped.
API Contract
Request: requestThreeDSecure
requestThreeDSecureAdd the optional requestThreeDSecure field to a sale or auth request on POST /v1/transactions. It is a string, never an object. Any other value, including an object or an empty string, returns a 400.
| Value | Behavior |
|---|---|
| (omitted) | Same as none. This is the default. |
none | No 3D Secure. A card that requires a challenge declines. |
automatic | Your page can present a challenge. The processor decides whether one is needed. |
challenge | Same as automatic, and additionally asks the issuer to authenticate the cardholder. The issuer still decides whether the cardholder sees a challenge. |
{
"merchantId": "mrc_test_5kpqt630zh9kdrev2kxmdr66tr",
"type": "sale",
"amount": 1234,
"currency": "USD",
"payment": { "token": "tkn_test_8rd2m4ka7n1xq0zj5c3vbe9hw6" },
"requestThreeDSecure": "automatic"
}ℹ️ For most integrations, use
automatic. Usechallengeonly when you have a reason to request authentication on every eligible payment.
Response: authorization.threeDSecureChallenge
authorization.threeDSecureChallengeauthorization.threeDSecureChallenge appears only when 3D Secure was requested and the processor returned a challenge or an authentication result.
When a challenge is required, the POST /v1/transactions response looks like this:
{
"id": "txn_test_5kpqt630zh9kdrev2kxmdr66tr",
"authorization": {
"status": "P",
"threeDSecureChallenge": {
"status": "CHALLENGE_REQUIRED",
"challengeKey": "9f2c4e6a8b0d1f3e5a7c9b1d3f5e7a9c2b4d6f8e0a1c3e5f7b9d1a3c5e7f9b2d",
"expiresAt": "2026-10-19T14:30:00.000Z"
}
}
}After the challenge resolves, GET /v1/transactions/:id returns the authentication result:
{
"id": "txn_test_5kpqt630zh9kdrev2kxmdr66tr",
"authorization": {
"status": "A",
"threeDSecureChallenge": {
"status": "AUTHENTICATED",
"result": "authenticated",
"authenticationFlow": "challenge",
"version": "2.2.0"
}
}
}| Field | Type | Description |
|---|---|---|
status | string | The 3D Secure status. See 3D Secure Statuses. |
challengeKey | string | The key you pass, with the transaction id, to Preczn.ThreeDSecure.handleChallenge. Returned only on the POST /v1/transactions response while status is CHALLENGE_REQUIRED. It is not returned by GET /v1/transactions/:id. |
expiresAt | string | ISO-8601 time after which the challenge can no longer be completed (30 minutes after the transaction is created). Returned with CHALLENGE_REQUIRED. |
result | string | The raw 3D Secure result from the processor (for Stripe: authenticated, attempt_acknowledged, exempted, failed, not_supported, processing_error). |
resultReason | string | The processor's reason for the result, when one is given (for example card_not_enrolled). |
authenticationFlow | string | challenge if the cardholder completed a challenge, frictionless if the issuer authenticated them without one. |
version | string | The 3D Secure protocol version used (for example 2.2.0). |
ℹ️ There is no challenge URL in the response. The SDK builds the challenge page address itself from the transaction
idandchallengeKey.
3D Secure Statuses
| Status | Meaning |
|---|---|
CHALLENGE_REQUIRED | The cardholder must complete a challenge before the transaction can finish. Present it with handleChallenge. |
AUTHENTICATED | The issuer verified the cardholder. |
ATTEMPTED | Authentication was attempted, but the issuer did not complete it. The payment can still proceed. |
EXEMPTED | The transaction qualified for an exemption and did not need authentication. |
FAILED | The cardholder failed authentication. |
NOT_PERFORMED | Authentication was not supported for this card or could not be performed. |
UNKNOWN | The transaction resolved, but the processor returned no 3D Secure result. |
Browser SDK
Load the SDK
Add the 3ds resource to the Preczn script you already load for tokenization, using your public API key:
<script src="https://api.preczn.com/v1/clients/preczn.min.js?resources=paymentelements,3ds&publicApiKey=pub_test_xxxxxxxxxxxxxxxx"></script>The SDK exposes window.Preczn.ThreeDSecure. When it is ready, it sets Preczn.ThreeDSecure.ready to true and dispatches a Preczn.ThreeDSecure.ready event on document:
document.addEventListener('Preczn.ThreeDSecure.ready', function () {
// handleChallenge is available
});⚠️ HTTPS is required
handleChallengeruns only on pages served over HTTPS. Your page's Content Security Policy must allow the Preczn origin inscript-srcandframe-src(for examplehttps://api.preczn.com), as for PaymentElements.
handleChallenge
handleChallengePreczn.ThreeDSecure.handleChallenge(transactionId, challengeKey, callback, options);| Parameter | Type | Description |
|---|---|---|
transactionId | string | The transaction id from the POST /v1/transactions response. |
challengeKey | string | authorization.threeDSecureChallenge.challengeKey from the same response. |
callback | function | Called once with (result, errors) when the challenge ends. |
options | object | Optional. timeoutMs: how long to wait before closing the lightbox. Defaults to, and is capped at, 30 minutes. |
// `checkout` is whatever your checkout endpoint returned:
// the transaction id and authorization.threeDSecureChallenge from Preczn.
Preczn.ThreeDSecure.handleChallenge(
checkout.id,
checkout.threeDSecureChallenge.challengeKey,
function (result, errors) {
if (errors) {
// Cancelled, timed out, expired, or failed to open. See the table below.
showError(errors[0]);
return;
}
// A UX signal only: confirm on your server before fulfilling the order.
confirmOrderOnServer(result.transactionId);
}
);The SDK opens a full-screen lightbox (a 600 × 400 challenge panel, full-screen on small screens) with a × button to cancel. Only one challenge can be open at a time.
Callback Result
On completion, result is:
| Field | Description |
|---|---|
transactionId | The transaction id. |
status | The transaction status: A (approved), D (declined), E (error) or P (still pending). |
threeDSecureStatus | The 3D Secure status, for example AUTHENTICATED or FAILED. |
After the cardholder finishes, the SDK waits up to about 20 seconds for the final result. If the processor has not confirmed it by then, status is P. The transaction is still resolving, and GET /v1/transactions/:id or the transaction webhook will give you the final status.
Errors, Cancellation, and Timeouts
If the challenge does not complete, result is null and errors is an array containing one message:
| Message | When it happens |
|---|---|
3D Secure challenge cancelled | The cardholder closed the lightbox (× button or Escape). |
3D Secure challenge timed out | options.timeoutMs (default 30 minutes) passed before the challenge finished. |
This authentication request has expired | The challenge was opened after expiresAt. |
Challenge not found | The transactionId and challengeKey do not match a challenge for your public API key. |
A 3D Secure challenge is already in progress | handleChallenge was called while another challenge was open. |
Invalid transactionId / Invalid challengeKey | An argument is not in the expected format. |
3D Secure can only run on a secure (HTTPS) connection | The page is not served over HTTPS. |
3D Secure challenge is not available | The SDK could not determine the Preczn challenge origin. |
Preczn 3DS unavailable: ... | The SDK could not load, for example because of an invalid public API key. |
A handleChallenge call with no callback function throws an error.
⚠️ Cancellation and timeout do not cancel the transaction
The transaction stays
P. If the cardholder abandons the challenge and it is never completed, the transaction remainsPafterexpiresAt; Preczn does not expire it automatically. Do not fulfill the order unless the transaction is approved.
Confirm the Outcome
The callback is a signal for your checkout UI, not proof of payment. Before you fulfill an order, confirm the result from your server with GET /v1/transactions/:id using your private API key. authorization.status gives the final result, and authorization.threeDSecureChallenge holds the authentication result.
We recommend also subscribing to the transaction.approved and transaction.declined webhooks. If the cardholder completes the challenge but closes the browser before your callback runs, the webhook still delivers the final status, so you can reconcile abandoned checkouts. Webhooks are not required for the normal flow.
Testing
Use these Preczn test cards with a test Merchant on a Stripe or Stripe Connect test connection. Preczn maps each one to the Stripe-published test card shown.
| Scenario | Preczn test card | Stripe test card | Result |
|---|---|---|---|
| 3D Secure challenge | 4321000000000038 | 4000000000003220 | Returns CHALLENGE_REQUIRED. Complete the challenge in the lightbox to approve, or fail it to decline. |
| Generic decline | 4321000000000020 | 4000000000000002 | Declines. |
⚠️ Tokenize test cards in the browser
Enter the test card through PaymentElements or PaymentFields so the transaction uses a single-use token, and send
requestThreeDSecureasautomaticorchallenge. A test card sent as raw card data, or saved as a multi-use token, is processed asnoneand declines instead of showing a challenge.
Updated about 7 hours ago
