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.
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.
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).
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.
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
Header
Beschreibung
X-CEK-Api-Key
Der API-Schlüssel der Umgebung.
X-CEK-Environment
sandbox oder live. Muss zur Umgebung des Schlüssels passen.
X-CEK-Merchant-Code
Ihr Händlercode, zum Beispiel MRC-123456. In der Sandbox Pflicht; live muss er, wenn gesendet, zu Ihrem Konto passen.
X-CEK-Timestamp
Aktuelle Unix-Zeit in Sekunden (10 Ziffern). Eine Abweichung von mehr als 300 Sekunden wird abgelehnt.
X-CEK-Signature
64 Hex-Zeichen in Kleinbuchstaben, berechnet wie unten beschrieben.
Content-Type
application/json bei POST-Anfragen.
So signieren Sie eine Anfrage
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.
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.
Berechnen Sie HMAC-SHA256 dieser Zeichenkette mit Ihrem API-Secret als Schlüssel und schreiben Sie das Ergebnis als 64 Hex-Zeichen in Kleinbuchstaben.
Senden Sie es zusammen mit den anderen Headern als X-CEK-Signature.
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.
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
Feld
Typ
Erforderlich
Beschreibung
bin
string · path
Erforderlich
Die ersten sechs Ziffern der Kartennummer.
amount
string · query
Erforderlich
Bestellbetrag in TRY mit 2 Nachkommastellen. Die Eignung wird für diesen Betrag geprüft.
Antwort · 200
Feld
Typ
Beschreibung
bin
string
Die abgefragte BIN.
bank_name
string | null
Ausstellende Bank oder null, falls unbekannt.
network
string | null
Kartennetzwerk: visa, mastercard, amex, troy oder null.
card_type
string | null
credit, debit, prepaid oder null.
card_program
string | null
Name des Kartenprogramms oder null.
country
string | null
Ausstellungsland (ISO alpha-2) oder null.
domestic
boolean
true bei einer in der Türkei ausgestellten Karte.
payment_mode
string
direct: Karte selbst erfassen. hosted: gehostete Seite verwenden.
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)
Feld
Typ
Erforderlich
Beschreibung
merchant_order_id
string
Erforderlich
Ihre Bestellreferenz: 1–64 Zeichen aus A-Z a-z 0-9 . _ -, eindeutig je Konto und Umgebung.
amount
string
Erforderlich
Betrag in TRY mit 2 Nachkommastellen, zum Beispiel "1500.00".
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.
Feld
Typ
Beschreibung
order_code
string
Bestellnummer, 9 Ziffern als Zeichenkette. Dieser Wert kommt an der Rückkehradresse an.
merchant_order_id
string
Ihre merchant_order_id.
status
string
requires_action, requires_hosted, pending, paid oder failed.
amount
string
Bestellbetrag, 2 Nachkommastellen.
currency
string
Immer TRY.
redirect_url
string | null
3-D-Secure-Adresse. Gesetzt, solange der Status requires_action ist.
hosted_url
string | null
Adresse der gehosteten Seite. Gesetzt, solange der Status requires_hosted ist.
hosted_expires_at
string | null
Zeitpunkt, ab dem hosted_url nicht mehr funktioniert (ISO 8601, UTC).
failure_reason_code
string | null
Ablehnungscode einer failed-Bestellung; siehe Ablehnungsgründe. Kann null sein, wenn die Zahlung nie versucht wurde.
failure_message
string | null
Kurzer, für Kunden gedachter Text zum Grund. Er ist immer auf Türkisch; verzweigen Sie stattdessen anhand des Codes.
card
object | null
Kartenübersicht (holder_name, bin, last_four, network, program, issuer_bank_name). Bei direct sofort vorhanden, bei hosted sobald das Ergebnis feststeht.
created_at
string
Erstellungszeit (ISO 8601, +00:00).
completed_at
string | null
Abschlusszeit oder null, solange die Bestellung offen ist.
Leiten Sie den Kunden für 3-D Secure zu redirect_url.
requires_hosted
Nein
Leiten Sie den Kunden zu hosted_url.
pending
Nein
Die Bank hat noch nicht geantwortet oder der Kunde hat den Bildschirm verlassen. Warten Sie auf den Webhook.
paid
Ja
Die Zahlung wurde eingezogen.
failed
Ja
Die 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
Feld
Typ
Erforderlich
Beschreibung
merchantOrderId
string · path
Erforderlich
Die 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.
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`)
Leiten Sie den Browser des Kunden zu redirect_url um.
Der Kunde bestätigt die Zahlung bei seiner Bank.
Ist die Zahlung endgültig, wird der Kunde zu Ihrer Rückkehradresse geleitet, und Sie erhalten den Webhook.
Gehostete Seite (`requires_hosted`)
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.
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.
Schließt der Kunde die Seite, öffnet dieselbe hosted_url sie bis hosted_expires_at erneut.
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.
Feld
Typ
Beschreibung
order_code
string
Die 9-stellige Bestellnummer. Die Adresse enthält keine merchant_order_id; ordnen Sie die Bestellung mit diesem Wert zu.
status
string
paid oder failed.
failure_code
string
Grundcode, nur bei einer failed-Bestellung mit Grund. Sonst fehlt der Parameter.
ts
integer
Unix-Zeit in Sekunden, zu der die Adresse gebildet wurde.
key_version
integer
Version Ihres Secrets. Sie steigt bei jeder Erneuerung des Secrets.
sig
string
Signatur, 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.
Ergebnisse werden als signierte JSON-POST-Anfragen an die HTTPS-Webhook-Adresse der Umgebung zugestellt.
Ereignis
Gesendet, wenn
checkout.order.succeeded
Die Bestellung ist paid.
checkout.order.failed
Die Bestellung ist failed.
Header
Header
Beschreibung
X-CEK-Timestamp
Unix-Zeit in Sekunden. Lehnen Sie eine Abweichung von mehr als 300 Sekunden ab.
X-CEK-Signature
Signatur: Hex des HMAC-SHA256 über Zeitstempel, Punkt und rohen Body, mit Ihrem API-Secret als Schlüssel.
X-CEK-Event-Id
UUID des Ereignisses. Entspricht event_id im Body und bleibt bei Wiederholungen gleich.
X-CEK-Environment
sandbox oder live.
Jede Benachrichtigung prüfen
Lesen Sie den rohen Body, bevor Sie das JSON parsen.
Berechnen Sie die Signatur über X-CEK-Timestamp, einen Punkt und den rohen Body und vergleichen Sie sie zeitkonstant mit X-CEK-Signature.
Lehnen Sie die Anfrage ab, wenn der Zeitstempel um mehr als 300 Sekunden von Ihrer Uhr abweicht.
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.
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.
Versuch
Wartezeit davor
1
—
2
60 s (1 min)
3
300 s (5 min)
4
900 s (15 min)
5
1800 s (30 min)
6
3600 s (1 h)
7
10800 s (3 h)
8
21600 s (6 h)
9
43200 s (12 h)
10
86400 s (24 h)
11
172800 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.
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.
Code
Beschreibung
payment_declined
Die ausstellende Bank hat die Zahlung nicht genehmigt.
do_not_honor
Die Bank hat ohne konkreten Grund abgelehnt.
insufficient_funds
Guthaben oder Limit der Karte reichen für diese Zahlung nicht aus.
card_limit_reached
Das Transaktionslimit der Karte ist erreicht.
card_details_invalid
Die Kartennummer wurde nicht akzeptiert.
card_expiry_invalid
Das Ablaufdatum ist ungültig.
card_security_code_invalid
Der Sicherheitscode (CVV) ist falsch.
card_not_active
Die Karte ist für Zahlungen nicht freigeschaltet.
card_brand_not_supported
Diese Kartenmarke wird für diese Zahlung nicht unterstützt.
three_ds_failed
3-D Secure konnte nicht abgeschlossen werden.
security_declined
Eine Sicherheitsprüfung hat die Zahlung gestoppt. Teilen Sie dem Kunden nur mit, dass die Bank sie nicht genehmigt hat.
payment_not_permitted
Diese Karte kann für diese Zahlung nicht verwendet werden.
amount_not_allowed
Der Betrag liegt außerhalb des Bereichs, den diese Karte akzeptiert.
currency_not_supported
Die Währung wird für diese Zahlung nicht unterstützt.
payment_details_rejected
Die Zahlungsangaben wurden abgelehnt.
duplicate_payment
Dieselbe Zahlung scheint bereits eingezogen worden zu sein.
cancelled_by_customer
Der Kunde hat abgebrochen, bevor die Zahlung abgeschlossen war.
payment_timed_out
Die Zahlungszeit ist abgelaufen.
bank_unavailable
Die Bank ist gerade nicht erreichbar. Versuchen Sie es in Kürze erneut.
bank_requested_retry
Die Bank hat um eine Wiederholung der Zahlung gebeten.
provider_unavailable
Die 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.
Feld
Typ
Beschreibung
type, title, status, detail
string · integer
Standardfelder des Problem-Formats.
instance
string
Der Pfad der Anfrage.
code
string
Der 7-stellige Fehlercode aus der Tabelle unten.
correlation_id
string
Nennen Sie ihn, wenn Sie den Support kontaktieren. Wird auch als X-Correlation-Id gesendet.
metadata
object
developer_detail, retry_class und audit_event: zusätzliche Informationen zur Fehlersuche.
amount_limit
object
Nur bei 0055235: min und max des erlaubten Betrags, bei einem Volumenlimit null.
errors
object
Nur 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).
Code
HTTP
Bedeutung
0055501
403
Die IP-Zulassungsliste ist leer oder die Anfrage kommt von einer Adresse, die nicht darauf steht.
0055502
401 / 403
Falscher Schlüssel, falsche Umgebung oder falscher Händlercode, inaktives Konto oder nicht geöffneter Direct-API-Zugang.
0055504
401
Der Zeitstempel hat keine 10 Ziffern oder weicht um mehr als 300 Sekunden ab, oder die Signatur besteht nicht aus 64 Hex-Zeichen in Kleinbuchstaben.
0055505
401
Die Signatur konnte nicht verifiziert werden.
1000001
422
Der Body entspricht nicht dem Schema: fehlendes, falsches oder unbekanntes Feld oder card zusammen mit bin. errors listet die Felder.
1000005
429
Ratenlimit überschritten. Warten Sie die Zeit aus Retry-After ab.
0055001
409
Das Konto ist noch nicht bereit, Zahlungen anzunehmen; ein Einrichtungsschritt fehlt.
0055002
403
Kartenzahlungen sind für das Konto ausgeschaltet.
0055235
422
Der 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).
0055230
409
Für diese BIN ist kein Zahlungskanal verfügbar.
0055233
409
Die BIN konnte nicht erkannt werden.
0055210
422
bin besteht nicht aus genau sechs Ziffern.
0055234
409
Kartennummer oder Ablaufdatum sind ungültig.
0055221
409
Das tägliche Versuchs- oder Betragslimit dieser Karte ist erreicht.
0055231
409
Diese Karte kann im Moment nicht belastet werden.
0055503
409
Dieselbe merchant_order_id wurde mit anderem Inhalt verwendet, oder die erste Anfrage wird noch verarbeitet.
0055402
404
Für diese merchant_order_id gibt es keine Bestellung.
0055404
409
Nur 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.
BIN
HTTP
Ergebniscode
435509
200
direct
540061
200
hosted
979210
409
0055230
jede andere BIN
409
0055233
Kartennummer
Verhalten
4355090000044242
requires_action. Genehmigen Sie den 3-D-Secure-Testbildschirm, dann wird die Bestellung paid.
4355090000005151
requires_action. Nach 3-D Secure wird die Bestellung failed mit insufficient_funds.
4355090000089999
requires_action. Nach 3-D Secure bleibt die Bestellung pending; schließen Sie sie mit dem Sandbox-Ergebnis-Endpunkt.
4355090000040000
Sofort failed mit payment_declined, noch vor 3-D Secure.
jede andere gültige Karte
requires_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.
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.
Feld
Typ
Erforderlich
Beschreibung
result
string
Erforderlich
paid oder failed (eine fehlgeschlagene Bestellung erhält den Grund payment_declined).