Συλλέξτε την κάρτα στη δική σας φόρμα πληρωμής και στείλτε την στη Manuell από τον διακομιστή σας. Ο πελάτης επιβεβαιώνει την πληρωμή με 3-D Secure και λαμβάνετε το αποτέλεσμα με webhook.
Ο διακομιστής σας μιλά απευθείας με τη Manuell: ρωτά ποια ροή χρειάζεται μια κάρτα, δημιουργεί την παραγγελία με τα στοιχεία της κάρτας και την παρακολουθεί μέχρι να πληρωθεί ή να αποτύχει.
Βασικό URL
https://www.manuellsolutions.com/api/v4
Νόμισμα
Μόνο TRY. Μία πληρωμή ανά παραγγελία, χωρίς δόσεις.
Ποσά
Συμβολοσειρές με 2 δεκαδικά, π.χ. "1500.00".
Περιβάλλον
sandbox ή live, στην κεφαλίδα X-CEK-Environment. Πρέπει να ταιριάζει με το περιβάλλον του κλειδιού σας.
Σφάλματα
application/problem+json (RFC 7807) με code 7 ψηφίων.
Όριο ρυθμού
300 αιτήματα ανά λεπτό ανά κλειδί (600 ανά λεπτό ανά διεύθυνση IP πριν την ταυτοποίηση). Πέρα από αυτό: 429 με Retry-After.
Διπλότυπα
Δεν υπάρχει Idempotency-Key. Το merchant_order_id προστατεύει από διπλές παραγγελίες.
Αναζητήστε το BIN
Στείλτε τα πρώτα 6 ψηφία της κάρτας και το ποσό στο GET /api/v4/bins/{bin}. Το payment_mode σας λέει αν θα συλλέξετε εσείς την κάρτα (direct) ή θα στείλετε τον πελάτη σε φιλοξενούμενη σελίδα (hosted).
Δημιουργήστε την παραγγελία
Στείλτε POST /api/v4/orders με την κάρτα (direct) ή μόνο με το BIN (hosted). Η παραγγελία επιστρέφει ως requires_action, requires_hosted, pending ή failed.
Ολοκληρώστε και επιβεβαιώστε
Οδηγήστε τον πελάτη στο redirect_url (3-D Secure) ή στο hosted_url. Το τελικό αποτέλεσμα φτάνει με webhook· αν καθυστερεί, διαβάστε την παραγγελία με GET /api/v4/orders/{merchantOrderId}.
Ρύθμιση στον πίνακα
Δημιουργήστε κλειδί API και μυστικό κλειδί για κάθε περιβάλλον στο “API Key & Ενσωμάτωση”. Φυλάξτε το μυστικό στον διακομιστή σας.
Προσθέστε τις δημόσιες διευθύνσεις IP των διακομιστών σας στη λίστα επιτρεπόμενων IP του περιβάλλοντος. Όσο η λίστα είναι κενή, κάθε κλήση απορρίπτεται με 403 και κωδικό 0055501.
Ορίστε μια διεύθυνση webhook HTTPS. Ο κεντρικός υπολογιστής της πρέπει να είναι δημόσιος.
Ορίστε τις διευθύνσεις επιστροφής για επιτυχία και αποτυχία (HTTPS στο live). Χωρίς αυτές ο πελάτης μένει στην οθόνη αποτελέσματος μετά την πληρωμή.
Ο κωδικός εμπόρου σας είναι MRC- ακολουθούμενο από τον 6ψήφιο αριθμό λογαριασμού σας, για παράδειγμα MRC-123456.
Έλεγχος ταυτότητας
Κάθε κλήση φέρει κεφαλίδες X-CEK-*. Η υπογραφή είναι ένα HMAC-SHA256 μιας συμβολοσειράς που ενώνει τη χρονοσήμανση, τη μέθοδο, τη διαδρομή και το hash του σώματος.
Υποχρεωτικές κεφαλίδες
Κεφαλίδα
Περιγραφή
X-CEK-Api-Key
Το κλειδί API του περιβάλλοντος.
X-CEK-Environment
sandbox ή live. Πρέπει να ταιριάζει με το περιβάλλον του κλειδιού.
X-CEK-Merchant-Code
Ο κωδικός εμπόρου σας, π.χ. MRC-123456. Υποχρεωτικός στο sandbox· στο live, αν τον στείλετε, πρέπει να ταιριάζει με τον λογαριασμό σας.
X-CEK-Timestamp
Τρέχων χρόνος Unix σε δευτερόλεπτα (10 ψηφία). Απόκλιση πάνω από 300 δευτερόλεπτα απορρίπτεται.
X-CEK-Signature
64 πεζοί χαρακτήρες hex, υπολογισμένοι όπως περιγράφεται παρακάτω.
Content-Type
application/json στα αιτήματα POST.
Πώς υπογράφεται ένα αίτημα
Πάρτε το σώμα ακριβώς όπως το στέλνετε και υπολογίστε το sha256(body) ως hex με πεζά. Για αίτημα χωρίς σώμα χρησιμοποιήστε το hash της κενής συμβολοσειράς.
Ενώστε τη χρονοσήμανση, τη μέθοδο με κεφαλαία, τη διαδρομή και το hash του σώματος με αλλαγές γραμμής (\n). Η διαδρομή είναι η πλήρης διαδρομή χωρίς κεντρικό υπολογιστή και συμβολοσειρά ερωτήματος, π.χ. /api/v4/orders.
Υπολογίστε το HMAC-SHA256 αυτής της συμβολοσειράς με το μυστικό κλειδί API ως κλειδί και γράψτε το ως 64 πεζούς χαρακτήρες hex.
Στείλτε την ως X-CEK-Signature μαζί με τις υπόλοιπες κεφαλίδες.
Υπογράψτε ακριβώς τα byte που στέλνετε: σειριοποιήστε το JSON μία φορά και χρησιμοποιήστε την ίδια συμβολοσειρά για το hash και για το σώμα του αιτήματος.
Παράδειγμα (με πλασματικά στοιχεία)
Όλες οι τιμές είναι πλασματικές: μυστικό sk_test_EXAMPLE, χρονοσήμανση 1790000000. Χρησιμοποιήστε τις τιμές από τον δικό σας πίνακα.
Κάθε παράδειγμα αναζητά ένα BIN και δημιουργεί μια παραγγελία στο sandbox. Τα διαπιστευτήρια διαβάζονται από μεταβλητές περιβάλλοντος· μην βάζετε ποτέ το μυστικό στον κώδικα. Κάθε εκτέλεση χρησιμοποιεί νέο 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
GET/api/v4/bins/{bin}
Αναζητήστε το BIN μιας κάρτας
Καλέστε το μόλις ο πελάτης πληκτρολογήσει τα πρώτα 6 ψηφία. Η απάντηση είναι ένα στιγμιότυπο που σας βοηθά να επιλέξετε ανάμεσα στη δική σας φόρμα κάρτας και τη φιλοξενούμενη σελίδα· η τελική απόφαση λαμβάνεται κατά τη δημιουργία της παραγγελίας.
Παράμετροι
Πεδίο
Τύπος
Υποχρεωτικό
Περιγραφή
bin
string · path
Υποχρεωτικό
Τα πρώτα έξι ψηφία του αριθμού της κάρτας.
amount
string · query
Υποχρεωτικό
Ποσό παραγγελίας σε TRY με 2 δεκαδικά. Η καταλληλότητα αξιολογείται για αυτό το ποσό.
Απόκριση · 200
Πεδίο
Τύπος
Περιγραφή
bin
string
Το BIN που ζητήσατε.
bank_name
string | null
Τράπεζα έκδοσης, ή null αν δεν είναι γνωστή.
network
string | null
Δίκτυο κάρτας: visa, mastercard, amex, troy ή null.
card_type
string | null
credit, debit, prepaid ή null.
card_program
string | null
Όνομα προγράμματος κάρτας ή null.
country
string | null
Χώρα έκδοσης (ISO alpha-2) ή null.
domestic
boolean
true για κάρτα που εκδόθηκε στην Τουρκία.
payment_mode
string
direct: συλλέξτε εσείς την κάρτα. hosted: χρησιμοποιήστε τη φιλοξενούμενη σελίδα.
Ένα endpoint δημιουργεί την παραγγελία και στις δύο λειτουργίες. Το σώμα αποφασίζει: στείλτε card για παραγγελία direct ή μόνο bin για παραγγελία hosted. Αν στείλετε και τα δύο επιστρέφεται 422.
Παραγγελία direct (με κάρτα)
Πεδίο
Τύπος
Υποχρεωτικό
Περιγραφή
merchant_order_id
string
Υποχρεωτικό
Η αναφορά της παραγγελίας σας: 1–64 χαρακτήρες από A-Z a-z 0-9 . _ -, μοναδική ανά λογαριασμό και περιβάλλον.
Το merchant_order_id πρέπει να είναι μοναδικό ανά λογαριασμό και περιβάλλον. Είναι η μόνη προστασία από διπλότυπα.
Η επανάληψη ενός αιτήματος με το ίδιο merchant_order_id και το ίδιο περιεχόμενο δεν ξεκινά νέα πληρωμή και επιστρέφει την παραγγελία όπως είναι τώρα. Διαφορετικό περιεχόμενο επιστρέφει 409 με κωδικό 0055503.
Ένα πεδίο που δεν υπάρχει στο σχήμα επιστρέφει 422 με κωδικό 1000001.
Σε απόκριση 4xx δεν έχει εισπραχθεί πληρωμή. Χρησιμοποιήστε νέο merchant_order_id για την επόμενη προσπάθεια. Και μια παραγγελία failed είναι τελική.
Μετά από απόκριση 5xx ή λήξη χρόνου, η πληρωμή μπορεί να έχει ήδη ξεκινήσει. Μη δημιουργήσετε νέα παραγγελία: επαναλάβετε το ίδιο αίτημα αμετάβλητο ή διαβάστε την παραγγελία με GET. Αφήστε τουλάχιστον 30 δευτερόλεπτα πριν λήξει ο χρόνος.
Αν το BIN είναι εκείνη τη στιγμή hosted, ακόμη και ένα αίτημα με card επιστρέφει requires_hosted και τα στοιχεία της κάρτας δεν χρησιμοποιούνται.
Το αντικείμενο παραγγελίας
Οι κλήσεις δημιουργίας και ανάγνωσης επιστρέφουν την παραγγελία μέσα στο data.
Πεδίο
Τύπος
Περιγραφή
order_code
string
Αριθμός παραγγελίας, 9 ψηφία ως συμβολοσειρά. Είναι η τιμή που έρχεται στη διεύθυνση επιστροφής.
merchant_order_id
string
Το δικό σας merchant_order_id.
status
string
requires_action, requires_hosted, pending, paid ή failed.
amount
string
Ποσό παραγγελίας, 2 δεκαδικά.
currency
string
Πάντα TRY.
redirect_url
string | null
Διεύθυνση 3-D Secure. Συμπληρωμένο όσο η κατάσταση είναι requires_action.
hosted_url
string | null
Διεύθυνση της φιλοξενούμενης σελίδας. Συμπληρωμένο όσο η κατάσταση είναι requires_hosted.
hosted_expires_at
string | null
Η στιγμή που το hosted_url παύει να λειτουργεί (ISO 8601, UTC).
failure_reason_code
string | null
Κωδικός αιτίας για παραγγελία failed· βλ. Λόγοι απόρριψης. Μπορεί να είναι null αν δεν έγινε ποτέ απόπειρα πληρωμής.
failure_message
string | null
Σύντομο κείμενο για τον πελάτη σχετικά με την αιτία. Είναι πάντα στα τουρκικά· διακλαδώστε με βάση τον κωδικό.
card
object | null
Περίληψη κάρτας (holder_name, bin, last_four, network, program, issuer_bank_name). Διαθέσιμη αμέσως στο direct· στο hosted όταν γίνει γνωστό το αποτέλεσμα.
created_at
string
Ώρα δημιουργίας (ISO 8601, +00:00).
completed_at
string | null
Ώρα ολοκλήρωσης ή null όσο η παραγγελία είναι ανοιχτή.
Οδηγήστε τον πελάτη στο redirect_url για 3-D Secure.
requires_hosted
Όχι
Οδηγήστε τον πελάτη στο hosted_url.
pending
Όχι
Η τράπεζα δεν έχει απαντήσει ακόμη ή ο πελάτης έφυγε από την οθόνη. Περιμένετε το webhook.
paid
Ναι
Η πληρωμή εισπράχθηκε.
failed
Ναι
Η πληρωμή δεν ολοκληρώθηκε. Το failure_reason_code εξηγεί γιατί.
Ο πελάτης έχει 10 λεπτά για το 3-D Secure. Η φιλοξενούμενη σελίδα λειτουργεί μέχρι το hosted_expires_at. Μια πληρωμή στην οποία η τράπεζα δεν απαντά ποτέ γίνεται paid ή failed μέσα σε 24 ώρες και μια ληγμένη πληρωμή γίνεται failed.
Σπάνια, μια παραγγελία failed μπορεί να γίνει αργότερα paid. Θεωρήστε το paid ως την αλήθεια.
Ανάγνωση παραγγελίας
GET/api/v4/orders/{merchantOrderId}
Διαβάστε μια παραγγελία
Επιστρέφει το ίδιο αντικείμενο με τη δημιουργία, χωρίς σώμα αιτήματος. Το redirect_url επιστρέφεται μέχρι να λήξει το παράθυρο 3-D Secure και το hosted_url μέχρι το hosted_expires_at.
Παράμετροι
Πεδίο
Τύπος
Υποχρεωτικό
Περιγραφή
merchantOrderId
string · path
Υποχρεωτικό
Το merchant_order_id που χρησιμοποιήσατε κατά τη δημιουργία της παραγγελίας.
Το webhook είναι το κύριο κανάλι. Αν καθυστερεί, διαβάστε την παραγγελία το πολύ κάθε 30 δευτερόλεπτα.
Μετά τη δημιουργία της παραγγελίας ο πελάτης ολοκληρώνει την πληρωμή σε σελίδα που δεν είναι δική σας. Στείλτε τον εκεί με κανονική ανακατεύθυνση ολόκληρης σελίδας.
3-D Secure (`requires_action`)
Ανακατευθύνετε το πρόγραμμα περιήγησης του πελάτη στο redirect_url.
Ο πελάτης επιβεβαιώνει την πληρωμή στην τράπεζά του.
Όταν η πληρωμή γίνει τελική, ο πελάτης οδηγείται στη διεύθυνση επιστροφής και εσείς λαμβάνετε το webhook.
Φιλοξενούμενη σελίδα (`requires_hosted`)
Οδηγήστε τον πελάτη στο hosted_url. Η ανακατεύθυνση ολόκληρης σελίδας λειτουργεί πάντα· η εμφάνιση σε modal πλαίσιο εξαρτάται από την άδεια της σελίδας, γι' αυτό κρατήστε την ανακατεύθυνση ως εφεδρική λύση.
Ο πελάτης εισάγει την κάρτα και ολοκληρώνει το 3-D Secure σε εκείνη τη σελίδα. Μια αποτυχημένη προσπάθεια δεν κλείνει την παραγγελία· ο πελάτης μπορεί να ξαναδοκιμάσει εκεί.
Αν ο πελάτης κλείσει τη σελίδα, το ίδιο hosted_url την ανοίγει ξανά μέχρι το hosted_expires_at.
Όταν τελειώσει η πληρωμή, ολόκληρη η σελίδα οδηγείται στη διεύθυνση επιστροφής. Επιβεβαιώστε το αποτέλεσμα με το webhook.
Διεύθυνση επιστροφής
Όταν μια παραγγελία γίνει paid ή failed, ο πελάτης οδηγείται στην αντίστοιχη διεύθυνση επιστροφής που ορίσατε στον πίνακα. Αν έχει οριστεί διεύθυνση webhook, στη διεύθυνση προστίθενται οι παρακάτω υπογεγραμμένες παράμετροι.
Πεδίο
Τύπος
Περιγραφή
order_code
string
Ο 9ψήφιος αριθμός παραγγελίας. Η διεύθυνση δεν περιέχει merchant_order_id· αντιστοιχίστε την παραγγελία με αυτή την τιμή.
status
string
paid ή failed.
failure_code
string
Κωδικός αιτίας, μόνο για παραγγελία failed που έχει αιτία. Διαφορετικά η παράμετρος λείπει.
ts
integer
Χρόνος Unix σε δευτερόλεπτα κατά τη δημιουργία της διεύθυνσης.
key_version
integer
Έκδοση του μυστικού σας. Αυξάνεται κάθε φορά που το ανανεώνετε.
sig
string
Υπογραφή, 64 πεζοί χαρακτήρες hex.
Κανονική συμβολοσειρά υπογραφής
sig = hex(HMAC-SHA256(key = API_SECRET, message = ts + "." + order_code + "." + status + "." + failure_code))
Επαληθεύστε το sig πριν εμπιστευτείτε τις παραμέτρους. Αν λείπει το failure_code, βάλτε κενή συμβολοσειρά στη θέση του· η τελεία παραμένει.
Απορρίψτε ένα ts παλαιότερο από 10 λεπτά.
Η επιστροφή δείχνει μόνο πού βρίσκεται ο πελάτης. Το αποτέλεσμα είναι τελικό όταν φτάσει το webhook ή το επιβεβαιώσει το GET /api/v4/orders/{merchantOrderId}.
Αν δεν έχει οριστεί διεύθυνση webhook, η διεύθυνση ανοίγει χωρίς αυτές τις παραμέτρους.
Τα αποτελέσματα παραδίδονται ως υπογεγραμμένα αιτήματα JSON POST στη διεύθυνση webhook HTTPS του περιβάλλοντος.
Συμβάν
Πότε αποστέλλεται
checkout.order.succeeded
Η παραγγελία είναι paid.
checkout.order.failed
Η παραγγελία είναι failed.
Κεφαλίδα
Κεφαλίδα
Περιγραφή
X-CEK-Timestamp
Χρόνος Unix σε δευτερόλεπτα. Απορρίψτε απόκλιση πάνω από 300 δευτερόλεπτα.
X-CEK-Signature
Υπογραφή: hex του HMAC-SHA256 της χρονοσήμανσης, μιας τελείας και του ακατέργαστου σώματος, με το μυστικό κλειδί API ως κλειδί.
X-CEK-Event-Id
UUID του συμβάντος. Ίδιο με το event_id στο σώμα και δεν αλλάζει στις επαναλήψεις.
X-CEK-Environment
sandbox ή live.
Επαληθεύστε κάθε ειδοποίηση
Διαβάστε το ακατέργαστο σώμα πριν αναλύσετε το JSON.
Υπολογίστε την υπογραφή πάνω στο X-CEK-Timestamp, μια τελεία και το ακατέργαστο σώμα και συγκρίνετέ την με το X-CEK-Signature σε σταθερό χρόνο.
Απορρίψτε το αίτημα αν η χρονοσήμανση διαφέρει από το ρολόι σας περισσότερο από 300 δευτερόλεπτα.
Αφαιρέστε τα διπλότυπα με το X-CEK-Event-Id. Η παράδοση γίνεται τουλάχιστον μία φορά και μπορεί να είναι εκτός σειράς. Αν έχετε δει checkout.order.succeeded για μια παραγγελία, αγνοήστε κάθε checkout.order.failed γι' αυτήν.
Απαντήστε με οποιοδήποτε 2xx μέσα σε 10 δευτερόλεπτα (3 για τη σύνδεση). Οι ανακατευθύνσεις (3xx), τα 4xx, 5xx και οι λήξεις χρόνου θεωρούνται αποτυχίες και επαναλαμβάνονται.
Παραδείγματα επαλήθευσης υπογραφής
<?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.
Επαναλήψεις
Μια αποτυχημένη παράδοση επαναλαμβάνεται μετά από τις παρακάτω αναμονές, μετρημένες από την προηγούμενη αποτυχία, έως 11 προσπάθειες συνολικά.
Προσπάθεια
Αναμονή πριν
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)
Σώμα
Το σώμα περιέχει schema_version (3), event_id, event_type, ένα αντικείμενο order, ένα αντικείμενο merchant και την περίληψη της κάρτας στο payment_details.card. Ένα συμβάν failed προσθέτει failure_code, failure_reason_code και failure_message. Αγνοήστε τα πεδία που δεν χρησιμοποιείτε· μπορεί να προστεθούν νέα.
Το failure_reason_code είναι ένας από αυτούς τους κωδικούς. Βασίστε τη λογική σας στον κωδικό· το failure_message είναι κείμενο στα τουρκικά για τον πελάτη.
Κωδικός
Περιγραφή
payment_declined
Η τράπεζα έκδοσης δεν ενέκρινε την πληρωμή.
do_not_honor
Η τράπεζα απέρριψε χωρίς συγκεκριμένο λόγο.
insufficient_funds
Το υπόλοιπο ή το όριο της κάρτας δεν επαρκεί για αυτή την πληρωμή.
card_limit_reached
Η κάρτα έφτασε το όριο συναλλαγών της.
card_details_invalid
Ο αριθμός κάρτας δεν έγινε δεκτός.
card_expiry_invalid
Η ημερομηνία λήξης δεν είναι έγκυρη.
card_security_code_invalid
Ο κωδικός ασφαλείας (CVV) είναι λάθος.
card_not_active
Η κάρτα δεν είναι ενεργοποιημένη για πληρωμές.
card_brand_not_supported
Αυτή η μάρκα κάρτας δεν υποστηρίζεται για αυτή την πληρωμή.
three_ds_failed
Το 3-D Secure δεν μπόρεσε να ολοκληρωθεί.
security_declined
Ένας έλεγχος ασφαλείας σταμάτησε την πληρωμή. Πείτε στον πελάτη μόνο ότι η τράπεζα δεν την ενέκρινε.
payment_not_permitted
Αυτή η κάρτα δεν μπορεί να χρησιμοποιηθεί για αυτή την πληρωμή.
amount_not_allowed
Το ποσό είναι εκτός του εύρους που δέχεται αυτή η κάρτα.
currency_not_supported
Το νόμισμα δεν υποστηρίζεται για αυτή την πληρωμή.
payment_details_rejected
Τα στοιχεία πληρωμής απορρίφθηκαν.
duplicate_payment
Φαίνεται ότι η ίδια πληρωμή έχει ήδη εισπραχθεί.
cancelled_by_customer
Ο πελάτης ακύρωσε πριν ολοκληρωθεί η πληρωμή.
payment_timed_out
Ο χρόνος της πληρωμής έληξε.
bank_unavailable
Δεν είναι δυνατή η επικοινωνία με την τράπεζα αυτή τη στιγμή. Δοκιμάστε ξανά σύντομα.
bank_requested_retry
Η τράπεζα ζήτησε επανάληψη της πληρωμής.
provider_unavailable
Η πληρωμή δεν μπόρεσε να ολοκληρωθεί αυτή τη στιγμή. Δοκιμάστε ξανά σύντομα.
Η λίστα μπορεί να επεκταθεί. Αντιμετωπίστε έναν κωδικό που δεν γνωρίζετε όπως το payment_declined. Στο webhook, το security_declined αναφέρεται ως payment_declined στο failure_code.
Κωδικοί σφαλμάτων
Τα σφάλματα χρησιμοποιούν τη μορφή application/problem+json. Διακλαδώστε με βάση το code· τα title και detail είναι για ανθρώπους.
Πεδίο
Τύπος
Περιγραφή
type, title, status, detail
string · integer
Τυπικά πεδία του problem.
instance
string
Η διαδρομή του αιτήματος.
code
string
Ο 7ψήφιος κωδικός σφάλματος από τον παρακάτω πίνακα.
correlation_id
string
Αναφέρετέ το όταν επικοινωνείτε με την υποστήριξη. Αποστέλλεται και ως X-Correlation-Id.
metadata
object
developer_detail, retry_class και audit_event: πρόσθετες πληροφορίες για αποσφαλμάτωση.
amount_limit
object
Μόνο στο 0055235: min και max του επιτρεπόμενου ποσού, ή null για όριο όγκου.
errors
object
Μόνο στο 1000001: τα πεδία που απέτυχαν στον έλεγχο.
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"
}
}
Οι αποκρίσεις σφάλματος φέρουν επίσης X-Correlation-Id, X-RateLimit-Limit, X-RateLimit-Remaining και Content-Language· το 429 προσθέτει Retry-After.
Οι έλεγχοι εκτελούνται με αυτή τη σειρά: σχήμα (1000001), ποσό (0055235), BIN (0055233, 0055230), κάρτα (0055234).
Κωδικός
HTTP
Σημασία
0055501
403
Η λίστα επιτρεπόμενων IP είναι κενή ή το αίτημα προέρχεται από διεύθυνση που δεν περιλαμβάνεται.
0055502
401 / 403
Λάθος κλειδί, περιβάλλον ή κωδικός εμπόρου, ανενεργός λογαριασμός ή μη ανοιγμένη πρόσβαση στο Direct API.
0055504
401
Η χρονοσήμανση δεν έχει 10 ψηφία ή αποκλίνει πάνω από 300 δευτερόλεπτα, ή η υπογραφή δεν είναι 64 πεζοί χαρακτήρες hex.
0055505
401
Η υπογραφή δεν μπόρεσε να επαληθευτεί.
1000001
422
Το σώμα δεν ταιριάζει με το σχήμα: πεδίο που λείπει, λάθος ή άγνωστο, ή card μαζί με bin. Το errors απαριθμεί τα πεδία.
1000005
429
Υπέρβαση ορίου ρυθμού. Περιμένετε το διάστημα του Retry-After.
0055001
409
Ο λογαριασμός δεν είναι ακόμη έτοιμος να δέχεται πληρωμές· λείπει ένα βήμα ρύθμισης.
0055002
403
Οι πληρωμές με κάρτα είναι απενεργοποιημένες για τον λογαριασμό.
0055235
422
Το ποσό δεν είναι έγκυρο ή είναι εκτός ορίων (το amount_limit είναι συμπληρωμένο), ή επιτεύχθηκε ημερήσιο, εβδομαδιαίο ή μηνιαίο όριο όγκου (το amount_limit είναι null).
0055230
409
Δεν υπάρχει διαθέσιμο κανάλι πληρωμής για αυτό το BIN.
0055233
409
Το BIN δεν αναγνωρίστηκε.
0055210
422
Το bin δεν είναι ακριβώς έξι ψηφία.
0055234
409
Ο αριθμός κάρτας ή η ημερομηνία λήξης δεν είναι έγκυρα.
0055221
409
Συμπληρώθηκε το ημερήσιο όριο προσπαθειών ή ποσού αυτής της κάρτας.
0055231
409
Αυτή η κάρτα δεν μπορεί να χρεωθεί αυτή τη στιγμή.
0055503
409
Το ίδιο merchant_order_id χρησιμοποιήθηκε με διαφορετικό περιεχόμενο ή το πρώτο αίτημα εξακολουθεί να επεξεργάζεται.
0055402
404
Δεν υπάρχει παραγγελία με αυτό το merchant_order_id.
0055404
409
Μόνο για το endpoint αποτελέσματος sandbox: η παραγγελία δεν είναι ούτε pending ούτε requires_hosted.
Sandbox
Το sandbox χρησιμοποιεί τα ίδια endpoints με κλειδί sandbox και X-CEK-Environment: sandbox. Δεν μετακινούνται πραγματικά χρήματα.
Το X-CEK-Merchant-Code είναι υποχρεωτικό στο sandbox. Χρησιμοποιούνται οι διευθύνσεις webhook και επιστροφής του περιβάλλοντος sandbox.
Χρησιμοποιήστε οποιαδήποτε μελλοντική ημερομηνία λήξης και οποιοδήποτε CVV 3 ψηφίων.
Τα όρια όγκου μετρούν μόνο παραγγελίες sandbox.
BIN
HTTP
Κωδικός αποτελέσματος
435509
200
direct
540061
200
hosted
979210
409
0055230
οποιοδήποτε άλλο BIN
409
0055233
Αριθμός κάρτας
Συμπεριφορά
4355090000044242
requires_action. Εγκρίνετε τη δοκιμαστική οθόνη 3-D Secure για να γίνει paid.
4355090000005151
requires_action. Μετά το 3-D Secure η παραγγελία γίνεται failed με insufficient_funds.
4355090000089999
requires_action. Μετά το 3-D Secure η παραγγελία μένει pending· κλείστε την με το endpoint αποτελέσματος sandbox.
4355090000040000
Γίνεται αμέσως failed με payment_declined, πριν το 3-D Secure.
οποιαδήποτε άλλη έγκυρη κάρτα
requires_action. Στη δοκιμαστική οθόνη 3-D Secure επιλέγετε το αποτέλεσμα: επιτυχία δίνει paid, αποτυχία failed (payment_declined), λήξη χρόνου αφήνει pending.
Ένας μη έγκυρος αριθμός κάρτας (Luhn) ή μια ημερομηνία λήξης στο παρελθόν επιστρέφει 409 με κωδικό 0055234. Μια παραγγελία hosted χρησιμοποιεί το BIN 540061· στη δοκιμαστική της σελίδα επιλέγετε επιτυχία (paid) ή αποτυχία (failed με insufficient_funds). Όσο βρίσκεται ακόμη σε requires_hosted, μπορείτε να την κλείσετε και με το endpoint αποτελέσματος sandbox.
Κλείνει μια παραγγελία sandbox που είναι pending ή, σε παραγγελία hosted, περιμένει ακόμη τη σελίδα hosted (requires_hosted). Το webhook και η διεύθυνση επιστροφής λειτουργούν όπως στο live.
Πεδίο
Τύπος
Υποχρεωτικό
Περιγραφή
result
string
Υποχρεωτικό
paid ή failed (μια αποτυχημένη παραγγελία παίρνει την αιτία payment_declined).