GELİŞTİRİCİLER

Direct API (v4)

Kartı kendi ödeme formunuzda toplayın ve sunucunuzdan Manuell adresine gönderin. Müşteri ödemeyi 3-D Secure ile onaylar, sonucu webhook ile alırsınız.

  • Temel URLhttps://www.manuellsolutions.com/api/v4
  • API sürümüv4

Genel bakışla başlayın

Ortam

Gerçek para hareketi olmadan test edin.

Gerçek ödemeler. Canlı anahtar gerekir.

Genel bakış

Sunucunuz Manuell ile doğrudan konuşur: kartın hangi akışa girdiğini sorar, kart bilgileriyle siparişi oluşturur ve sipariş ödenene ya da başarısız olana kadar izler.

Temel URL
https://www.manuellsolutions.com/api/v4
Para birimi
Yalnızca TRY. Sipariş başına tek ödeme, taksit yok.
Tutarlar
Ondalık 2 haneli dizgiler, örn. "1500.00".
Ortam
sandbox ya da live; X-CEK-Environment başlığıyla gönderilir. Anahtarınızın ortamıyla aynı olmalıdır.
Hatalar
application/problem+json (RFC 7807) ve 7 haneli code.
Hız sınırı
Anahtar başına dakikada 300 istek (kimlik doğrulamadan önce IP başına dakikada 600). Aşılırsa Retry-After ile 429.
Tekrar koruması
Idempotency-Key yoktur. Tekrarlanan siparişlere karşı merchant_order_id koruma sağlar.
  1. BIN sorgulayın

    Kartın ilk 6 hanesini ve tutarı GET /api/v4/bins/{bin} ile gönderin. payment_mode, kartı kendinizin mi toplayacağınızı (direct) yoksa müşteriyi barındırılan sayfaya mı yönlendireceğinizi (hosted) söyler.

  2. Siparişi oluşturun

    POST /api/v4/orders isteğini kartla (direct) ya da yalnızca BIN ile (hosted) gönderin. Sipariş requires_action, requires_hosted, pending veya failed durumuyla döner.

  3. Tamamlayın ve doğrulayın

    Müşteriyi redirect_url (3-D Secure) ya da hosted_url adresine yönlendirin. Kesin sonuç webhook ile gelir; gecikirse siparişi GET /api/v4/orders/{merchantOrderId} ile okuyun.

Panelde hazırlık

  • “API anahtarı ve entegrasyon” sayfasında her ortam için bir API anahtarı ve gizli anahtar üretin. Gizli anahtarı sunucunuzda saklayın.
  • Sunucularınızın genel IP adreslerini ortamın IP izin listesine ekleyin. Liste boşken her çağrı 0055501 kodlu 403 ile reddedilir.
  • HTTPS bir webhook adresi tanımlayın. Ana bilgisayar adı herkese açık olmalıdır.
  • Başarı ve hata dönüş adreslerini tanımlayın (canlıda HTTPS). Tanımlı değilse müşteri ödemeden sonra sonuç ekranında kalır.
  • Üye iş yeri kodunuz MRC- ve ardından 6 haneli hesap numaranızdır, örneğin MRC-123456.

Kimlik doğrulama

Her çağrı X-CEK-* başlıkları taşır. İmza; zaman damgasını, yöntemi, yolu ve gövde özetini birleştiren bir metnin HMAC-SHA256 değeridir.

Zorunlu başlıklar

BaşlıkAçıklama
X-CEK-Api-KeyOrtamın API anahtarı.
X-CEK-Environmentsandbox ya da live. Anahtarın ortamıyla aynı olmalı.
X-CEK-Merchant-CodeÜye iş yeri kodunuz, örn. MRC-123456. Sandbox'ta zorunlu; canlıda gönderirseniz hesabınızla eşleşmeli.
X-CEK-TimestampGeçerli Unix zamanı, saniye cinsinden (10 hane). 300 saniyeden fazla sapma reddedilir.
X-CEK-Signature64 küçük harfli hex karakter; aşağıda anlatıldığı gibi hesaplanır.
Content-TypePOST isteklerinde application/json.

Bir istek nasıl imzalanır

  1. Gövdeyi gönderdiğiniz haliyle alın ve sha256(body) değerini küçük harfli hex olarak hesaplayın. Gövdesiz istekte boş dizenin özetini kullanın.

  2. Zaman damgasını, büyük harfli yöntemi, yolu ve gövde özetini satır sonlarıyla (\n) birleştirin. Yol; ana bilgisayar adı ve sorgu dizesi olmayan tam yoldur, örn. /api/v4/orders.

  3. Bu metnin HMAC-SHA256 değerini API gizli anahtarınızı anahtar olarak kullanarak hesaplayın ve 64 küçük harfli hex karakter olarak yazın.

  4. Diğer başlıklarla birlikte X-CEK-Signature olarak gönderin.

Kanonik imza metni

string_to_sign = X-CEK-Timestamp + "\n" + METHOD + "\n" + path + "\n" + hex(sha256(body))
X-CEK-Signature = hex(HMAC-SHA256(key = API_SECRET, message = string_to_sign))  // 64 lowercase hex characters

Gönderdiğiniz baytların aynısını imzalayın: JSON'u bir kez serileştirin ve aynı dizgiyi hem hash için hem de istek gövdesi olarak kullanın.

Örnek (sahte bilgilerle)

Tüm değerler uydurmadır: gizli anahtar sk_test_EXAMPLE, zaman damgası 1790000000. Kendi panelinizdeki değerleri kullanın.

İstek gövdesi

{"merchant_order_id":"ORDER-1001","amount":"1500.00","bin":"540061"}

sha256(body)

3bb9ee51caacfb41bd7a8774f12ff861f97b1fd8cf416a00b5fa92618e8390e7

string_to_sign

1790000000
POST
/api/v4/orders
3bb9ee51caacfb41bd7a8774f12ff861f97b1fd8cf416a00b5fa92618e8390e7

X-CEK-Signature

f721cbcccb6ed8a249ed0e331133686fe5f9dfb227856bbd1b6ff9e0b2a8f348

Örnek kod

Her örnek sandbox'ta bir BIN sorgular ve sipariş oluşturur. Kimlik bilgileri ortam değişkenlerinden okunur; gizli anahtarı koda yazmayın. Her çalıştırma yeni bir merchant_order_id kullanır.

#!/usr/bin/env bash
# export MERCHANT_API_KEY=... MERCHANT_API_SECRET=... MERCHANT_CODE=MRC-123456   (sandbox key)
set -euo pipefail

BASE_URL="https://www.manuellsolutions.com"

signed() {  # signed METHOD PATH [BODY]
  local method="$1" path="$2" body="${3:-}" timestamp sha string_to_sign signature
  timestamp="$(date +%s)"
  sha="$(printf '%s' "$body" | openssl dgst -sha256 -hex | awk '{print $NF}')"
  string_to_sign="$(printf '%s\n%s\n%s\n%s' "$timestamp" "$method" "${path%%\?*}" "$sha")"
  # the secret travels in the environment, not on the command line
  signature="$(printf '%s' "$string_to_sign" | python3 -c 'import hashlib,hmac,os,sys; print(hmac.new(os.environ["MERCHANT_API_SECRET"].encode(), sys.stdin.buffer.read(), hashlib.sha256).hexdigest())')"
  local args=(--silent --show-error --request "$method" "$BASE_URL$path"
    --header "X-CEK-Api-Key: $MERCHANT_API_KEY"
    --header "X-CEK-Environment: sandbox"
    --header "X-CEK-Merchant-Code: $MERCHANT_CODE"
    --header "X-CEK-Timestamp: $timestamp"
    --header "X-CEK-Signature: $signature")
  if [ -n "$body" ]; then args+=(--header "Content-Type: application/json" --data-binary "$body"); fi
  curl "${args[@]}"
  echo
}

# 1) BIN lookup: no body, so the hash is sha256("")
signed GET "/api/v4/bins/435509?amount=1500.00"

# 2) create an order; a new merchant_order_id for every order
ORDER_ID="ORDER-$(date +%s)-$(openssl rand -hex 4)"
signed POST "/api/v4/orders" "{\"merchant_order_id\":\"$ORDER_ID\",\"amount\":\"1500.00\",\"card\":{\"holder_name\":\"TEST CUSTOMER\",\"number\":\"4355090000044242\",\"expiry_month\":\"12\",\"expiry_year\":\"30\",\"cvv\":\"123\"}}"
<?php
$config = [
    'base'   => 'https://www.manuellsolutions.com',
    'key'    => getenv('MERCHANT_API_KEY'),
    'secret' => getenv('MERCHANT_API_SECRET'),
    'code'   => getenv('MERCHANT_CODE'),
    'env'    => 'sandbox',
];

function signedRequest(array $config, string $method, string $path, ?array $payload = null): array
{
    $method = strtoupper($method);
    $signingPath = explode('?', $path, 2)[0];
    // serialise once: the same bytes are hashed and sent
    $body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $timestamp = (string) time();
    $stringToSign = implode("\n", [$timestamp, $method, $signingPath, hash('sha256', $body)]);
    $signature = hash_hmac('sha256', $stringToSign, $config['secret']);

    $headers = [
        'X-CEK-Api-Key: ' . $config['key'],
        'X-CEK-Environment: ' . $config['env'],
        'X-CEK-Merchant-Code: ' . $config['code'],
        'X-CEK-Timestamp: ' . $timestamp,
        'X-CEK-Signature: ' . $signature,
    ];
    if ($body !== '') {
        $headers[] = 'Content-Type: application/json';
    }

    $ch = curl_init($config['base'] . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 35, // an order can take up to 30 seconds
    ]);
    if ($body !== '') {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($response === false) {
        throw new RuntimeException('No response; repeat the same request or read the order with GET.');
    }

    return [$status, json_decode((string) $response, true)];
}

// 1) BIN lookup: no body, so the hash is sha256("")
[$status, $bin] = signedRequest($config, 'GET', '/api/v4/bins/435509?amount=1500.00');
echo $status, ' ', $bin['data']['payment_mode'] ?? '', PHP_EOL;

// 2) create an order; a new merchant_order_id for every order
[$status, $order] = signedRequest($config, 'POST', '/api/v4/orders', [
    'merchant_order_id' => 'ORDER-' . time() . '-' . bin2hex(random_bytes(4)),
    'amount' => '1500.00',
    'card' => ['holder_name' => 'TEST CUSTOMER', 'number' => '4355090000044242', 'expiry_month' => '12', 'expiry_year' => '30', 'cvv' => '123'],
]);
echo $status, ' ', $order['data']['status'] ?? '', PHP_EOL;
const crypto = require("node:crypto");

const BASE_URL = "https://www.manuellsolutions.com";
const { MERCHANT_API_KEY, MERCHANT_API_SECRET, MERCHANT_CODE } = process.env;

async function signedRequest(method, path, payload) {
  method = method.toUpperCase();
  const signingPath = path.split("?", 1)[0];
  // serialise once: the same bytes are hashed and sent
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const timestamp = String(Math.floor(Date.now() / 1000));
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const stringToSign = [timestamp, method, signingPath, bodyHash].join("\n");
  const signature = crypto.createHmac("sha256", MERCHANT_API_SECRET).update(stringToSign).digest("hex");

  const headers = {
    "X-CEK-Api-Key": MERCHANT_API_KEY,
    "X-CEK-Environment": "sandbox",
    "X-CEK-Merchant-Code": MERCHANT_CODE,
    "X-CEK-Timestamp": timestamp,
    "X-CEK-Signature": signature,
  };
  if (body !== "") headers["Content-Type"] = "application/json";

  // an order can take up to 30 seconds
  const res = await fetch(BASE_URL + path, { method, headers, body: body === "" ? undefined : body, signal: AbortSignal.timeout(35000) });
  return { status: res.status, body: JSON.parse(await res.text()) };
}

(async () => {
  // 1) BIN lookup: no body, so the hash is sha256("")
  const bin = await signedRequest("GET", "/api/v4/bins/435509?amount=1500.00");
  console.log(bin.status, bin.body.data?.payment_mode);

  // 2) create an order; a new merchant_order_id for every order
  const order = await signedRequest("POST", "/api/v4/orders", {
    merchant_order_id: `ORDER-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`,
    amount: "1500.00",
    card: { holder_name: "TEST CUSTOMER", number: "4355090000044242", expiry_month: "12", expiry_year: "30", cvv: "123" },
  });
  console.log(order.status, order.body.data?.status);
})();
import hashlib
import hmac
import json
import os
import time
import urllib.error
import urllib.request
import uuid

BASE_URL = "https://www.manuellsolutions.com"
API_KEY = os.environ["MERCHANT_API_KEY"]
API_SECRET = os.environ["MERCHANT_API_SECRET"]
MERCHANT_CODE = os.environ["MERCHANT_CODE"]


def signed_request(method, path, payload=None):
    method = method.upper()
    signing_path = path.split("?", 1)[0]
    # serialise once: the same bytes are hashed and sent
    body = json.dumps(payload, separators=(",", ":")) if payload is not None else ""
    timestamp = str(int(time.time()))
    string_to_sign = "\n".join([timestamp, method, signing_path, hashlib.sha256(body.encode()).hexdigest()])
    signature = hmac.new(API_SECRET.encode(), string_to_sign.encode(), hashlib.sha256).hexdigest()

    headers = {
        "X-CEK-Api-Key": API_KEY,
        "X-CEK-Environment": "sandbox",
        "X-CEK-Merchant-Code": MERCHANT_CODE,
        "X-CEK-Timestamp": timestamp,
        "X-CEK-Signature": signature,
    }
    if body:
        headers["Content-Type"] = "application/json"

    request = urllib.request.Request(BASE_URL + path, data=body.encode() or None, headers=headers, method=method)
    try:
        with urllib.request.urlopen(request, timeout=35) as response:  # an order can take up to 30 seconds
            return response.status, json.loads(response.read())
    except urllib.error.HTTPError as error:  # problem+json body
        return error.code, json.loads(error.read())


# 1) BIN lookup: no body, so the hash is sha256("")
status, bin_info = signed_request("GET", "/api/v4/bins/435509?amount=1500.00")
print(status, bin_info.get("data", {}).get("payment_mode"))

# 2) create an order; a new merchant_order_id for every order
status, order = signed_request("POST", "/api/v4/orders", {
    "merchant_order_id": f"ORDER-{int(time.time())}-{uuid.uuid4().hex[:8]}",
    "amount": "1500.00",
    "card": {"holder_name": "TEST CUSTOMER", "number": "4355090000044242", "expiry_month": "12", "expiry_year": "30", "cvv": "123"},
})
print(status, order.get("data", {}).get("status"))

BIN sorgulama

GET/api/v4/bins/{bin}

Kart BIN'i sorgulayın

Müşteri ilk 6 haneyi yazar yazmaz çağırın. Yanıt, kendi kart formunuz ile barındırılan sayfa arasında seçim yapmanıza yardım eden anlık bir görüntüdür; kesin karar sipariş oluşturulurken verilir.

Parametreler

AlanTürZorunluAçıklama
binstring · pathZorunluKart numarasının ilk altı hanesi.
amountstring · queryZorunluTRY cinsinden sipariş tutarı, 2 ondalıklı. Uygunluk bu tutara göre değerlendirilir.

Yanıt · 200

AlanTürAçıklama
binstringSorguladığınız BIN.
bank_namestring | nullKartı çıkaran banka; bilinmiyorsa null.
networkstring | nullKart ağı: visa, mastercard, amex, troy ya da null.
card_typestring | nullcredit, debit, prepaid ya da null.
card_programstring | nullKart programı adı ya da null.
countrystring | nullÇıkaran ülke (ISO alfa-2) ya da null.
domesticbooleanTürkiye'de çıkarılmış kart için true.
payment_modestringdirect: kartı kendiniz toplayın. hosted: barındırılan sayfayı kullanın.

Yanıt · Örnek

{
  "data": {
    "bin": "435509",
    "bank_name": "Example Bank",
    "network": "visa",
    "card_type": "credit",
    "card_program": null,
    "country": "TR",
    "domestic": true,
    "payment_mode": "direct"
  }
}
  • direct ise tüm kart bilgilerini toplayın ve siparişi card ile oluşturun.
  • hosted ise siparişi yalnızca bin ile oluşturun ve müşteriyi dönen hosted_url adresine yönlendirin.

Olası hatalar

Sipariş oluşturma

POST/api/v4/orders

Sipariş oluşturun

Tek bir uç iki kipte de sipariş oluşturur. Gövde belirler: direct sipariş için card, hosted sipariş için yalnızca bin gönderin. İkisini birlikte göndermek 422 döndürür.

Direct sipariş (kartla)

AlanTürZorunluAçıklama
merchant_order_idstringZorunluSipariş referansınız: A-Z a-z 0-9 . _ - karakterlerinden 1–64 karakter; hesap ve ortam başına tekil.
amountstringZorunluTRY cinsinden tutar, 2 ondalıklı, örn. "1500.00".
card.holder_namestringZorunluKart üzerindeki ad, 2–120 karakter.
card.numberstringZorunluKart numarası, 13–19 hane, yalnızca rakam.
card.expiry_monthstringZorunluSon kullanma ayı, "01" – "12".
card.expiry_yearstringZorunluSon kullanma yılı, 2 hane, örn. "30".
card.cvvstringZorunluGüvenlik kodu, 3 veya 4 hane.

İstek gövdesi · Örnek

{
  "merchant_order_id": "ORDER-1001",
  "amount": "1500.00",
  "card": {
    "holder_name": "TEST CUSTOMER",
    "number": "4355090000044242",
    "expiry_month": "12",
    "expiry_year": "30",
    "cvv": "123"
  }
}

Olası hatalar

Hosted sipariş (yalnızca BIN)

AlanTürZorunluAçıklama
merchant_order_idstringZorunluSipariş referansınız: A-Z a-z 0-9 . _ - karakterlerinden 1–64 karakter; hesap ve ortam başına tekil.
amountstringZorunluTRY cinsinden tutar, 2 ondalıklı, örn. "1500.00".
binstringZorunluKart numarasının ilk altı hanesi.

İstek gövdesi · Örnek

{
  "merchant_order_id": "ORDER-1002",
  "amount": "1500.00",
  "bin": "540061"
}

Olası hatalar

Kurallar

  • merchant_order_id hesap ve ortam başına tekil olmalıdır. Tekrarlara karşı tek koruma budur.
  • Aynı merchant_order_id ve aynı içerikle yinelenen istek yeni ödeme başlatmaz, siparişi o anki haliyle döndürür. İçerik farklıysa 0055503 kodlu 409 döner.
  • Şemada olmayan bir alan 1000001 kodlu 422 döndürür.
  • 4xx yanıtında ödeme alınmamıştır. Sonraki deneme için yeni bir merchant_order_id kullanın. failed sipariş de kesindir.
  • 5xx yanıtı veya zaman aşımından sonra ödeme başlamış olabilir. Yeni sipariş açmayın: aynı isteği aynen tekrarlayın ya da siparişi GET ile okuyun. Zaman aşımı süresini en az 30 saniye tutun.
  • BIN o anda hosted ise card içeren istek de requires_hosted döndürür ve kart bilgileri kullanılmaz.

Sipariş nesnesi

Oluşturma ve okuma çağrıları siparişi data içinde döndürür.

AlanTürAçıklama
order_codestringSipariş numarası, metin olarak 9 hane. Dönüş adresinde gelen değer budur.
merchant_order_idstringSizin merchant_order_id değeriniz.
statusstringrequires_action, requires_hosted, pending, paid ya da failed.
amountstringSipariş tutarı, 2 ondalık.
currencystringHer zaman TRY.
redirect_urlstring | null3-D Secure adresi. Durum requires_action iken dolu.
hosted_urlstring | nullBarındırılan sayfa adresi. Durum requires_hosted iken dolu.
hosted_expires_atstring | nullhosted_url adresinin geçerliliğinin bittiği an (ISO 8601, UTC).
failure_reason_codestring | nullfailed sipariş için ret sebebi kodu; bkz. Ret sebepleri. Ödeme hiç denenmediyse null olabilir.
failure_messagestring | nullSebep için müşteriye gösterilebilecek kısa metin. Her zaman Türkçedir; kod üzerinden dallanın.
cardobject | nullKart özeti (holder_name, bin, last_four, network, program, issuer_bank_name). direct siparişte hemen, hosted siparişte sonuç belli olunca dolar.
created_atstringOluşturulma zamanı (ISO 8601, +00:00).
completed_atstring | nullTamamlanma zamanı; sipariş açıkken null.

Yanıt · Örnek

{
  "data": {
    "order_code": "123456789",
    "merchant_order_id": "ORDER-1001",
    "status": "requires_action",
    "amount": "1500.00",
    "currency": "TRY",
    "redirect_url": "https://bank.example.com/3ds/123456789",
    "hosted_url": null,
    "hosted_expires_at": null,
    "failure_reason_code": null,
    "failure_message": null,
    "card": {
      "holder_name": "TEST CUSTOMER",
      "bin": "435509",
      "last_four": "4242",
      "network": "visa",
      "program": null,
      "issuer_bank_name": "Example Bank"
    },
    "created_at": "2026-10-13T10:15:02+00:00",
    "completed_at": null
  }
}

requires_hosted

{
  "data": {
    "order_code": "123456789",
    "merchant_order_id": "ORDER-1001",
    "status": "requires_hosted",
    "amount": "1500.00",
    "currency": "TRY",
    "redirect_url": null,
    "hosted_url": "https://pay.example.com/h/123456789",
    "hosted_expires_at": "2026-10-13T10:30:02+00:00",
    "failure_reason_code": null,
    "failure_message": null,
    "card": null,
    "created_at": "2026-10-13T10:15:02+00:00",
    "completed_at": null
  }
}

Durumlar ve süreler

AlanKesin mi?Açıklama
requires_actionHayırMüşteriyi 3-D Secure için redirect_url adresine yönlendirin.
requires_hostedHayırMüşteriyi hosted_url adresine yönlendirin.
pendingHayırBanka henüz yanıt vermedi ya da müşteri ekrandan ayrıldı. Webhook'u bekleyin.
paidEvetÖdeme tahsil edildi.
failedEvetÖdeme gerçekleşmedi. Nedeni failure_reason_code alanında.

Müşterinin 3-D Secure için 10 dakikası vardır. Barındırılan sayfa hosted_expires_at anına kadar çalışır. Bankanın hiç yanıt vermediği ödeme en geç 24 saat içinde paid veya failed olur; süresi dolan ödeme failed olur.

Nadiren, failed bir sipariş sonradan paid olabilir. paid durumunu esas alın.

Sipariş sorgulama

GET/api/v4/orders/{merchantOrderId}

Siparişi sorgulayın

Oluşturma çağrısıyla aynı nesneyi döndürür; istek gövdesi yoktur. redirect_url 3-D Secure süresi bitene, hosted_url hosted_expires_at anına kadar döner.

Parametreler

AlanTürZorunluAçıklama
merchantOrderIdstring · pathZorunluSiparişi oluştururken kullandığınız merchant_order_id.

Asıl kanal webhook'tur. Gecikirse siparişi en sık 30 saniyede bir okuyun.

Olası hatalar

3-D Secure ve barındırılan sayfa

Sipariş oluşturulduktan sonra müşteri ödemeyi sizin olmayan bir sayfada tamamlar. Onu normal bir tam sayfa yönlendirmesiyle oraya gönderin.

3-D Secure (`requires_action`)

  1. Müşterinin tarayıcısını redirect_url adresine yönlendirin.

  2. Müşteri ödemeyi bankasında onaylar.

  3. Ödeme kesinleşince müşteri dönüş adresinize yönlendirilir ve webhook size ulaşır.

Barındırılan sayfa (`requires_hosted`)

  1. Müşteriyi hosted_url adresine yönlendirin. Tam sayfa yönlendirme her zaman çalışır; bir modal çerçevede göstermek sayfanın iznine bağlıdır, bu yüzden yönlendirmeyi yedek olarak tutun.

  2. Müşteri kartı girer ve 3-D Secure'u o sayfada tamamlar. Başarısız deneme siparişi kapatmaz; müşteri orada yeniden deneyebilir.

  3. Müşteri sayfayı kapatırsa aynı hosted_url adresi hosted_expires_at anına kadar yeniden açar.

  4. Ödeme bitince sayfanın tamamı dönüş adresinize yönlendirilir. Sonucu webhook ile doğrulayın.

Dönüş adresi

Sipariş paid ya da failed olunca müşteri, panelde tanımladığınız ilgili dönüş adresine yönlendirilir. Webhook adresi tanımlıysa adrese aşağıdaki imzalı sorgu parametreleri eklenir.

AlanTürAçıklama
order_codestring9 haneli sipariş numarası. Adreste merchant_order_id yoktur; siparişi bu değerle eşleştirin.
statusstringpaid ya da failed.
failure_codestringRet sebebi kodu; yalnızca sebebi olan failed siparişte bulunur. Aksi halde parametre yoktur.
tsintegerAdresin oluşturulduğu Unix zamanı (saniye).
key_versionintegerGizli anahtarınızın sürümü. Gizli anahtarı her yenilediğinizde artar.
sigstringİmza, 64 küçük harfli hex karakter.

Kanonik imza metni

sig = hex(HMAC-SHA256(key = API_SECRET, message = ts + "." + order_code + "." + status + "." + failure_code))
  • Parametrelere güvenmeden önce sig değerini doğrulayın. failure_code yoksa yerine boş dize koyun; nokta kalır.
  • 10 dakikadan eski bir ts değerini reddedin.
  • Dönüş yalnızca müşterinin nerede olduğunu söyler. Sonuç, webhook geldiğinde ya da GET /api/v4/orders/{merchantOrderId} öyle dediğinde kesinleşir.
  • Webhook adresi tanımlı değilse adres bu parametreler olmadan açılır.

Örnek

https://shop.example.com/payment/success?order_code=123456789&status=failed&failure_code=insufficient_funds&ts=1790000000&key_version=1&sig=a6d0ee5e1774b836d04836d4046fff37e719904d9fea69e28a558e619743c4c2

Webhook

Sonuçlar, ortamın HTTPS webhook adresine imzalı JSON POST istekleriyle iletilir.

OlayNe zaman gönderilir
checkout.order.succeededSipariş paid oldu.
checkout.order.failedSipariş failed oldu.

Başlık

BaşlıkAçıklama
X-CEK-TimestampUnix zamanı (saniye). 300 saniyeden fazla sapmayı reddedin.
X-CEK-Signatureİmza: API gizli anahtarınızla, zaman damgası + nokta + ham gövde üzerinden hesaplanan HMAC-SHA256 değerinin hex'i.
X-CEK-Event-IdOlayın UUID değeri. Gövdedeki event_id ile aynıdır ve yeniden denemelerde değişmez.
X-CEK-Environmentsandbox ya da live.

Her bildirimi doğrulayın

  1. JSON'u ayrıştırmadan önce ham gövdeyi okuyun.

  2. İmzayı X-CEK-Timestamp, nokta ve ham gövde üzerinden hesaplayın ve X-CEK-Signature ile sabit zamanlı karşılaştırın.

  3. Zaman damgası saatinizden 300 saniyeden fazla sapıyorsa isteği reddedin.

  4. X-CEK-Event-Id ile tekilleştirin. Teslimat en az bir kezdir ve sırasız olabilir. Bir sipariş için checkout.order.succeeded gördüyseniz o sipariş için gelen checkout.order.failed bildirimlerini yok sayın.

  5. 10 saniye içinde (bağlantı için 3 saniye) herhangi bir 2xx ile yanıtlayın. Yönlendirme (3xx), 4xx, 5xx ve zaman aşımı başarısız sayılır ve yeniden denenir.

İmza doğrulama örnekleri

<?php
// Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>".
function isValidDirectWebhook(string $rawBody, string $timestamp, string $signature, string $secret): bool
{
    if (preg_match('/^[0-9]{10}$/D', $timestamp) !== 1 || preg_match('/^[0-9a-f]{64}$/D', $signature) !== 1
        || abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    return hash_equals(hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret), $signature);
}

$rawBody = file_get_contents('php://input');
if (!isValidDirectWebhook($rawBody, $_SERVER['HTTP_X_CEK_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_CEK_SIGNATURE'] ?? '', getenv('MERCHANT_API_SECRET'))) {
    http_response_code(401);
    exit;
}
// Deduplicate with X-CEK-Event-Id, save the event, then answer 2xx.
http_response_code(204);
const crypto = require("node:crypto");

// Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>".
// rawBody must be the untouched request bytes (Buffer), not re-serialised JSON.
function isValidDirectWebhook(rawBody, timestamp, signature, secret) {
  if (!/^[0-9]{10}$/.test(timestamp) || !/^[0-9a-f]{64}$/.test(signature)
      || Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(timestamp + "." + rawBody.toString("utf8")).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Then deduplicate with X-CEK-Event-Id, save the event and answer 2xx.
module.exports = { isValidDirectWebhook };
import hashlib
import hmac
import re
import time


def is_valid_direct_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    """Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>"."""
    if (not re.fullmatch(r"[0-9]{10}", timestamp) or not re.fullmatch(r"[0-9a-f]{64}", signature)
            or abs(int(time.time()) - int(timestamp)) > 300):
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


# Then deduplicate with X-CEK-Event-Id, save the event and answer 2xx.

Yeniden denemeler

Başarısız teslimat, önceki başarısızlıktan itibaren aşağıdaki beklemelerden sonra tekrarlanır; toplam en çok 11 deneme yapılır.

DenemeÖncesinde bekleme
1—
260 s (1 min)
3300 s (5 min)
4900 s (15 min)
51800 s (30 min)
63600 s (1 h)
710800 s (3 h)
821600 s (6 h)
943200 s (12 h)
1086400 s (24 h)
11172800 s (48 h)

Gövde

Gövde schema_version (3), event_id, event_type, bir order nesnesi, bir merchant nesnesi ve payment_details.card içinde kart özetini taşır. failed olayı ayrıca failure_code, failure_reason_code ve failure_message ekler. Kullanmadığınız alanları yok sayın; yeni alanlar eklenebilir.

İstek gövdesi · Örnek

{
  "schema_version": 3,
  "event_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6",
  "event_type": "checkout.order.failed",
  "order": {
    "code": "123456789",
    "merchant_order_id": "ORDER-1001",
    "status": "failed",
    "amount": "1500.00",
    "currency": "TRY"
  },
  "merchant": {
    "code": "MRC-123456"
  },
  "payment_details": {
    "card": {
      "holder_name": "TEST CUSTOMER",
      "bin": "435509",
      "last_four": "4242",
      "network": "visa",
      "program": null,
      "issuer_bank_code": null,
      "issuer_bank_name": "Example Bank",
      "installment_count": 1,
      "three_d": true
    }
  },
  "failure_code": "payment_declined",
  "failure_reason_code": "insufficient_funds",
  "failure_message": "Kart bakiyeniz veya limitiniz bu ödeme için yeterli değil."
}

Ret sebepleri

failure_reason_code bu kodlardan biridir. Mantığınızı kod üzerine kurun; failure_message müşteriye gösterilen Türkçe bir metindir.

KodAçıklama
payment_declinedKartı çıkaran banka ödemeyi onaylamadı.
do_not_honorBanka belirli bir sebep göstermeden reddetti.
insufficient_fundsKartın bakiyesi veya limiti bu ödeme için yetersiz.
card_limit_reachedKartın işlem limiti doldu.
card_details_invalidKart numarası kabul edilmedi.
card_expiry_invalidSon kullanma tarihi geçersiz.
card_security_code_invalidGüvenlik kodu (CVV) hatalı.
card_not_activeKart ödemeye açık değil.
card_brand_not_supportedBu kart markası bu ödeme için desteklenmiyor.
three_ds_failed3-D Secure doğrulaması tamamlanamadı.
security_declinedBir güvenlik kontrolü ödemeyi durdurdu. Müşteriye yalnızca bankanın onaylamadığını söyleyin.
payment_not_permittedBu kartla bu ödeme yapılamıyor.
amount_not_allowedTutar, bu kartın kabul ettiği aralığın dışında.
currency_not_supportedPara birimi bu ödeme için desteklenmiyor.
payment_details_rejectedÖdeme bilgileri kabul edilmedi.
duplicate_paymentAynı ödeme daha önce alınmış görünüyor.
cancelled_by_customerMüşteri ödeme bitmeden iptal etti.
payment_timed_outÖdeme süresi doldu.
bank_unavailableBankaya şu an ulaşılamıyor. Kısa süre sonra yeniden deneyin.
bank_requested_retryBanka ödemenin tekrarlanmasını istedi.
provider_unavailableÖdeme şu anda tamamlanamadı. Kısa süre sonra yeniden deneyin.

Liste genişleyebilir. Tanımadığınız bir kodu payment_declined gibi ele alın. Webhook'ta security_declined, failure_code alanında payment_declined olarak bildirilir.

Hata kodları

Hatalar application/problem+json biçimindedir. code üzerinden dallanın; title ve detail insanlar içindir.

AlanTürAçıklama
type, title, status, detailstring · integerStandart problem alanları.
instancestringİsteğin yolu.
codestringAşağıdaki tablodaki 7 haneli hata kodu.
correlation_idstringDestekle yazışırken belirtin. X-Correlation-Id başlığında da gelir.
metadataobjectdeveloper_detail, retry_class ve audit_event: hata ayıklama için ek bilgi.
amount_limitobjectYalnızca 0055235'te: izin verilen tutarın min ve max değerleri; hacim limitinde null.
errorsobjectYalnızca 1000001'de: doğrulamadan geçemeyen alanlar.

application/problem+json

{
  "type": "https://www.manuellsolutions.com/problems/0055233",
  "title": "BIN not recognised",
  "status": 409,
  "detail": "No card range matches this BIN.",
  "instance": "/api/v4/bins/999999",
  "code": "0055233",
  "correlation_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6",
  "metadata": {
    "developer_detail": "bin_not_found",
    "retry_class": "no_retry",
    "audit_event": "bin_lookup_rejected"
  }
}

Hata yanıtları ayrıca X-Correlation-Id, X-RateLimit-Limit, X-RateLimit-Remaining ve Content-Language taşır; 429 yanıtına Retry-After eklenir.

Denetimler şu sırayla çalışır: şema (1000001), tutar (0055235), BIN (0055233, 0055230), kart (0055234).

KodHTTPAnlamı
0055501403IP izin listesi boş ya da istek listede olmayan bir adresten geliyor.
0055502401 / 403Anahtar, ortam veya üye iş yeri kodu yanlış; hesap etkin değil ya da Direct API erişimi açık değil.
0055504401Zaman damgası 10 hane değil ya da 300 saniyeden fazla sapıyor veya imza 64 küçük harfli hex değil.
0055505401İmza doğrulanamadı.
1000001422Gövde şemaya uymuyor: eksik, yanlış ya da bilinmeyen alan veya card ile bin birlikte. errors alanları listeler.
1000005429Hız sınırı aşıldı. Retry-After süresini bekleyin.
0055001409Hesap henüz ödeme almaya hazır değil; bir kurulum adımı eksik.
0055002403Hesapta kartla ödeme kapalı.
0055235422Tutar geçersiz ya da sınırların dışında (amount_limit dolu) veya günlük/haftalık/aylık hacim limitine ulaşıldı (amount_limit null).
0055230409Bu BIN için uygun ödeme kanalı yok.
0055233409BIN tanımlanamadı.
0055210422bin tam altı haneli rakam değil.
0055234409Kart numarası ya da son kullanma tarihi geçersiz.
0055221409Bu kartın günlük deneme veya tutar sınırı doldu.
0055231409Bu kartla şu an ödeme alınamıyor.
0055503409Aynı merchant_order_id farklı içerikle kullanıldı ya da ilk istek hâlâ işleniyor.
0055402404Bu merchant_order_id ile sipariş yok.
0055404409Yalnızca sandbox sonuç ucu: sipariş ne pending ne de requires_hosted.

Sandbox

Sandbox aynı uçları sandbox anahtarı ve X-CEK-Environment: sandbox ile kullanır. Gerçek para hareket etmez.

  • X-CEK-Merchant-Code sandbox'ta zorunludur. Sandbox ortamının webhook ve dönüş adresleri kullanılır.
  • Gelecek tarihli herhangi bir son kullanma tarihi ve herhangi bir 3 haneli CVV kullanın.
  • Hacim limitleri yalnızca sandbox siparişlerini sayar.
BINHTTPSonuç kodu
435509200direct
540061200hosted
9792104090055230
diğer her BIN4090055233
Kart numarasıDavranış
4355090000044242requires_action. Test 3-D Secure ekranında onaylayın, sipariş paid olur.
4355090000005151requires_action. 3-D Secure sonrası sipariş insufficient_funds ile failed olur.
4355090000089999requires_action. 3-D Secure sonrası sipariş pending kalır; sandbox sonuç ucuyla kapatın.
43550900000400003-D Secure öncesinde doğrudan payment_declined ile failed olur.
diğer geçerli kartlarrequires_action. Test 3-D Secure ekranında sonucu siz seçersiniz: başarı paid, başarısızlık failed (payment_declined), zaman aşımı pending bırakır.

Geçersiz kart numarası (Luhn) ya da geçmiş son kullanma tarihi 0055234 kodlu 409 döndürür. Hosted sipariş için BIN 540061 kullanılır; test sayfasında başarı (paid) ya da başarısızlık (insufficient_funds ile failed) seçersiniz. Sipariş hâlâ requires_hosted iken sandbox sonuç ucuyla da kapatabilirsiniz.

POST/api/v4/sandbox/orders/{merchantOrderId}/result

Siparişin sonucunu belirleyin

pending durumundaki ya da hosted siparişte hosted sayfasını bekleyen (requires_hosted) bir sandbox siparişini kapatır. Webhook ve dönüş adresi canlıdaki gibi çalışır.

AlanTürZorunluAçıklama
resultstringZorunlupaid ya da failed (başarısız sipariş payment_declined sebebini alır).

İstek gövdesi · Örnek

{
  "result": "paid"
}

Olası hatalar

Canlı terminal ve son uçtan uca test için hesap yöneticinizle görüşün. İletişim