Payments
Quark Commerce supports multiple payment providers configured per-store. The checkout flow handles payment initiation; this page covers the payment completion and webhook handling.
Supported Providers
| Provider | Markets | Features |
|---|---|---|
| Stripe | Global | Credit cards, Apple Pay, Google Pay, 3D Secure |
| Iyzico | Turkey | Credit cards, installments, local payment methods |
Payment Flow
Stripe Integration
Client-Side (JavaScript/TypeScript)
import { loadStripe } from '@stripe/stripe-js';
const stripe = await loadStripe('pk_live_...');
// After checkout confirm returns clientSecret
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: 'John Doe' }
}
});
if (error) {
// Show error to user
} else if (paymentIntent.status === 'succeeded') {
// Confirm with API
await fetch('/api/storefront/payments/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: checkoutSessionId,
paymentIntentId: paymentIntent.id
})
});
}
Starting a Payment: payment/initialize
Every provider is started with the same call. Note that it is addressed by basket id, unlike the other checkout endpoints:
POST /api/storefront/checkout/{basketId}/payment/initialize
{
"methodId": "<id from GET /checkout/payment-methods>",
"returnUrl": "myapp://checkout/payment-result",
"buyerName": "Ayşe", "buyerSurname": "Yılmaz", "buyerEmail": "ayse@example.com", "buyerPhone": "+905551112233",
"invoiceType": "individual", "identityNumber": "…"
}
There are no card fields: the shopper enters the card on the payment provider's own page, so card data never passes through your client or the API. The buyer fields are optional overrides of what the checkout session already knows. For a corporate invoice send invoiceType: "corporate" with invoiceCompanyName, a 10-digit taxNumber and taxOffice. The amount charged is always the checkout session's total (items − discounts + shipping) — the client never sends an amount.
The response tells the client what to do next:
nextAction | Provider | What the client does |
|---|---|---|
redirect | Iyzico (Checkout Form), Payten | Navigate the browser or web view to redirectUrl — the provider's payment page |
redirect_html | Providers that return a ready page | Show htmlContent in a browser context or web view |
redirect_form_post | EstPOS (tr_est_v3) | Post formFields to formAction (formMethod), e.g. from an auto-submitting form |
bank_transfer | Bank transfer / EFT | Nothing to redirect: the order exists already. Show orderNumber and bankDetails |
client_integration | Stripe | Continue with the provider's client SDK (above); provider data, when any, is in clientData |
This set is closed: a client that handles these values works with every payment provider, including ones added later.
3-D Secure and where the shopper ends up
After the shopper confirms with their bank, the gateway sends the browser back to the API — /api/storefront/payments/return/{methodId} (Iyzico and EstPOS still use /api/storefront/payments/callback/{provider}). The API verifies the payment with the provider, server to server, checks that the amount charged matches the checkout total, creates the order once (a repeated callback returns the same order), and redirects the shopper to a result address with the outcome in the query string:
…?status=success&orderId=<guid>&orderNumber=ORD-20260921-482137
…?status=failure&error=<message or code>
…?status=pending&reason=<code>
pending means the money may have moved but there is no order yet — the gateway has not decided (payment_pending), or the charged amount did not match and the store will follow up (payment_under_review). Tell the shopper the payment is being checked; never that it failed.
That address is, in order of preference:
- the
returnUrlsent withpayment/initialize— for apps (myapp://…) and for storefronts on several domains; {Store.Url}/checkout/payment-result;{FrontendUrl}/checkout/payment-resultfrom the API's configuration.
Because the API redirects to it, a returnUrl must be allow-listed or the call is refused with 400: anything under the store's own Url is allowed, and further prefixes are listed by the operator in Checkout:AllowedReturnUrls (see the configuration reference).
The callback always ends in that redirect — also when something goes wrong — so a client can rely on it. A mobile app loads the 3-D Secure page in a web view, watches navigations, and treats the first one to its returnUrl as the result instead of loading it.
Never treat the redirect alone as proof of payment in a system of record. It tells the client what to show; the order and its paymentStatus in the API are the truth.
Bank transfer
The order is created at once with paymentStatus: "PendingPayment"; the merchant marks it paid when the money arrives. bankDetails is the key–value list configured on the payment method (bank, account holder, IBAN, …) — show it as it comes.
Payment Statuses
| Status | Description |
|---|---|
Pending | Payment initiated, awaiting completion |
Paid | Payment successfully received |
Failed | Payment failed or declined |
Refunded | Payment refunded (full or partial) |
Webhook Handling
Payment providers send webhooks to confirm payment status asynchronously. The API handles these at:
- Gateways on the payments core (e.g. Payten):
POST /api/storefront/payments/notify/{methodId}— register this address in the gateway's panel. It answers200once a notification is handled and500only when a retry may help. - Stripe:
POST /api/storefront/payments/webhook - Iyzico:
POST /api/storefront/payments/callback/iyzico - EstPOS:
POST /api/storefront/payments/callback/tr_est_v3
Webhooks serve as a backup confirmation mechanism. The primary flow is the client-side confirmation call (POST /payments/confirm). Webhooks handle edge cases where the user closes the browser before confirming.