Card Payment — 2D
A 2D payment charges the card in a single request, with no bank-side 3D Secure authentication and no redirect. The buyer never leaves the form and the result comes back immediately. The trade-off: without 3D authentication there is no liability shift.
This page covers the Checkout Form path. The Checkout Form SDK calls POST /payments/form/2d itself from the buyer's browser; you do not write requests to this endpoint. The code you write is creating the intent on your server and mounting the form on your page → Checkout Form.
If you send card data from your own server, this page is not for you → Card — 2D.
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. 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 charges
If the intent's 3D decision is 2D, the SDK calls POST /payments/form/2d. The transaction is risk-checked, routed to a suitable provider and charged immediately.
The buyer returns to your result page
If the charge succeeds and you supplied a callbackUrl on the intent, the SDK sends the buyer to that address with a signed form POST. Without a callbackUrl the form shows its own success screen. On decline the form shows the error to the buyer.
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.
The 3D decision: when the form does not call 2D
2D is a request, not a guarantee. The intent's 3D decision determines which endpoint runs. The decision is resolved when the intent is created, from the sources below, and is fixed on the intent:
| Source | Behavior |
|---|---|
force3D: true on the intent | Overrides everything; the payment is processed with 3D Secure. |
force3D: false on the intent | The payment is not forced to 3D by policy; overrides both the panel setting and settings.threeD. |
settings.threeD.mode: "3d" on the intent | Charges on this intent require 3D Secure. |
settings.threeD.forceAmountMinor threshold | Amounts at or above the threshold require 3D Secure even in auto mode. |
| Panel 3D policy | Your account default, applied when the intent carries neither force3D nor settings.threeD. |
If the decision is 3D, the SDK uses the 3D Secure endpoint directly. A POST /payments/form/2d call for the same intent is refused with 409 THREE_D_REQUIRED; there is no silent switch to 3D. When the SDK receives this response it sends the same body once to POST /payments/form/3d and the flow continues as 3D Secure.
You set these fields when creating the intent; full description → Checkout Form.
When the provider cannot do 2D: automatic 3D Secure
Even when your policy says 2D, the provider routing the payment may not be permitted to perform 2D (non-secure) sales under its agreement. On the Checkout Form the buyer is at the screen, so the platform does not decline the charge — it upgrades it to 3D Secure:
POST /payments/form/2dreturns a 3D redirect response instead ofCAPTURED. The SDK sends the buyer to bank authentication; from there the flow proceeds like the 3D Secure flow.- The payment is marked with
escalationReason: "PROVIDER_2D_UNSUPPORTED", so you can tell that it was requested as 2D but moved to 3D because of provider capability. - The intent becomes 3D. Sending
POST /payments/form/2dagain for the same intent is refused with409 THREE_D_REQUIRED.
Even if you explicitly sent force3D: false on the intent, the provider's inability to do 2D overrides that preference and the payment is upgraded to 3D Secure. Assume that an intent where you chose 2D may still complete with 3D Secure; your result page and webhook handler must handle both cases.
Endpoint reference — POST /payments/form/2d
This section is for reference: the SDK builds and sends the body.
Authentication does not use an Authorization header; it has two parts:
tokenin the body: the form token from the intent response.- The session proof: the
pg_checkout_sessioncookie. Where cookies cannot be carried (WebView, third-party cookie blocking), the same proof is sent in the body ascheckoutSessionProof.
Fields
POST /payments/form/3d uses the same body.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The form token from the intent response. |
installment | integer | Yes | Number of installments; 1 for a single payment. |
paymentCards | object | Yes | The card. Either the raw card fields (cardHolderName, cardNumber, expireMonth, expireYear, cvcNumber) or the single-use token from the POST /payments/form/tokenize response (secureCardToken). cardSave: true if the buyer wants to save the card. |
paymentChannel | enum | Yes | WEB, WEEMOBILE, MOBILE, MOBILE_WEB, MOBILE_ANDROID, MOBILE_WINDOWS, MOBILE_TABLET, MOBILE_PHONE, MOBILE_IOS |
browserFingerprint | string | Yes | Browser fingerprint; produced by the SDK. |
installmentQuoteToken | string | Yes for installments | The binding quote token from the installment table. The server requires it when installment is greater than 1. |
checkoutSessionProof | string (max 2048) | No | The session proof; sent in the body where cookies cannot be carried. |
liveSessionTraceId | string | No | Trace id returned by the risk analysis. |
gsmNumber | string | No | The buyer's phone number when Buyer Protection is selected. |
buyerProtected | boolean | No | Whether the buyer selected Buyer Protection. |
The card is not saved on a 2D charge. If the buyer ticks "save my card" the payment is not affected; cards are saved only with 3D Secure.
Example body
The body the SDK sends (secure-fields mode):
{
"token": "<form token from the intent response>",
"installment": 1,
"paymentCards": {
"secureCardToken": "<cardToken from the tokenize response>"
},
"paymentChannel": "WEB",
"browserFingerprint": "<fingerprint>"
}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
A successful charge returns 200:
{
"status": "success",
"code": "PAYMENT_CAPTURED",
"message": "Payment processed",
"data": {
"id": "9c6f2b1a-4e8d-4a3f-b5e6-2d7c1a9b8e30",
"orderId": "ORD-9988",
"amount": 10050,
"currency": "TRY",
"status": "CAPTURED"
},
"statusCode": 200,
"requestId": "…"
}- On 2D,
data.statusis normally final:CAPTUREDon success,FAILEDon decline. data.amountis the charged amount: the intent'samount, plus the installment fee the system added for an installment payment.- If you supplied a
callbackUrl, the response also contains the signed form that carries the buyer to your result page. The SDK submits it. The fields are the same as on the 3D Secure return → The signed POST to your result page. - A charge upgraded to 3D because of provider capability returns the 3D Secure endpoint's response (
status: "partial",AWAITING_3D) → Card Payment — 3D Secure › Response. - A transaction whose outcome is not yet final returns
status: "partial"(HTTP202); the webhook delivers the final state.
The result that arrives through the browser is informational. Treat the order as paid based on the payment.captured webhook or a GET /payments/:id lookup.
Pre-authorization
You do not request a pre-authorization from the form: send "preauth": true when you create the intent with POST /payments/intents. The form charges according to this decision; when the payment succeeds it becomes AUTHORIZED instead of CAPTURED and payment.authorized is sent. Providers that do not support pre-authorization are removed from the candidates; if no suitable provider remains, the payment becomes FAILED with decline code PREAUTH_NOT_SUPPORTED.
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.
| Situation | Code | What to do |
|---|---|---|
| The body failed validation | 400 VALIDATION_ERROR | Use the current SDK; the SDK builds the body. |
| The intent's 3D decision is 3D | 409 THREE_D_REQUIRED | Do not retry 2D. The SDK sends the same body to POST /payments/form/3d. |
| The form token is invalid | 401 payment.checkout.session_invalid | Create a new intent and mount the form again. |
| The form token expired (about 15 minutes) | 401 payment.checkout.session_expired | Create a new intent and mount the form again. |
| The form was opened outside the origin the intent is bound to | 401 payment.checkout.origin_mismatch | Create the intent with the origin of the page that opens the form → Checkout Form. |
| The session proof is missing or invalid | 401 payment.checkout.proof_missing | Open 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 session | 401 payment.checkout.proof_mismatch | Open the form again. |
| The form token does not belong to this payment | 401 payment.checkout.token_mismatch | Open the form again with the token from the intent response. |
| The single-use card token is invalid or already used | 401 payment.secure_card.invalid_token | The buyer enters the card again; the SDK obtains a new token. |
| The transaction did not pass the security check | 409 payment.intent.live_risk_blocked | Retrying does not change the outcome. |
| This payment attempt is closed (the payment has a result) | 409 payment.intent.attempt_closed | Create 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_card | This 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. |
| No provider supports pre-authorization | Payment FAILED, code PREAUTH_NOT_SUPPORTED | Nothing was held; see Pre-authorization. |
| The installment quote is invalid or expired | Payment FAILED, code INSTALLMENT_QUOTE_REJECTED | Create 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 server | Payment FAILED, code CLIENT_PRICE_DIVERGENCE | Make sure the form is opened with the current SDK and create a new intent. |
| Provider decline (insufficient funds, invalid card …) | Payment FAILED | The form shows the decline to the buyer. The same intent cannot be charged again; if the buyer wants to pay again, create a new intent. |
| Temporary infrastructure error | 503 payment.dependency.unavailable, payment.credential.resolution_failed, payment.lock.unavailable | Temporary; the buyer can retry shortly. |
Webhooks
| Event | When |
|---|---|
payment.captured | The charge completed, funds captured |
payment.failed | The charge was declined |
payment.authorized | For a pre-authorization intent the amount was held (AUTHORIZED); no funds were captured yet |
Delivery and signature verification → Webhooks.
Next steps
- Card Payment — 3D Secure: the flow when the 3D decision or provider capability requires 3D
- Checkout Form: creating the intent,
force3Dandsettings.threeD - Installments & BIN: the displayed amount is the charged amount
- Refunds: returning money after capture