Integrating a virtual POS into custom software is, contrary to what most teams assume, not just a matter of "calling iyzico's or PayTR's API." A production-ready integration has to solve four separate problems at the same time: (1) wiring up the 3D Secure (3DS) flow correctly — sending the user to the bank's verification screen and bringing them back; (2) verifying the callback/webhook securely — proving with a signature that the incoming notification really came from the provider, being idempotent against repeated notifications, and returning a fast 2xx; (3) reconciling the payment with the order — binding the event where you actually collected the money to the right order; and (4) handling post-transaction scenarios such as refunds, cancellations, and installments. If any one of these is missing, the integration looks like it "works" but is exposed to silent, money-losing bugs in production: an order processed twice, a cart that shows as "paid" yet was never confirmed, or a free product handed out via a forged callback. In this article, as a Sakarya-based B2B / white-label software firm, we share exactly the framework we apply in the virtual POS integrations we build: bank virtual POS versus payment institution, hosted form versus direct API, the 3DS flow, secure webhooks, reconciliation, refunds/installments, and a pre-launch checklist.
This article treats payment integration as both an API integration project and an e-commerce infrastructure concern. We applied the same discipline on the ERP/e-invoicing side; if you are weighing the "off-the-shelf or custom" decision for integration, see our article on e-commerce, ERP, and e-invoicing integration. The technical details below are based on the official documentation of iyzico and PayTR; because every provider can change over time, the only source of truth is the official documentation at the time of integration.
Bank virtual POS or payment institution?
The first architectural decision is which channel you will use to collect card payments. In Turkey, virtual POS is offered in two forms: (a) a bank virtual POS under contract directly with a bank and (b) bank-independent payment institutions (such as iyzico and PayTR). Payment institutions operate under Law No. 6493, the Law on Payment and Securities Settlement Systems, Payment Services and Electronic Money Institutions, and their operating license comes not from the BRSA but from the CBRT (Article 12 of the Law) (CBRT — Payment Institutions).
The practical difference is this: with a bank virtual POS you typically need a separate integration and contract for each bank; a bank-independent payment institution, on the other hand, lets you accept cards from most banks with a single integration. iyzico stands out particularly in marketplace / sub-merchant (submerchant) scenarios. The decision rule is simple:
- If your priority is a single bank, high volume, and the lowest commission and you have a strong relationship with one bank, a bank virtual POS can make sense; however, if you want multi-bank support, the integration burden multiplies.
- If you want a fast go-live, multi-bank acceptance, and a single integration, a payment institution (iyzico/PayTR) brings lower maintenance cost in most B2B and SMB scenarios.
- If you need marketplace / commission sharing / sub-merchant functionality, a payment institution with submerchant support is almost mandatory.
Hosted form or direct API? This is where your PCI-DSS scope is decided
The second and perhaps most critical decision is where card data will pass through — because this directly determines your PCI-DSS scope. There are two approaches:
- Hosted payment form: Card details are collected on a page or iframe hosted by the provider. In PayTR's iFrame API the user enters their card inside PayTR's iframe; on the iyzico side, CheckoutForm is iyzico's hosted payment page (CF-Initialize returns a
paymentPageUrl/token, and it can be embedded withiframe=true). In both cases the sensitive card data never touches your server, so your PCI-DSS scope shrinks significantly. - Direct API: You collect the payment with your own payment form; the card data passes through your own infrastructure. This offers full control and a branded experience but substantially increases your PCI-DSS obligations.
Both iyzico and PayTR take on PCI protection on the provider side; iyzico positions itself as a PCI DSS Level 1 service provider. But let us be clear: your exact SAQ type (SAQ A, SAQ A-EP, etc.) depends on the integration details and is not claimed in this article; the general truth is that scope is reduced when card data does not pass through your server. Our practical advice is clear:
Unless you have a genuine business requirement for a branded, fully controlled checkout experience, choose the hosted form. The few pixels of customization you gain are not worth the PCI-DSS audit and liability burden you take on.
How does the 3DS flow work? (iyzico example)
3D Secure is an additional verification performed by the cardholder's bank (a one-time code, in-app bank approval, etc.). If this step is skipped, the chargeback risk and liability largely remain with the business. In iyzico, 3DS is a two-step POST flow:
- Step 1 — Init 3DS: You POST to the
/payment/3dsecure/initializeendpoint with card/buyer details and acallbackUrl. The response returns a base64-encodedthreeDSHtmlContent. This HTML is rendered to redirect the user to the bank's 3DS verification screen. - Step 2 — Auth 3DS: After the user completes verification at the bank and returns to your
callbackUrl, you make a second POST to the auth endpoint with thepaymentIdreturned from Init. Here iyzico recommends using the v2 endpoint (/payment/v2/3dsecure/auth, i.e. 3DS 2.0); the legacy/payment/3dsecure/authis 3DS 1.0.
The verification result is carried by the mdStatus field: 1 means success, while the values 0 and 2–8 indicate a failed verification. An important caveat: we do not provide the exact field schema of the POST body the bank sends to your callbackUrl here as a definitive list — because it can vary by version and integration; it is a detail that must be verified against the current official documentation before going live (iyzico — Init 3DS, iyzico — Auth 3DS).
Throughout the flow, conversationId is used to match the request with the response. iyzico defines it as a "unique ID for request/response correlation"; you can put your own order number here, and the value you send in the request is returned unchanged in the response. Separately, iyzico assigns its own paymentId to every successful payment on its side; refunds and cancellations are carried out via this paymentId.
Secure webhook/callback verification: signature, idempotency, fast 2xx
This is the most overlooked and most dangerous part of a payment integration. The callback/webhook is the actual server-to-server event where you will mark the money as collected. Three rules are non-negotiable: verify the signature, be idempotent, return a fast 2xx.
1) Signature verification — prove the notification really came from the provider
Never treat any POST arriving at your callback as "paid" without verifying its signature. Otherwise an attacker could send a forged "successful payment" notification and have a free order opened. The two providers' mechanisms differ:
- PayTR (iFrame API): In the notification sent to
notify_url, the hash is computed asbase64_encode(hash_hmac('sha256', merchant_oid + merchant_salt + status + total_amount, merchant_key, true)). If thehashin the incoming POST does not match this value exactly, the request must be treated as not coming from PayTR and rejected. This formula is for the iFrame API (four fields); PayTR's other APIs may use a different set of fields, so do not mix them up. - iyzico (webhook): Verification is done using the
X-IYZ-SIGNATURE-V3header with HMAC-SHA256 (HEX); older header versions are no longer supported. The critical point: the order of the fields concatenated for the signature varies by webhook type. Direct payment, HPP, and subscription use different parameter orders — there is no single "generic" webhook signature order; verify the order for your own integration type against the official table.
iyzico also offers a separate response signature mechanism for synchronous API responses: specific parameters are concatenated with a colon (:), hashed with the secretKey using HMAC-SHA256, and encoded to HEX; unnecessary trailing zeros in prices are stripped (10.50 → 10.5, 10.00 → 10). The parameter order varies by endpoint; for example, for non-3DS auth the order is paymentId:currency:basketId:conversationId:paidPrice:price, and v2 3DS auth uses the same field order (iyzico — Response Signature Validation, iyzico — Webhook, PayTR — iFrame API Step 2).
2) Idempotency — if the same notification arrives twice, do not process it twice
Both providers may send more than one notification for the same transaction. If you do not deduplicate by your order ID, the same order gets confirmed twice, stock is decremented twice, and a shipment is opened twice. PayTR states this explicitly: repeat control must be done based on the unique merchant_oid field; to confirm/cancel the order, only the first notification should be processed, while repeated ones should merely receive a response. Practical implementation: keep the notification's state (processed/not processed) in your database as a single, unique record tied to the order ID, and perform the processing within a single atomic operation.
3) Return a fast 2xx — do the heavy work asynchronously
The provider expects a positive response to the notification within a short time; if it does not receive one, it retries. On the iyzico webhook, if the merchant does not return 2xx, it retries every 15 minutes, up to 3 times. PayTR, if it does not receive "OK", retries after 1 minute and requires the response body to be plain text OK only — with no HTML or other content. Two rules follow from this:
- Verify first, then save, then respond: verify the signature, mark the order uniquely, then return the expected response. Push heavy work such as sending emails, issuing invoices, or writing to the ERP onto an asynchronous queue; do not do these synchronously in a way that blocks the callback response.
- Return the response body in the format the provider expects: for PayTR exactly
OK; for iyzico a 2xx HTTP status code. Otherwise you will experience unnecessary retries and "lost" payments.
| Topic | iyzico (webhook) | PayTR (iFrame API callback) |
|---|---|---|
| Signature method | HMAC-SHA256 (HEX), X-IYZ-SIGNATURE-V3 header |
HMAC-SHA256 + base64, hash field in the POST |
| Signature field order | Varies by webhook type (direct / HPP / subscription differ) | merchant_oid + merchant_salt + status + total_amount (iFrame API) |
| Expected response | 2xx HTTP status code | Plain text OK |
| Retry behavior | If no 2xx, every 15 min, up to 3 times | If no OK, retry after 1 min |
| Deduplication key | paymentId / conversationId |
merchant_oid (only the first notification is processed) |
Payment–order reconciliation
Reconciliation is the bridge between "the money the provider collected" and "the order in your system." A poorly built reconciliation is the part that causes the most headaches on the accounting and customer-service sides. The way both providers build this bridge is a unique order ID generated by the merchant:
- PayTR:
merchant_oid(up to 64 alphanumeric characters) is the unique order number you assign for the transaction; it is returned to you in the callback to enable reconciliation. Also, in step 1 thepaytr_tokenis generated with HMAC-SHA256 + base64, and the concatenated string includes the fieldsmerchant_id + user_ip + merchant_oid + email + payment_amount + user_basket + no_installment + max_installment + currency + test_mode + merchant_salt. - iyzico: Use
conversationIdas your own order number; the value you send in the request is returned unchanged in the response. Its counterpart on the provider side ispaymentId— refunds/cancellations are done via this. Store both IDs in your own order record.
Practical principles for a robust reconciliation:
- Give authority to the callback, not to the browser: The "success" page the user's browser returns to is a hint, not proof. Confirm the order primarily via the server-to-server callback whose signature has been verified.
- Compare the amount and currency: Match the amount in the callback with the amount expected on your order; if they do not match, do not process it and raise an alert.
- Close out pending states: If the callback never arrives, use a background job to check "payment initiated but not concluded" orders against the provider's query API and close them out.
- Daily reconciliation: Regularly compare the provider's transaction/settlement reports with your own records; do not rely on the webhook alone.
Refunds, partial refunds, and installments
The second half of the payment flow is post-transaction scenarios; customer experience and accounting accuracy are won here. The key distinctions specific to iyzico:
- Refund — full or partial: There are two paths.
/payment/refundworks at the line-item level (withpaymentTransactionId, i.e. the transaction breakdown ID of the basket item, andprice)./v2/payment/refundworks at the payment level (withpaymentIdandprice); the system itself determines which basket item to refund. Both versions support full/partial refunds and return a bank reference (refundHostReference,authCode). - Cancel:
/payment/cancelis called with onlypaymentId, does not support a partial amount, and cancels the entire payment. According to the iyzico documentation, a cancel can be performed on the same day as the payment and does not create a debit/credit entry on the card statement; a refund, on the other hand, is reflected on the statement and can take several days depending on the bank. The exact same-day/cutoff rules vary by bank and value date; take this as a general principle, not a precise accounting rule. - Installment and BIN query: With
POST /payment/iyzipos/installmentyou can query installment options, rates, and amounts, as well as card family/scheme and bank information, by sendingprice(required) and an optionalbinNumber(8-digit BIN). Installment rates can also be obtained with the amount alone (without a BIN);conversationIdis sent in the request and returned unchanged in the response (iyzico — Refund & Cancel, iyzico — Installment & BIN).
As a business rule: embed the logic "cancel if same day, refund if later" into your system; when you issue a partial refund to a user, record on your side which item/amount was refunded and store the bank references.
Pre-launch checklist
- Signature verification: Is the signature verified for every callback/webhook? Is an unverified request definitively rejected? In iyzico, is the correct field order used for your integration type?
- Idempotency: For a notification arriving a second time for the same order ID (
merchant_oid/conversationId), is it not reprocessed, with only the appropriate response returned? - Fast response: Does the callback return a fast 2xx /
OKin the expected format? Is heavy work placed on an asynchronous queue? - Amount check: Are the amount and currency in the callback compared with the value expected on the order?
- Source of authority: Does order confirmation rely on the server-to-server notification rather than the browser redirect?
- Pending close-out: Is there a background query for "initiated but not concluded" payments where the callback never arrived?
- Refund/cancel: Have the full and partial refund, cancel, and installment scenarios been tested? Are the bank references stored?
- Secrets and HTTPS: Are secrets such as
secretKey/merchant_key/merchant_saltkept only on the server, in environment variables? Is the callback URL HTTPS? - Logging: Is every payment event (request, response, callback, signature result) logged in a traceable way?
- Test environment: Has end-to-end verification (including 3DS) been performed in the provider's sandbox/test mode?
How does Partnerfy build this?
As a Sakarya-based B2B / white-label software engineering firm, we treat virtual POS integration not as an "API call" but as a security and reconciliation problem. In a custom web software or e-commerce project, we first clarify the decision: payment institution or bank POS, hosted form or direct API (based on your PCI-DSS scope). Then we build and verify, end to end in a test environment, the 3DS flow, a signature-verified and idempotent callback, amount reconciliation, and the refund/installment scenarios. We usually wire this into your existing ERP/accounting systems as an API integration.
An honest note: no integration can guarantee "you will never have a problem" — banks, providers, and documentation change over time. Our promise is not a guarantee but discipline: verify the signature, be idempotent, give authority to the callback, and log and reconcile every step. If you want to discuss whether a ready-made plugin or a custom integration is right for you, our article on custom software vs. off-the-shelf software can also help you frame that decision.
Conclusion
A production-ready virtual POS integration starts with calling the API but does not end there. The real work is: choosing the right channel (payment institution vs. bank POS), deliberately reducing PCI-DSS scope (hosted vs. direct), wiring up the 3DS flow correctly, verifying the callback with a signature, being idempotent, returning a fast 2xx/OK, reconciling the amount and the order, and fully managing the refund/cancel/installment scenarios. Each of these steps is what separates an integration that "looks like it works" from one that is genuinely secure. If you need a companion as you design your payment integration or secure an existing setup, get in touch with us; we will map out the channel, PCI scope, and reconciliation plan together based on your scenario.
Sources
- iyzico — Init 3DS (/payment/3dsecure/initialize, threeDSHtmlContent, conversationId)
- iyzico — Auth 3DS (v2 recommended, completion with paymentId, mdStatus)
- iyzico — Response Signature Validation (HMAC-SHA256 + HEX)
- iyzico — Webhook (X-IYZ-SIGNATURE-V3, 2xx requirement, retry)
- iyzico — Installment & BIN Service (/payment/iyzipos/installment)
- iyzico — Refund & Cancel (/payment/refund, /v2/payment/refund, /payment/cancel)
- iyzico — CheckoutForm (hosted payment page, reduced PCI scope)
- PayTR — iFrame API Step 1 (paytr_token, hosted iframe, merchant_oid)
- PayTR — iFrame API Step 2 (callback hash, "OK" response, deduplication via merchant_oid)
- CBRT — Payment Institutions (Law No. 6493 Art. 12, operating license)