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. Crypto assets may not be used in payments in Turkey, directly or indirectly, under a 2021 TCMB regulation (opens in a new tab) (Turkish).
iyzico API key and sandbox: where credentials live
Register a sandbox account, then read the two keys under Settings, Merchant Settings, API Keys.
| 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 (opens in a new tab) 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.
iyzico API authentication: how the IYZWSv2 header is built
The header is three steps, and the signature covers the exact bytes of the body.
encryptedData = HMACSHA256(randomKey + uri.path + request.body, secretKey), in hex.base64("apiKey:" + apiKey + "&randomKey:" + randomKey + "&signature:" + encryptedData).Authorization: IYZWSv2 <that value>, plusx-iyzi-rndcarrying 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.
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.
| 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.
| 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 (opens in a new tab) 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
fraudStatusis 1, and to wait for a notification when it is 0. - 3-D Secure. An
mdStatusof 1 means authentication succeeded. Complete the payment only then, and sendconversationDataif it was posted. - Response signatures. Each endpoint has its own field order (opens in a new tab), joined with colons, and
10.50must become10.5first. - 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 (opens in a new tab) (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.
| 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 (opens in a new tab), whose
GET /v2/reporting/payment/transactionslists a day's operations. - Not every status is final. The status list also holds values such as
INIT_THREEDSandCALLBACK_THREEDS; treat onlySUCCESSandFAILUREas 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.
- Generate a unique
conversationIdfor each attempt and store it before the call. The page calls it optional and does not say that iyzico enforces uniqueness. - On a timeout, call
POST /payment/detailwithpaymentConversationId. Its page markspaymentIdrequired, yet allowspaymentConversationIdalone; test that lookup in the sandbox. - Save
paymentId, which the page says is needed for cancellation, andtokenon hosted flows.
| 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 (opens in a new tab) 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. For integration work in general, see Fincode's 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 (opens in a new tab)
- iyzico Docs: Sandbox (opens in a new tab)
- iyzico Docs: Live vs Sandbox (opens in a new tab)
- iyzico Docs: Idempotency (opens in a new tab)
- iyzico Docs: Limiters (opens in a new tab)
- iyzico Docs: Webhook (opens in a new tab)
- iyzico Docs: Response Signature Validation (opens in a new tab)
- iyzico Docs: Error Codes (opens in a new tab)
- iyzico Docs: Integration Checklist (opens in a new tab)
- iyzico Docs: Checkout Form retrieve (fraudStatus) (opens in a new tab)
- iyzico Docs: Init 3DS (opens in a new tab)
- iyzico Docs: Retrieve payment (opens in a new tab)
- iyzico on GitHub: iyzipay-python client, version 1.0.46, signing code (opens in a new tab)
- TCMB: E-money institutions register (Turkish) (opens in a new tab)
- PCI Security Standards Council: PCI DSS (opens in a new tab)
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.