Payment Orchestrator
2D Form

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:

SourceBehavior
force3D: true on the intentOverrides everything; the payment is processed with 3D Secure.
force3D: false on the intentThe payment is not forced to 3D by policy; overrides both the panel setting and settings.threeD.
settings.threeD.mode: "3d" on the intentCharges on this intent require 3D Secure.
settings.threeD.forceAmountMinor thresholdAmounts at or above the threshold require 3D Secure even in auto mode.
Panel 3D policyYour 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/2d returns a 3D redirect response instead of CAPTURED. 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/2d again for the same intent is refused with 409 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:

  • token in the body: the form token from the intent response.
  • The session proof: the pg_checkout_session cookie. Where cookies cannot be carried (WebView, third-party cookie blocking), the same proof is sent in the body as checkoutSessionProof.

Fields

POST /payments/form/3d uses the same body.

FieldTypeRequiredDescription
tokenstringYesThe form token from the intent response.
installmentintegerYesNumber of installments; 1 for a single payment.
paymentCardsobjectYesThe 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.
paymentChannelenumYesWEB, WEEMOBILE, MOBILE, MOBILE_WEB, MOBILE_ANDROID, MOBILE_WINDOWS, MOBILE_TABLET, MOBILE_PHONE, MOBILE_IOS
browserFingerprintstringYesBrowser fingerprint; produced by the SDK.
installmentQuoteTokenstringYes for installmentsThe binding quote token from the installment table. The server requires it when installment is greater than 1.
checkoutSessionProofstring (max 2048)NoThe session proof; sent in the body where cookies cannot be carried.
liveSessionTraceIdstringNoTrace id returned by the risk analysis.
gsmNumberstringNoThe buyer's phone number when Buyer Protection is selected.
buyerProtectedbooleanNoWhether 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.status is normally final: CAPTURED on success, FAILED on decline.
  • data.amount is the charged amount: the intent's amount, 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" (HTTP 202); 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.

SituationCodeWhat to do
The body failed validation400 VALIDATION_ERRORUse the current SDK; the SDK builds the body.
The intent's 3D decision is 3D409 THREE_D_REQUIREDDo not retry 2D. The SDK sends the same body to POST /payments/form/3d.
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.
No provider supports pre-authorizationPayment FAILED, code PREAUTH_NOT_SUPPORTEDNothing was held; see Pre-authorization.
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.
Provider decline (insufficient funds, invalid card …)Payment FAILEDThe 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 error503 payment.dependency.unavailable, payment.credential.resolution_failed, payment.lock.unavailableTemporary; the buyer can retry shortly.

Webhooks

EventWhen
payment.capturedThe charge completed, funds captured
payment.failedThe charge was declined
payment.authorizedFor a pre-authorization intent the amount was held (AUTHORIZED); no funds were captured yet

Delivery and signature verification → Webhooks.

Next steps