# iyzico API: sandbox, HMAC signing and webhooks in Python

URL: https://fincodelabs.com/en/blog/iyzico-api-integration · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Fintech development · Published: 2026-11-06 · Updated: 2026-11-06

iyzico API guide: sandbox keys, IYZWSv2 HMAC-SHA256 signing, X-IYZ-SIGNATURE-V3 webhooks, error 1000 causes and tested Python, from docs read 6 November 2026.

**Short answer:** The **iyzico API** is a JSON REST API at api.iyzipay.com, with a separate sandbox at sandbox-api.iyzipay.com and separate keys for each. Every request is signed: HMAC-SHA256 of the random key, URI path and exact body with your secret key, then base64 of apiKey, randomKey and signature in an Authorization header starting IYZWSv2. Webhooks are verified with the X-IYZ-SIGNATURE-V3 header, which has to be enabled on your account.

This iyzico API integration (*iyzico API entegrasyonu*) guide comes from the iyzico API docs at docs.iyzico.com and iyzico's Python client (version 1.0.46 on GitHub), all read on 6 November 2026. Their last-updated dates run from March 2024 to October 2026, so recheck what you depend on. The code ran with synthetic keys and made no live or sandbox call.

The e-money institution list of the Central Bank of the Republic of Türkiye (TCMB) names the company as İyzi Ödeme ve Elektronik Para Hizmetleri A.Ş., code 864. No recommendation or referral is intended. Who may collect payments is covered in the [Turkey payment gateway guide](https://fincodelabs.com/en/blog/turkey-payment-gateway). Crypto assets may not be used in payments in Turkey, directly or indirectly, under a [2021 TCMB regulation](https://www.resmigazete.gov.tr/eskiler/2021/04/20210416-4.htm) (Turkish).

## iyzico API key and sandbox: where credentials live

Register a sandbox account, then read the two keys under Settings, Merchant Settings, API Keys.

**Environments per the Sandbox, Live vs Sandbox and Integration Checklist pages, read 6 November 2026**

| Point | Sandbox | Live |
| --- | --- | --- |
| Host | `https://sandbox-api.iyzipay.com` | `https://api.iyzipay.com` |
| Keys | `sandbox-` prefix on both | Different pair, no such prefix |
| Access | Self-registration at `sandbox-merchant.iyzipay.com/auth/register` | Apply through your account manager |
| 3-D Secure code | Fixed at 123456 | Real bank flow |
| Cards | Test cards only | Real cards |

The Error Codes page ties error 1001 to keys that do not match the host. [iyzico's test card page](https://docs.iyzico.com/en/add-ons/test-cards) groups cards into success cases, non-Turkish cards and error generators, including 3-D Secure failures. Keys belong in a secret store, as in [API key security](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

## iyzico API authentication: how the IYZWSv2 header is built

The header is three steps, and the signature covers the exact bytes of the body.

1. `encryptedData = HMACSHA256(randomKey + uri.path + request.body, secretKey)`, in hex.
2. `base64("apiKey:" + apiKey + "&randomKey:" + randomKey + "&signature:" + encryptedData)`.
3. `Authorization: IYZWSv2 <that value>`, plus `x-iyzi-rnd` carrying the random key.

> **Worked string with synthetic values.** For random key 123456789, path `/payment/bin/check` and body `{"binNumber":"589004"}`, the signed message is `123456789/payment/bin/check{"binNumber":"589004"}`. A space added to the body changes the signature.

The authentication page leaves three points open:

- **Query strings.** The page says `uri.path`. The official Python client cuts the URL at `?` before signing.
- **Empty bodies.** The page's Postman script signs only random key plus path. The Python client instead sends and signs the text `null`. Test a GET call in the sandbox before you pick one.
- **Serialization.** Sign the exact string you send; never let your HTTP library re-encode the object.

## A tested Python signer and webhook verifier for iyzico

This code builds the header and checks the Direct and HPP webhook formats; it ran on Python 3.11.3 on 6 November 2026 with synthetic keys and no network.

```python
import base64, hashlib, hmac, json, secrets
from decimal import Decimal

API_KEY, SECRET = "sandbox-synthetic-api", "sandbox-synthetic-secret"  # not real keys

def auth_headers(path, body, api_key=API_KEY, secret=SECRET, rnd=None):
    """path has no query string; body is the exact string you will send."""
    rnd = rnd or secrets.token_hex(8)
    sig = hmac.new(secret.encode(), (rnd + path + body).encode(),
                   hashlib.sha256).hexdigest()
    token = f"apiKey:{api_key}&randomKey:{rnd}&signature:{sig}"
    return {"Authorization": "IYZWSv2 " + base64.b64encode(token.encode()).decode(),
            "x-iyzi-rnd": rnd, "Content-Type": "application/json"}

def webhook_signature(event, secret=SECRET):
    """X-IYZ-SIGNATURE-V3: HPP format if the event has a token, else Direct."""
    if not isinstance(event, dict):
        return None
    fields = (["iyziEventType", "iyziPaymentId", "token"] if "token" in event
              else ["iyziEventType", "paymentId"]) + ["paymentConversationId", "status"]
    values = [event.get(f) for f in fields]
    if any(v is None or v == "" for v in values):
        return None
    message = secret + "".join(str(v) for v in values)
    return hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()

def verify_webhook(event, header, secret=SECRET):
    expected = webhook_signature(event, secret)
    return expected is not None and hmac.compare_digest(expected.encode(),
                                                        (header or "").encode())

def strip_zeros(price):     # "10.50" -> "10.5"; read JSON with parse_float=Decimal
    d = Decimal(str(price))
    if not d.is_finite():
        raise ValueError("price must be finite")
    s = format(d, "f")
    return s.rstrip("0").rstrip(".") if "." in s else s

# 1. One dict, two serializations, two signatures.
data = {"locale": "en", "conversationId": "order-1", "buyerName": "Ayşe"}
compact = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
default = json.dumps(data)
for body in (compact, default):
    h = auth_headers("/payment/bin/check", body, rnd="123456789")
    print(base64.b64decode(h["Authorization"].split()[1]).decode()[-16:])

# 2. The decoded header.
print(base64.b64decode(h["Authorization"].split()[1]).decode()[:48])

# 3. Webhook: valid, tampered, no header, unknown shape.
direct = {"iyziEventType": "THREE_DS_AUTH", "paymentId": "1001",
          "paymentConversationId": "order-1", "status": "SUCCESS"}
header = webhook_signature(direct)
print(verify_webhook(direct, header), verify_webhook({**direct, "status": "FAILURE"}, header))
hpp = {"iyziEventType": "CHECKOUT_FORM_AUTH", "iyziPaymentId": 1001, "token": "tok-1",
       "paymentConversationId": "order-2", "status": "SUCCESS"}
print(verify_webhook(hpp, webhook_signature(hpp)), verify_webhook(hpp, None),
      verify_webhook({"iyziEventType": "subscription.order.success"}, header))

# 4. The seven trailing-zero cases from the docs.
print([strip_zeros(p) for p in ("10", "10.0", "10.5", "10.50", "10.510", "10.5105", "10.51050")])
```

Output:

```
4909992472cfc9e1
156b0840f91d9c83
apiKey:sandbox-synthetic-api&randomKey:123456789
True False
True False False
['10', '10', '10.5', '10.5', '10.51', '10.5105', '10.5105']
```

Lines one and two are signature tails: one dictionary serialized two ways signs differently, a route to error 1000. A notification with a missing or empty signed field, such as an unset conversation ID, fails instead of raising `KeyError`, and `strip_zeros` refuses `inf` and `NaN`. The docs' own samples also check out: the authorization string base64-encodes to the sample header, and the response-signature example reproduces the value ending `610173`. No HTTP call was run, since that needs real sandbox keys; only a sandbox call returning `success` proves your header.

## Why iyzico returns Invalid signature (error 1000)

Error 1000 means the hash iyzico computed differs from yours, so compare the exact string you signed with the bytes on the wire.

**Authentication errors from iyzico's Error Codes page (50000 from the Limiters page); the Check column is this guide's reading, not iyzico's text**

| Code | iyzico's message | Check |
| --- | --- | --- |
| 1000 | Invalid signature | Body re-serialized after signing; non-ASCII names such as Ayşe escaped in one version; query string in the signed path; `x-iyzi-rnd` differs from the randomKey you signed; wrong secret |
| 1001 | API details not found | Key does not match the host, or the account is restricted |
| 1003 | Authorization error | Compare with iyzico's sample collection, then ask support |
| 1004 | Random string is mandatory | `x-iyzi-rnd` missing |
| 1006, 1007 | API key, signature mandatory | Empty value in the decoded header |
| 1008, 1009 | Prefix not found, header mandatory | `IYZWSv2` and one space missing, or no Authorization header |
| 50000 | Request Limit Exceeded | See the limits below |

To debug, base64-decode your header and confirm it reads `apiKey:...&randomKey:...&signature:...`, as the third output line shows. iyzico asks you to contact support if one error repeats more than five times in a minute.

## Checkout Form or API: choosing an iyzico payment flow

The Checkout Form keeps card entry on iyzico's page, while API flows put the card form on yours, which changes your PCI DSS position.

**Flows described in the docs read 6 November 2026**

| Flow | Start | How the result arrives | Card data |
| --- | --- | --- | --- |
| Checkout Form | `POST /payment/iyzipos/checkoutform/initialize/auth/ecom` returns `token`, form content and a payment page URL | A token is posted to your `callbackUrl`; then `POST .../checkoutform/auth/ecom/detail` | On iyzico's form |
| API, Non-3DS | `POST /payment/auth` | In the response | Your form, your request |
| API, 3DS | `POST /payment/3dsecure/initialize` returns base64 `threeDSHtmlContent` | Callback with `mdStatus`, then `POST /payment/3dsecure/auth` with `paymentId` and any `conversationData` | Your form, your request |

API requests are signed with your secret key, so they leave your server with the card number inside, and [PCI DSS](https://www.pcisecuritystandards.org/standards/pci-dss/) covers anyone who stores, processes or transmits cardholder data; ask your provider which validation applies to you.

- **Success is not shipping.** The Checkout Form retrieve page says to ship only when `fraudStatus` is 1, and to wait for a notification when it is 0.
- **3-D Secure.** An `mdStatus` of 1 means authentication succeeded. Complete the payment only then, and send `conversationData` if it was posted.
- **Response signatures.** Each endpoint has [its own field order](https://docs.iyzico.com/en/advanced/response-signature-validation), joined with colons, and `10.50` must become `10.5` first.
- **Options.** Currency enums list TRY, USD, EUR, GBP, NOK and CHF. Installment enums conflict: the Non-3DS page lists 1, 2, 3, 6, 9 and 12, while the 3DS enum and the Checkout Form sample add 4, so test the count you need. Turkey's banking regulator, BDDK, also [caps card installments by spending category](https://www.bddk.org.tr/Mevzuat/DokumanGetir/16) (Turkish).
- **Path conflict.** The docs post Checkout Form initialize to `.../initialize/auth/ecom`, and Python client 1.0.46 posts to `.../initialize/ecom`. The path is signed, so confirm yours in the sandbox.

## iyzico webhook: V3 signature, retries and traps

Treat a webhook as a prompt to confirm the payment, because the docs call webhooks optional and delivery stops after three attempts.

**X-IYZ-SIGNATURE-V3: HMAC-SHA256 in hex, keyed with the secret key, over these fields in order**

| Format | Used for | Message |
| --- | --- | --- |
| Direct | Non-3DS, 3DS | secretKey, iyziEventType, paymentId, paymentConversationId, status |
| HPP | Checkout Form, Pay with iyzico | secretKey, iyziEventType, iyziPaymentId, token, paymentConversationId, status |
| Subscription | Recurring orders | merchantId, secretKey, eventType, subscriptionReferenceCode, orderReferenceCode, customerReferenceCode, per the code samples |

- **Documents conflict on subscriptions.** The prose lists the secret key first, the PHP, Java and Node samples put merchantId first, and the payload field is called `iyziEventType`. The two orders give different hashes, so test against a real sandbox notification.
- **Enable it.** The V3 header must be switched on through entegrasyon@iyzico.com, and the page says older headers will no longer be supported. The notification URL must be HTTPS.
- **Timing.** The first notice comes 10 to 15 seconds after the attempt, then every 15 minutes until you answer 2xx, stopping after 3 attempts. An outage of 30 to 45 minutes can lose the event, depending on whether the first delivery counts, so reconcile against the [reporting service](https://docs.iyzico.com/en/advanced/reporting-service), whose `GET /v2/reporting/payment/transactions` lists a day's operations.
- **Not every status is final.** The status list also holds values such as `INIT_THREEDS` and `CALLBACK_THREEDS`; treat only `SUCCESS` and `FAILURE` as final.
- **Replays.** The signed string has no timestamp, so make state changes repeatable: the same conversation ID and status must change nothing twice.

## Idempotency and limits: retrying without a double charge

The docs say most services are non-idempotent and describe no idempotency key, so a timeout must trigger a lookup, never a blind resend.

1. Generate a unique `conversationId` for each attempt and store it before the call. The page calls it optional and does not say that iyzico enforces uniqueness.
2. On a timeout, call `POST /payment/detail` with `paymentConversationId`. Its page marks `paymentId` required, yet allows `paymentConversationId` alone; test that lookup in the sandbox.
3. Save `paymentId`, which the page says is needed for cancellation, and `token` on hosted flows.

**Per-minute limits on the Limiters page (last updated 1 October 2026), read 6 November 2026**

| Service | Endpoint | Limit |
| --- | --- | --- |
| Checkout Form initialize | `/payment/iyzipos/checkoutform/initialize/auth/ecom` | 8,000 |
| Checkout Form retrieve | `/payment/iyzipos/checkoutform/auth/ecom/detail` | 1,000 |
| Refund | `/payment/refund` | 400 |
| Refund V2, Cancel | `/v2/payment/refund`, `/payment/cancel` | 150 each |
| BIN check | `/payment/bin/check` | 50 |

The page gives no limit for `/payment/auth` or `/payment/3dsecure/initialize` and documents no `Retry-After`, so back off on error 50000 and ask your account manager for missing figures.

## Going live, and who can be an iyzico merchant

The Integration Checklist ends with a production application through an account manager, and the pages read do not settle eligibility for a foreign company.

Its webhook step asks for a success response, while the webhook page specifies 2xx, so return 200.

[iyzico's Help Center](https://www.iyzico.com/en/support/help-center) asks companies for signature circulars, a tax plate, a bank document showing the company account and a Findeks risk report. It does not say whether a company without a Turkish tax ID can become a merchant, so ask iyzico in writing before you build. It also says foreign-currency payments are converted at the Central Bank's rate of the payment date and paid out in lira, with no installments. Budget drivers are in [fintech app development cost](https://fincodelabs.com/en/blog/fintech-app-development-cost). For integration work in general, see Fincode's [white-label development](https://fincodelabs.com/en/services/white-label-development) page; naming iyzico here does not mean Fincode integrates it or has built on it.

## Frequently asked questions

### Where do I find my iyzico API key?

In the merchant panel under Settings, Merchant Settings, API Keys. Sandbox and live keys are separate pairs and not interchangeable.

### Does iyzico have a test API?

Yes. The sandbox host is sandbox-api.iyzipay.com, registration is self-service and the docs list test cards; live cards do not work there.

### Does iyzico support idempotency keys?

The docs read on 6 November 2026 describe no such key and say most services are non-idempotent. Look a payment up by paymentConversationId before you resend after a timeout.

### Can a company outside Turkey use iyzico?

The pages read do not say. They list Turkish company documents such as a tax plate and signatory circular, so ask iyzico in writing.

## Sources

- [iyzico Docs: HMACSHA256 Auth](https://docs.iyzico.com/en/getting-started/preliminaries/authentication/hmacsha256-auth)
- [iyzico Docs: Sandbox](https://docs.iyzico.com/en/getting-started/preliminaries/sandbox)
- [iyzico Docs: Live vs Sandbox](https://docs.iyzico.com/en/getting-started/preliminaries/live-vs-sandbox)
- [iyzico Docs: Idempotency](https://docs.iyzico.com/en/getting-started/preliminaries/idempotency)
- [iyzico Docs: Limiters](https://docs.iyzico.com/en/getting-started/preliminaries/limiters)
- [iyzico Docs: Webhook](https://docs.iyzico.com/en/advanced/webhook)
- [iyzico Docs: Response Signature Validation](https://docs.iyzico.com/en/advanced/response-signature-validation)
- [iyzico Docs: Error Codes](https://docs.iyzico.com/en/add-ons/error-codes)
- [iyzico Docs: Integration Checklist](https://docs.iyzico.com/en/add-ons/integration-checklist)
- [iyzico Docs: Checkout Form retrieve (fraudStatus)](https://docs.iyzico.com/en/payment-methods/checkoutform/cf-implementation/cf-retrieve)
- [iyzico Docs: Init 3DS](https://docs.iyzico.com/en/payment-methods/api/3ds/3ds-implementation/init-3ds)
- [iyzico Docs: Retrieve payment](https://docs.iyzico.com/en/payment-methods/api/non-3ds/non-3ds-implementation/retrieve)
- [iyzico on GitHub: iyzipay-python client, version 1.0.46, signing code](https://github.com/iyzico/iyzipay-python/blob/master/iyzipay/iyzipay_resource.py)
- [TCMB: E-money institutions register (Turkish)](https://www.tcmb.gov.tr/wps/wcm/connect/TR/TCMB+TR/Main+Menu/Temel+Faaliyetler/Odeme+Hizmetleri/Elektronik+Para+Kuruluslari)
- [PCI Security Standards Council: PCI DSS](https://www.pcisecuritystandards.org/standards/pci-dss/)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/iyzico-api-integration

Cite as: Fincode, "iyzico API: sandbox, HMAC signing and webhooks in Python", updated November 6, 2026, https://fincodelabs.com/en/blog/iyzico-api-integration
