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 with iframe=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/initialize endpoint with card/buyer details and a callbackUrl. The response returns a base64-encoded threeDSHtmlContent. 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 the paymentId returned from Init. Here iyzico recommends using the v2 endpoint (/payment/v2/3dsecure/auth, i.e. 3DS 2.0); the legacy /payment/3dsecure/auth is 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 as base64_encode(hash_hmac('sha256', merchant_oid + merchant_salt + status + total_amount, merchant_key, true)). If the hash in 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-V3 header 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.5010.5, 10.0010). 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 the paytr_token is generated with HMAC-SHA256 + base64, and the concatenated string includes the fields merchant_id + user_ip + merchant_oid + email + payment_amount + user_basket + no_installment + max_installment + currency + test_mode + merchant_salt.
  • iyzico: Use conversationId as your own order number; the value you send in the request is returned unchanged in the response. Its counterpart on the provider side is paymentId — 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/refund works at the line-item level (with paymentTransactionId, i.e. the transaction breakdown ID of the basket item, and price). /v2/payment/refund works at the payment level (with paymentId and price); the system itself determines which basket item to refund. Both versions support full/partial refunds and return a bank reference (refundHostReference, authCode).
  • Cancel: /payment/cancel is called with only paymentId, 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/installment you can query installment options, rates, and amounts, as well as card family/scheme and bank information, by sending price (required) and an optional binNumber (8-digit BIN). Installment rates can also be obtained with the amount alone (without a BIN); conversationId is 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 / OK in 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_salt kept 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

Frequently Asked Questions (FAQ)

Should I choose a hosted form or a direct API for virtual POS integration?

Unless you have a business requirement for a branded, fully controlled checkout experience, choose the hosted form. In hosted solutions like PayTR iFrame or iyzico CheckoutForm, card data is collected inside the provider's iframe and never touches your server; this significantly reduces your PCI-DSS scope. A direct API gives full control but substantially increases your PCI burden because card data passes through your infrastructure.

How does the iyzico 3DS flow work?

In iyzico, 3DS is a two-step POST flow. First you POST to the /payment/3dsecure/initialize endpoint with card details and a callbackUrl; the base64 threeDSHtmlContent returned in the response is rendered to redirect the user to the bank's verification screen. After verification, you make a second POST to the auth endpoint with the paymentId returned from Init; iyzico recommends using v2 (/payment/v2/3dsecure/auth). The result is carried by mdStatus: 1 is success, 0 and 2-8 are failures.

How do I make the payment callback secure?

Three rules: verify the signature, be idempotent, respond fast. Never treat an incoming notification as "paid" without verifying its signature (HMAC-SHA256+base64 hash in PayTR, X-IYZ-SIGNATURE-V3 in iyzico). Do not process a second time a notification that arrives again with the same order ID (merchant_oid / conversationId). Push heavy work onto an asynchronous queue and return the expected response quickly: iyzico expects a 2xx, PayTR expects plain text "OK"; if they do not receive it, they retry.

How do I reconcile the payment with the order?

Use a unique order ID generated by the merchant: merchant_oid in PayTR, conversationId in iyzico. This value is returned to you in the callback and lets you bind it to the correct order. Confirm the order based on the signature-verified server-to-server callback, not the browser's "success" page; compare the amount in the callback with the expected amount; close out pending payments with no callback via a background query; and perform daily reconciliation against the provider reports.

What is the difference between refund and cancel in iyzico?

Cancel (/payment/cancel) is called with only paymentId, does not support a partial amount, and cancels the entire payment; according to iyzico it can generally be done the same day and does not create a debit/credit on the card statement. A refund, on the other hand, can be full or partial: /payment/refund works at the line-item level, and /v2/payment/refund at the payment level; a refund is reflected on the statement and can take several days depending on the bank. The exact same-day/value-date rules vary by bank.

Why is the best software company in Sakarya preferred for virtual POS integration?

Partnerfy, a Sakarya-based B2B/white-label software engineering firm, treats virtual POS integration as a security and reconciliation problem: starting from the decision of payment institution or bank POS, hosted form or direct API (based on PCI-DSS scope), it builds and verifies, end to end in a test environment, the 3DS flow, a signature-verified and idempotent callback, amount reconciliation, and the refund/installment scenarios. By offering not a guarantee but discipline and a logged, traceable foundation, it is counted among Sakarya's preferred software companies.