Payment Orchestrator
3D Form

Card Payment — 3D Secure

3D Secure has the buyer confirm the transaction with their own bank before the card is charged (SMS code, banking app approval, biometrics). This flow runs when you want liability shift, when your 3D policy requires it, or when the bank asks for authentication. The platform routes the transaction to a suitable provider and handles the bank round-trip for you.

This page covers the Checkout Form path. The Checkout Form SDK calls POST /payments/form/3d itself from the buyer's browser and also redirects the buyer to the bank; you do not write requests to this endpoint. The code you write is creating the intent on your server, mounting the form on your page and handling the result → Checkout Form.

If you send card data from your own server, this page is not for you → Card — 3D Secure.

All amounts are integers in minor units (kuruş): "amount": 10050 = ₺100.50. Never send decimals or floats.

How it works

Create an intent (your server)

Call POST /payments/intents with Step-Up authentication and supply the callbackUrl the result returns to. The form token in the response is valid for about 15 minutes and is bound to the origin the form opens on. All fields and 3D settings → Checkout Form.

Mount the form (browser)

Pass the token to the SDK and place the form on your page. On start-up the SDK obtains the session proof (POST /payments/form/session-init) and starts risk analysis (POST /payments/form/live-analyze). The proof is written to the pg_checkout_session cookie; card-touching calls are rejected without it.

The buyer enters the card

The form resolves the card brand and family, shows the installment table, and turns the card into a single-use token inside an isolated iframe (POST /payments/form/tokenize). The card number is never exposed to your page. If installments are chosen, the charged amount is bound to the displayed quote → Installments & BIN.

The SDK starts the 3D Secure payment

The SDK calls POST /payments/form/3d. The payment moves to AWAITING_3D and the response carries the bank redirect.

The buyer authenticates at the bank

The SDK sends the buyer to GET /payments/threed/:token. This address requires no authentication; the single-use token in the path is the credential. The buyer approves the transaction on the bank's 3D Secure page.

The platform charges

The bank returns the result to the platform. The platform verifies it and charges with the provider only if authentication succeeded.

The buyer returns to your result page

The buyer's browser is sent to the callbackUrl you supplied on the intent with a signed form POST → The signed POST to your result page.

Confirm with the webhook

payment.captured or payment.failed is delivered to your webhook endpoint. Close the order based on that event or on a GET /payments/:id lookup.

⚠️

A successful redirect is not a successful payment. 3D Secure has two separate phases: the buyer authenticating to the bank, and the money being charged. A payment can pass authentication and still fail the charge (insufficient funds, issuer decline, limits). Never ship goods because "the buyer came back from the bank"; wait for the payment.captured webhook or a GET /payments/:id confirmation.

When the form uses 3D Secure

The intent's 3D decision determines which endpoint runs. The decision is resolved when the intent is created, from force3D, settings.threeD and the panel 3D policy, and is fixed on the intent → Card Payment — 2D › The 3D decision.

The form charges with 3D Secure when:

  • The intent's 3D decision is 3D. The SDK calls POST /payments/form/3d directly.
  • The decision is 2D, but the provider routing the payment cannot perform 2D sales. The platform upgrades the charge to 3D Secure and marks the payment with escalationReason: "PROVIDER_2D_UNSUPPORTED" → Card Payment — 2D › When the provider cannot do 2D.

In both cases the flow proceeds as described on this page.

Endpoint reference — POST /payments/form/3d

This section is for reference: the SDK builds and sends the body.

Authentication does not use an Authorization header; it uses the form token in the body and the session proof (the pg_checkout_session cookie or checkoutSessionProof). The body is the same as for POST /payments/form/2d → field table.

Example body

The body the SDK sends (secure-fields mode, 3 installments):

{
  "token": "<form token from the intent response>",
  "installment": 3,
  "paymentCards": {
    "secureCardToken": "<cardToken from the tokenize response>"
  },
  "paymentChannel": "WEB",
  "browserFingerprint": "<fingerprint>",
  "installmentQuoteToken": "<quote token from the installment table>"
}

installmentQuoteToken is sent for installment payments; the charged amount is bound to the quote shown to the buyer.

The Checkout Form SDK builds this body. The SDK also adds the total it displayed to the buyer as an internal form field; it is not an amount you send. The server computes the charged amount from the intent's amount and, for installments, the installment fee in the binding quote.

Response

The payment waits in AWAITING_3D until the bank round-trip completes. The response returns with status: "partial" (HTTP 202):

{
  "status": "partial",
  "code": "AWAITING_3D",
  "message": "Payment processed",
  "data": {
    "id": "9c6f2b1a-4e8d-4a3f-b5e6-2d7c1a9b8e30",
    "orderId": "ORD-9988",
    "amount": 10350,
    "currency": "TRY",
    "status": "AWAITING_3D",
    "threeDRedirectUrl": "/api/v1/payments/threed/<single-use token>"
  },
  "statusCode": 202,
  "requestId": "…"
}

data.amount is the charged amount: the intent's amount plus the installment fee the system added from the installment quote. In the example the intent amount is 10050 and the amount charged over 3 installments is 10350.

FieldDescription
data.threeDRedirectUrlWhere the buyer is sent. It may be relative; in that case it is resolved against the gateway root. Single-use.
data.threeDRedirectMethodThe redirect method, when present (GET or POST).
data.threeDRedirectDataOnly for a POST redirect: the form fields to send to the bank.

The SDK performs the redirect itself. A payment abandoned in AWAITING_3D is picked up by a background cleanup job: it asks the provider for the real outcome and binds the payment to its true final state (FAILED if nothing was charged, CAPTURED if the charge landed).

The signed POST to your result page

After the bank round-trip, the buyer's browser is sent to the callbackUrl you supplied on the intent with an HTML form POST (application/x-www-form-urlencoded). The same POST also arrives after a successful 2D charge. The body carries these fields:

FieldMeaning
paymentIdThe payment id
orderIdThe order reference you supplied at intent creation (sent only when set on the intent)
statussuccess, failure or pending (outcome not yet final)
success"true" / "false"
codeMachine-readable result code
messageHuman-readable result message
_timestampSigning time, ISO 8601
_signaturev2=<hex>: HMAC-SHA256 over ${paymentId}.${_timestamp}.${canonical}, hex digest

The canonical string (canonical) is every field except _signature (including _timestamp), sorted by key, each written as key=value and joined with a newline (\n).

⚠️

This POST is informational; do not rely on it for fulfilment. _signature and _timestamp are integrity fields produced by the platform; no secret is distributed that would let you verify the signature on the merchant side. Never treat the order as paid based on any field in this POST; confirm the final state with GET /payments/:id or the payment.captured webhook.

Use this POST to drive the buyer's UI (render the result page, resume the session). The browser may never reach your callbackUrl (closed tab, network loss); the webhook is sent in every case.

Status transitions

FromToWhen
INITIATEDAWAITING_3D3D Secure payment accepted, redirect issued
AWAITING_3DCAPTUREDBank authentication and charge succeeded
AWAITING_3DFAILEDThe buyer failed or abandoned authentication, or the charge was declined
AWAITING_3DNEEDS_RECONCILIATIONThe outcome could not be determined (e.g. the bank's return was lost); a reconciliation job queries the provider again and binds the final state

Card storage

On the form, a card is saved when the buyer ticks "save my card" and the charge completes successfully with 3D Secure. This requires:

  • Card storage enabled in the form settings of your panel.
  • The intent created with customerId (or cardUserKey); the card is saved under that key.

Because the payment response goes to the buyer's browser, the permanent card reference is not returned there. After the payment completes, read the cards from your server with GET /payments/cards → Stored Cards.

Pre-authorization

You do not request a pre-authorization from the form: send "preauth": true when you create the intent with POST /payments/intents. When the buyer completes the bank verification no money is charged; the payment becomes AUTHORIZED instead of CAPTURED and payment.authorized is sent.

  • The provider must also support pre-authorization over 3D Secure. If no suitable provider remains, the payment becomes FAILED with decline code PREAUTH_3DS_NOT_SUPPORTED.
  • If no provider supports pre-authorization over 3D Secure at all, the request is rejected with 422 payment.preauth.three_ds_unsupported. The instruction is never turned into an immediate charge.

Capture, void and the 7-day window: Pre-authorization, Capture & Void.

Errors

Error responses use the common error envelope; branch on code, not on message → Error Handling.

SituationCodeWhat to do
The body failed validation400 VALIDATION_ERRORUse the current SDK; the SDK builds the body.
The form token is invalid401 payment.checkout.session_invalidCreate a new intent and mount the form again.
The form token expired (about 15 minutes)401 payment.checkout.session_expiredCreate a new intent and mount the form again.
The form was opened outside the origin the intent is bound to401 payment.checkout.origin_mismatchCreate the intent with the origin of the page that opens the form → Checkout Form.
The session proof is missing or invalid401 payment.checkout.proof_missingOpen the form with the SDK. Where cookies cannot be carried, the proof is sent in the body as checkoutSessionProof.
The session proof belongs to another session401 payment.checkout.proof_mismatchOpen the form again.
The form token does not belong to this payment401 payment.checkout.token_mismatchOpen the form again with the token from the intent response.
The single-use card token is invalid or already used401 payment.secure_card.invalid_tokenThe buyer enters the card again; the SDK obtains a new token.
The transaction did not pass the security check409 payment.intent.live_risk_blockedRetrying does not change the outcome.
This payment attempt is closed (the payment has a result)409 payment.intent.attempt_closedCreate a new intent and open the form again.
The intent was opened for a non-card method (e.g. paymentMethod: "WALLET")409 payment.method.intent_not_cardThis intent cannot be paid by card; create the intent without paymentMethod or with "CARD". The intent was not changed and can still be paid with its own method → Checkout Form — Payment.
Pre-authorization over 3D Secure is not supported422 payment.preauth.three_ds_unsupported or payment FAILED, code PREAUTH_3DS_NOT_SUPPORTEDNothing was held; see Pre-authorization.
The redirect token expired or was already usedGET /payments/threed/:token shows an error pageCreate a new intent and start the payment again.
Bank authentication failedPayment FAILED; status: failure in the signed POST; payment.failedCreate a new intent if the buyer wants to pay again.
Authentication succeeded, charge declinedPayment FAILEDShow the decline to the buyer; do not fulfil.
The installment quote is invalid or expiredPayment FAILED, code INSTALLMENT_QUOTE_REJECTEDCreate a new intent; the buyer picks the installment again from the current table.
The total the form displayed to the buyer does not match the charged amount computed by the serverPayment FAILED, code CLIENT_PRICE_DIVERGENCEMake sure the form is opened with the current SDK and create a new intent.
Temporary infrastructure error503 payment.dependency.unavailable, payment.credential.resolution_failed, payment.lock.unavailableTemporary; the buyer can retry shortly.

Webhooks

EventWhen
payment.capturedAuthentication and charge completed, funds captured
payment.failedAuthentication or charge failed (including abandoned 3D Secure sessions resolved as not charged)
payment.authorizedFor a pre-authorization intent the verification completed and the amount was held (AUTHORIZED); no funds were captured yet

Delivery and signature verification → Webhooks.

Next steps