ENTWICKLER

Direct API (v4)

Erfassen Sie die Karte in Ihrem eigenen Zahlungsformular und senden Sie sie von Ihrem Server an Manuell. Der Kunde bestätigt die Zahlung mit 3-D Secure, das Ergebnis erhalten Sie per Webhook.

  • Basis-URLhttps://www.manuellsolutions.com/api/v4
  • API-Versionv4

Mit dem Überblick beginnen

Umgebung

Testen Sie, ohne echtes Geld zu bewegen.

Echte Zahlungen. Ein Live-Schlüssel ist erforderlich.

Überblick

Ihr Server spricht direkt mit Manuell: Er fragt, welchen Ablauf eine Karte braucht, legt die Bestellung mit den Kartendaten an und verfolgt sie, bis sie bezahlt ist oder fehlschlägt.

Basis-URL
https://www.manuellsolutions.com/api/v4
Währung
Nur TRY. Eine Zahlung pro Bestellung, keine Ratenzahlung.
Beträge
Zeichenketten mit 2 Nachkommastellen, z. B. "1500.00".
Umgebung
sandbox oder live, gesendet im Header X-CEK-Environment. Muss zur Umgebung Ihres Schlüssels passen.
Fehler
application/problem+json (RFC 7807) mit einem 7-stelligen code.
Ratenlimit
300 Anfragen pro Minute und Schlüssel (vor der Authentifizierung 600 pro Minute und IP-Adresse). Darüber: 429 mit Retry-After.
Duplikate
Es gibt keinen Idempotency-Key. merchant_order_id schützt vor doppelten Bestellungen.
  1. BIN abfragen

    Senden Sie die ersten 6 Ziffern der Karte und den Betrag an GET /api/v4/bins/{bin}. payment_mode sagt Ihnen, ob Sie die Karte selbst erfassen (direct) oder den Kunden auf eine gehostete Seite leiten (hosted).

  2. Bestellung anlegen

    Senden Sie POST /api/v4/orders mit der Karte (direct) oder nur mit der BIN (hosted). Die Bestellung kommt als requires_action, requires_hosted, pending oder failed zurück.

  3. Abschließen und bestätigen

    Leiten Sie den Kunden zu redirect_url (3-D Secure) oder hosted_url. Das endgültige Ergebnis kommt per Webhook; verspätet es sich, lesen Sie die Bestellung mit GET /api/v4/orders/{merchantOrderId}.

Einrichtung im Panel

  • Erzeugen Sie unter “API-Schlüssel und Integration” je Umgebung einen API-Schlüssel und ein Secret. Bewahren Sie das Secret auf Ihrem Server auf.
  • Tragen Sie die öffentlichen IP-Adressen Ihrer Server in die IP-Zulassungsliste der Umgebung ein. Solange die Liste leer ist, wird jeder Aufruf mit 403 und Code 0055501 abgelehnt.
  • Legen Sie eine HTTPS-Webhook-Adresse fest. Ihr Host muss öffentlich sein.
  • Legen Sie die Rückkehradressen für Erfolg und Fehler fest (live HTTPS). Ohne sie bleibt der Kunde nach der Zahlung auf der Ergebnisseite.
  • Ihr Händlercode ist MRC- gefolgt von Ihrer 6-stelligen Kontonummer, zum Beispiel MRC-123456.

Authentifizierung

Jeder Aufruf trägt X-CEK-*-Header. Die Signatur ist ein HMAC-SHA256 über eine Zeichenkette aus Zeitstempel, Methode, Pfad und Body-Hash.

Erforderliche Header

HeaderBeschreibung
X-CEK-Api-KeyDer API-Schlüssel der Umgebung.
X-CEK-Environmentsandbox oder live. Muss zur Umgebung des Schlüssels passen.
X-CEK-Merchant-CodeIhr Händlercode, zum Beispiel MRC-123456. In der Sandbox Pflicht; live muss er, wenn gesendet, zu Ihrem Konto passen.
X-CEK-TimestampAktuelle Unix-Zeit in Sekunden (10 Ziffern). Eine Abweichung von mehr als 300 Sekunden wird abgelehnt.
X-CEK-Signature64 Hex-Zeichen in Kleinbuchstaben, berechnet wie unten beschrieben.
Content-Typeapplication/json bei POST-Anfragen.

So signieren Sie eine Anfrage

  1. Nehmen Sie den Body genau so, wie Sie ihn senden, und berechnen Sie sha256(body) als Hex in Kleinbuchstaben. Bei einer Anfrage ohne Body verwenden Sie den Hash der leeren Zeichenkette.

  2. Verbinden Sie Zeitstempel, Methode in Großbuchstaben, Pfad und Body-Hash mit Zeilenumbrüchen (\n). Der Pfad ist der vollständige Pfad ohne Host und Query-String, zum Beispiel /api/v4/orders.

  3. Berechnen Sie HMAC-SHA256 dieser Zeichenkette mit Ihrem API-Secret als Schlüssel und schreiben Sie das Ergebnis als 64 Hex-Zeichen in Kleinbuchstaben.

  4. Senden Sie es zusammen mit den anderen Headern als X-CEK-Signature.

Kanonische Signaturzeichenkette

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

Signieren Sie exakt die Bytes, die Sie senden: Serialisieren Sie das JSON einmal und verwenden Sie dieselbe Zeichenkette für den Hash und für den Request-Body.

Beispiel (mit Fantasiedaten)

Alle Werte sind erfunden: Secret sk_test_EXAMPLE, Zeitstempel 1790000000. Verwenden Sie die Werte aus Ihrem eigenen Panel.

Anfrage-Body

{"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

Beispielcode

Jedes Beispiel fragt in der Sandbox eine BIN ab und legt eine Bestellung an. Die Zugangsdaten stammen aus Umgebungsvariablen; codieren Sie das Secret niemals fest ein. Jeder Lauf verwendet eine neue merchant_order_id.

#!/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 abfragen

GET/api/v4/bins/{bin}

Karten-BIN abfragen

Rufen Sie ihn auf, sobald der Kunde die ersten 6 Ziffern eingegeben hat. Die Antwort ist eine Momentaufnahme, die bei der Wahl zwischen Ihrem eigenen Kartenformular und der gehosteten Seite hilft; endgültig wird beim Anlegen der Bestellung entschieden.

Parameter

FeldTypErforderlichBeschreibung
binstring · pathErforderlichDie ersten sechs Ziffern der Kartennummer.
amountstring · queryErforderlichBestellbetrag in TRY mit 2 Nachkommastellen. Die Eignung wird für diesen Betrag geprüft.

Antwort · 200

FeldTypBeschreibung
binstringDie abgefragte BIN.
bank_namestring | nullAusstellende Bank oder null, falls unbekannt.
networkstring | nullKartennetzwerk: visa, mastercard, amex, troy oder null.
card_typestring | nullcredit, debit, prepaid oder null.
card_programstring | nullName des Kartenprogramms oder null.
countrystring | nullAusstellungsland (ISO alpha-2) oder null.
domesticbooleantrue bei einer in der Türkei ausgestellten Karte.
payment_modestringdirect: Karte selbst erfassen. hosted: gehostete Seite verwenden.

Antwort · Beispiel

{
  "data": {
    "bin": "435509",
    "bank_name": "Example Bank",
    "network": "visa",
    "card_type": "credit",
    "card_program": null,
    "country": "TR",
    "domestic": true,
    "payment_mode": "direct"
  }
}
  • Bei direct erfassen Sie alle Kartendaten und legen die Bestellung mit card an.
  • Bei hosted legen Sie die Bestellung nur mit bin an und leiten den Kunden zur zurückgegebenen hosted_url.

Mögliche Fehler

Bestellung anlegen

POST/api/v4/orders

Bestellung anlegen

Ein Endpunkt legt die Bestellung in beiden Modi an. Der Body entscheidet: Senden Sie card für eine direct-Bestellung oder nur bin für eine hosted-Bestellung. Beides zusammen liefert 422.

Direct-Bestellung (mit Karte)

FeldTypErforderlichBeschreibung
merchant_order_idstringErforderlichIhre Bestellreferenz: 1–64 Zeichen aus A-Z a-z 0-9 . _ -, eindeutig je Konto und Umgebung.
amountstringErforderlichBetrag in TRY mit 2 Nachkommastellen, zum Beispiel "1500.00".
card.holder_namestringErforderlichName auf der Karte, 2–120 Zeichen.
card.numberstringErforderlichKartennummer, 13–19 Ziffern, nur Ziffern.
card.expiry_monthstringErforderlichAblaufmonat, "01" bis "12".
card.expiry_yearstringErforderlichAblaufjahr zweistellig, zum Beispiel "30".
card.cvvstringErforderlichSicherheitscode, 3 oder 4 Ziffern.

Anfrage-Body · Beispiel

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

Mögliche Fehler

Hosted-Bestellung (nur BIN)

FeldTypErforderlichBeschreibung
merchant_order_idstringErforderlichIhre Bestellreferenz: 1–64 Zeichen aus A-Z a-z 0-9 . _ -, eindeutig je Konto und Umgebung.
amountstringErforderlichBetrag in TRY mit 2 Nachkommastellen, zum Beispiel "1500.00".
binstringErforderlichDie ersten sechs Ziffern der Kartennummer.

Anfrage-Body · Beispiel

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

Mögliche Fehler

Regeln

  • merchant_order_id muss je Konto und Umgebung eindeutig sein. Es ist der einzige Schutz vor Duplikaten.
  • Eine Wiederholung mit derselben merchant_order_id und demselben Inhalt startet keine neue Zahlung und liefert die Bestellung im aktuellen Zustand. Abweichender Inhalt liefert 409 mit Code 0055503.
  • Ein Feld, das nicht im Schema steht, liefert 422 mit Code 1000001.
  • Bei einer 4xx-Antwort wurde keine Zahlung eingezogen. Verwenden Sie für den nächsten Versuch eine neue merchant_order_id. Auch eine failed-Bestellung ist endgültig.
  • Nach einer 5xx-Antwort oder einem Timeout kann die Zahlung bereits gestartet sein. Legen Sie keine neue Bestellung an: Wiederholen Sie dieselbe Anfrage unverändert oder lesen Sie die Bestellung per GET. Planen Sie vor dem Timeout mindestens 30 Sekunden ein.
  • Ist die BIN in diesem Moment hosted, liefert auch eine Anfrage mit card den Status requires_hosted; die Kartendaten werden nicht verwendet.

Das Bestellobjekt

Anlegen und Lesen liefern die Bestellung in data.

FeldTypBeschreibung
order_codestringBestellnummer, 9 Ziffern als Zeichenkette. Dieser Wert kommt an der Rückkehradresse an.
merchant_order_idstringIhre merchant_order_id.
statusstringrequires_action, requires_hosted, pending, paid oder failed.
amountstringBestellbetrag, 2 Nachkommastellen.
currencystringImmer TRY.
redirect_urlstring | null3-D-Secure-Adresse. Gesetzt, solange der Status requires_action ist.
hosted_urlstring | nullAdresse der gehosteten Seite. Gesetzt, solange der Status requires_hosted ist.
hosted_expires_atstring | nullZeitpunkt, ab dem hosted_url nicht mehr funktioniert (ISO 8601, UTC).
failure_reason_codestring | nullAblehnungscode einer failed-Bestellung; siehe Ablehnungsgründe. Kann null sein, wenn die Zahlung nie versucht wurde.
failure_messagestring | nullKurzer, für Kunden gedachter Text zum Grund. Er ist immer auf Türkisch; verzweigen Sie stattdessen anhand des Codes.
cardobject | nullKartenübersicht (holder_name, bin, last_four, network, program, issuer_bank_name). Bei direct sofort vorhanden, bei hosted sobald das Ergebnis feststeht.
created_atstringErstellungszeit (ISO 8601, +00:00).
completed_atstring | nullAbschlusszeit oder null, solange die Bestellung offen ist.

Antwort · Beispiel

{
  "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
  }
}

Status und Zeiten

FeldEndgültig?Beschreibung
requires_actionNeinLeiten Sie den Kunden für 3-D Secure zu redirect_url.
requires_hostedNeinLeiten Sie den Kunden zu hosted_url.
pendingNeinDie Bank hat noch nicht geantwortet oder der Kunde hat den Bildschirm verlassen. Warten Sie auf den Webhook.
paidJaDie Zahlung wurde eingezogen.
failedJaDie Zahlung ist nicht zustande gekommen. failure_reason_code nennt den Grund.

Der Kunde hat 10 Minuten für 3-D Secure. Die gehostete Seite funktioniert bis hosted_expires_at. Eine Zahlung, auf die die Bank nie antwortet, wird innerhalb von 24 Stunden paid oder failed; eine abgelaufene Zahlung wird failed.

In seltenen Fällen kann eine failed-Bestellung später paid werden. Maßgeblich ist paid.

Bestellung abrufen

GET/api/v4/orders/{merchantOrderId}

Bestellung abrufen

Liefert dasselbe Objekt wie das Anlegen, ohne Request-Body. redirect_url wird bis zum Ende des 3-D-Secure-Fensters geliefert, hosted_url bis hosted_expires_at.

Parameter

FeldTypErforderlichBeschreibung
merchantOrderIdstring · pathErforderlichDie merchant_order_id, die Sie beim Anlegen verwendet haben.

Der Webhook ist der Hauptkanal. Verspätet er sich, lesen Sie die Bestellung höchstens alle 30 Sekunden.

Mögliche Fehler

3-D Secure und die gehostete Seite

Nach dem Anlegen der Bestellung schließt der Kunde die Zahlung auf einer Seite ab, die nicht Ihnen gehört. Leiten Sie ihn mit einer normalen Vollseiten-Weiterleitung dorthin.

3-D Secure (`requires_action`)

  1. Leiten Sie den Browser des Kunden zu redirect_url um.

  2. Der Kunde bestätigt die Zahlung bei seiner Bank.

  3. Ist die Zahlung endgültig, wird der Kunde zu Ihrer Rückkehradresse geleitet, und Sie erhalten den Webhook.

Gehostete Seite (`requires_hosted`)

  1. Leiten Sie den Kunden zu hosted_url. Eine Vollseiten-Weiterleitung funktioniert immer; die Anzeige in einem Modal-Frame hängt von der Erlaubnis der Seite ab, halten Sie die Weiterleitung daher als Rückfall bereit.

  2. Der Kunde gibt die Karte ein und schließt 3-D Secure auf dieser Seite ab. Ein fehlgeschlagener Versuch schließt die Bestellung nicht; der Kunde kann dort erneut versuchen.

  3. Schließt der Kunde die Seite, öffnet dieselbe hosted_url sie bis hosted_expires_at erneut.

  4. Endet die Zahlung, wird die ganze Seite zu Ihrer Rückkehradresse geleitet. Bestätigen Sie das Ergebnis per Webhook.

Rückkehradresse

Wird eine Bestellung paid oder failed, leiten wir den Kunden zur passenden Rückkehradresse, die Sie im Panel festgelegt haben. Ist eine Webhook-Adresse gesetzt, erhält die Adresse diese signierten Query-Parameter.

FeldTypBeschreibung
order_codestringDie 9-stellige Bestellnummer. Die Adresse enthält keine merchant_order_id; ordnen Sie die Bestellung mit diesem Wert zu.
statusstringpaid oder failed.
failure_codestringGrundcode, nur bei einer failed-Bestellung mit Grund. Sonst fehlt der Parameter.
tsintegerUnix-Zeit in Sekunden, zu der die Adresse gebildet wurde.
key_versionintegerVersion Ihres Secrets. Sie steigt bei jeder Erneuerung des Secrets.
sigstringSignatur, 64 Hex-Zeichen in Kleinbuchstaben.

Kanonische Signaturzeichenkette

sig = hex(HMAC-SHA256(key = API_SECRET, message = ts + "." + order_code + "." + status + "." + failure_code))
  • Prüfen Sie sig, bevor Sie den Parametern vertrauen. Fehlt failure_code, setzen Sie eine leere Zeichenkette ein; der Punkt bleibt.
  • Lehnen Sie ein ts ab, das älter als 10 Minuten ist.
  • Die Rückkehr zeigt nur, wo der Kunde ist. Das Ergebnis ist endgültig, wenn der Webhook eintrifft oder GET /api/v4/orders/{merchantOrderId} es bestätigt.
  • Ist keine Webhook-Adresse gesetzt, wird die Adresse ohne diese Parameter aufgerufen.

Beispiel

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

Webhook

Ergebnisse werden als signierte JSON-POST-Anfragen an die HTTPS-Webhook-Adresse der Umgebung zugestellt.

EreignisGesendet, wenn
checkout.order.succeededDie Bestellung ist paid.
checkout.order.failedDie Bestellung ist failed.

Header

HeaderBeschreibung
X-CEK-TimestampUnix-Zeit in Sekunden. Lehnen Sie eine Abweichung von mehr als 300 Sekunden ab.
X-CEK-SignatureSignatur: Hex des HMAC-SHA256 über Zeitstempel, Punkt und rohen Body, mit Ihrem API-Secret als Schlüssel.
X-CEK-Event-IdUUID des Ereignisses. Entspricht event_id im Body und bleibt bei Wiederholungen gleich.
X-CEK-Environmentsandbox oder live.

Jede Benachrichtigung prüfen

  1. Lesen Sie den rohen Body, bevor Sie das JSON parsen.

  2. Berechnen Sie die Signatur über X-CEK-Timestamp, einen Punkt und den rohen Body und vergleichen Sie sie zeitkonstant mit X-CEK-Signature.

  3. Lehnen Sie die Anfrage ab, wenn der Zeitstempel um mehr als 300 Sekunden von Ihrer Uhr abweicht.

  4. Deduplizieren Sie mit X-CEK-Event-Id. Die Zustellung erfolgt mindestens einmal und kann in beliebiger Reihenfolge eintreffen. Haben Sie für eine Bestellung checkout.order.succeeded gesehen, ignorieren Sie jedes checkout.order.failed dazu.

  5. Antworten Sie innerhalb von 10 Sekunden (3 Sekunden für den Verbindungsaufbau) mit einem beliebigen 2xx. Weiterleitungen (3xx), 4xx, 5xx und Timeouts gelten als Fehlschlag und werden wiederholt.

Beispiele zur Signaturprüfung

<?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.

Wiederholungen

Eine fehlgeschlagene Zustellung wird nach den unten genannten Wartezeiten, gerechnet ab dem vorherigen Fehlschlag, wiederholt, insgesamt höchstens 11 Versuche.

VersuchWartezeit davor
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)

Body

Der Body enthält schema_version (3), event_id, event_type, ein order-Objekt, ein merchant-Objekt und die Kartenübersicht in payment_details.card. Ein failed-Ereignis ergänzt failure_code, failure_reason_code und failure_message. Ignorieren Sie Felder, die Sie nicht nutzen; neue Felder können hinzukommen.

Anfrage-Body · Beispiel

{
  "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."
}

Ablehnungsgründe

failure_reason_code ist einer dieser Codes. Bauen Sie Ihre Logik auf den Code; failure_message ist ein türkischer Text für den Kunden.

CodeBeschreibung
payment_declinedDie ausstellende Bank hat die Zahlung nicht genehmigt.
do_not_honorDie Bank hat ohne konkreten Grund abgelehnt.
insufficient_fundsGuthaben oder Limit der Karte reichen für diese Zahlung nicht aus.
card_limit_reachedDas Transaktionslimit der Karte ist erreicht.
card_details_invalidDie Kartennummer wurde nicht akzeptiert.
card_expiry_invalidDas Ablaufdatum ist ungültig.
card_security_code_invalidDer Sicherheitscode (CVV) ist falsch.
card_not_activeDie Karte ist für Zahlungen nicht freigeschaltet.
card_brand_not_supportedDiese Kartenmarke wird für diese Zahlung nicht unterstützt.
three_ds_failed3-D Secure konnte nicht abgeschlossen werden.
security_declinedEine Sicherheitsprüfung hat die Zahlung gestoppt. Teilen Sie dem Kunden nur mit, dass die Bank sie nicht genehmigt hat.
payment_not_permittedDiese Karte kann für diese Zahlung nicht verwendet werden.
amount_not_allowedDer Betrag liegt außerhalb des Bereichs, den diese Karte akzeptiert.
currency_not_supportedDie Währung wird für diese Zahlung nicht unterstützt.
payment_details_rejectedDie Zahlungsangaben wurden abgelehnt.
duplicate_paymentDieselbe Zahlung scheint bereits eingezogen worden zu sein.
cancelled_by_customerDer Kunde hat abgebrochen, bevor die Zahlung abgeschlossen war.
payment_timed_outDie Zahlungszeit ist abgelaufen.
bank_unavailableDie Bank ist gerade nicht erreichbar. Versuchen Sie es in Kürze erneut.
bank_requested_retryDie Bank hat um eine Wiederholung der Zahlung gebeten.
provider_unavailableDie Zahlung konnte gerade nicht abgeschlossen werden. Versuchen Sie es in Kürze erneut.

Die Liste kann wachsen. Behandeln Sie einen unbekannten Code wie payment_declined. Im Webhook wird security_declined in failure_code als payment_declined gemeldet.

Fehlercodes

Fehler verwenden das Format application/problem+json. Verzweigen Sie anhand von code; title und detail sind für Menschen.

FeldTypBeschreibung
type, title, status, detailstring · integerStandardfelder des Problem-Formats.
instancestringDer Pfad der Anfrage.
codestringDer 7-stellige Fehlercode aus der Tabelle unten.
correlation_idstringNennen Sie ihn, wenn Sie den Support kontaktieren. Wird auch als X-Correlation-Id gesendet.
metadataobjectdeveloper_detail, retry_class und audit_event: zusätzliche Informationen zur Fehlersuche.
amount_limitobjectNur bei 0055235: min und max des erlaubten Betrags, bei einem Volumenlimit null.
errorsobjectNur bei 1000001: die Felder, die die Validierung nicht bestanden haben.

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"
  }
}

Fehlerantworten tragen außerdem X-Correlation-Id, X-RateLimit-Limit, X-RateLimit-Remaining und Content-Language; ein 429 ergänzt Retry-After.

Die Prüfungen laufen in dieser Reihenfolge: Schema (1000001), Betrag (0055235), BIN (0055233, 0055230), Karte (0055234).

CodeHTTPBedeutung
0055501403Die IP-Zulassungsliste ist leer oder die Anfrage kommt von einer Adresse, die nicht darauf steht.
0055502401 / 403Falscher Schlüssel, falsche Umgebung oder falscher Händlercode, inaktives Konto oder nicht geöffneter Direct-API-Zugang.
0055504401Der Zeitstempel hat keine 10 Ziffern oder weicht um mehr als 300 Sekunden ab, oder die Signatur besteht nicht aus 64 Hex-Zeichen in Kleinbuchstaben.
0055505401Die Signatur konnte nicht verifiziert werden.
1000001422Der Body entspricht nicht dem Schema: fehlendes, falsches oder unbekanntes Feld oder card zusammen mit bin. errors listet die Felder.
1000005429Ratenlimit überschritten. Warten Sie die Zeit aus Retry-After ab.
0055001409Das Konto ist noch nicht bereit, Zahlungen anzunehmen; ein Einrichtungsschritt fehlt.
0055002403Kartenzahlungen sind für das Konto ausgeschaltet.
0055235422Der Betrag ist ungültig oder liegt außerhalb der Grenzen (amount_limit gesetzt), oder ein Tages-, Wochen- oder Monatsvolumenlimit ist erreicht (amount_limit ist null).
0055230409Für diese BIN ist kein Zahlungskanal verfügbar.
0055233409Die BIN konnte nicht erkannt werden.
0055210422bin besteht nicht aus genau sechs Ziffern.
0055234409Kartennummer oder Ablaufdatum sind ungültig.
0055221409Das tägliche Versuchs- oder Betragslimit dieser Karte ist erreicht.
0055231409Diese Karte kann im Moment nicht belastet werden.
0055503409Dieselbe merchant_order_id wurde mit anderem Inhalt verwendet, oder die erste Anfrage wird noch verarbeitet.
0055402404Für diese merchant_order_id gibt es keine Bestellung.
0055404409Nur Sandbox-Ergebnis-Endpunkt: Die Bestellung ist weder pending noch requires_hosted.

Sandbox

Die Sandbox nutzt dieselben Endpunkte mit einem Sandbox-Schlüssel und X-CEK-Environment: sandbox. Es bewegt sich kein echtes Geld.

  • X-CEK-Merchant-Code ist in der Sandbox Pflicht. Es gelten die Webhook- und Rückkehradressen der Sandbox-Umgebung.
  • Verwenden Sie ein beliebiges Ablaufdatum in der Zukunft und eine beliebige dreistellige CVV.
  • Volumenlimits zählen nur Sandbox-Bestellungen.
BINHTTPErgebniscode
435509200direct
540061200hosted
9792104090055230
jede andere BIN4090055233
KartennummerVerhalten
4355090000044242requires_action. Genehmigen Sie den 3-D-Secure-Testbildschirm, dann wird die Bestellung paid.
4355090000005151requires_action. Nach 3-D Secure wird die Bestellung failed mit insufficient_funds.
4355090000089999requires_action. Nach 3-D Secure bleibt die Bestellung pending; schließen Sie sie mit dem Sandbox-Ergebnis-Endpunkt.
4355090000040000Sofort failed mit payment_declined, noch vor 3-D Secure.
jede andere gültige Karterequires_action. Auf dem 3-D-Secure-Testbildschirm wählen Sie das Ergebnis: Erfolg ergibt paid, Fehler failed (payment_declined), Timeout lässt pending.

Eine ungültige Kartennummer (Luhn) oder ein Ablaufdatum in der Vergangenheit liefert 409 mit Code 0055234. Eine Hosted-Bestellung nutzt die BIN 540061; auf der Testseite wählen Sie Erfolg (paid) oder Fehler (failed mit insufficient_funds). Solange sie noch requires_hosted ist, können Sie sie auch mit dem Sandbox-Ergebnis-Endpunkt schließen.

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

Ergebnis einer Bestellung festlegen

Schließt eine Sandbox-Bestellung, die pending ist oder – bei einer Hosted-Bestellung – noch auf ihre Hosted-Seite wartet (requires_hosted). Webhook und Rückkehradresse funktionieren wie live.

FeldTypErforderlichBeschreibung
resultstringErforderlichpaid oder failed (eine fehlgeschlagene Bestellung erhält den Grund payment_declined).

Anfrage-Body · Beispiel

{
  "result": "paid"
}

Mögliche Fehler

Sprechen Sie mit Ihrem Account-Manager über das Live-Terminal und den abschließenden Ende-zu-Ende-Test. Kontakt