Bei der Direct API ist das Kartenformular Teil Ihrer Website, und Ihr Server übermittelt die Kartendaten an Payniko. Der Browser des Zahlers wird nur für 3-D Secure und für hosted-BINs zu Payniko weitergeleitet. Verwenden Sie die Direct API, wenn sich das Kartenformular auf Ihrer eigenen Seite befinden muss.
Integrationsschritte
BIN prüfen
Sobald der Zahler sechs Ziffern eingegeben hat, rufen Sie GET /api/payments/bin/{bin6} auf. payment_mode gibt an, ob Sie die vollständigen Kartendaten in Ihrem Formular erfassen oder den Zahler auf die Zahlungsseite weiterleiten.
Zahlung erstellen
Rufen Sie POST /api/payments/direct mit den Kartendaten auf, bei einer hosted-BIN nur mit bin6.
3-D Secure abschließen
Lautet der Status requires_action, leiten Sie den Browser auf next_action.url weiter. Anschließend kehrt der Browser mit signierten Parametern zu Ihrer success_url oder fail_url zurück.
Ergebnis bestätigen
Warten Sie auf den Webhook payment.succeeded oder payment.failed, oder rufen Sie GET /api/payments/{payment_id} auf. Führen Sie die Bestellung nur bei succeeded aus.
Zahlungsablauf
Your server Payniko Payer's browser
1 GET /api/payments/bin/{bin6} ───► payment_mode: direct | hosted
2 POST /api/payments/direct ─────► 200 requires_action + next_action.url
(or processing / succeeded / failed)
3 redirect to next_action.url ──────────────────────────────────► 3-D Secure page
───────────────────────────► success_url / fail_url
4 webhook ◄─────────────────────── payment.succeeded / payment.failed
GET /api/payments/{payment_id} ──► current status
Voraussetzungen
Die Direct API wird pro Konto freigeschaltet. Ohne diese Freischaltung werden Anfragen mit 4030055502 abgelehnt.
Vor dem Erstellen der Zahlung gibt die BIN-Prüfung an, ob die Karte in Ihrem Formular bezahlt werden kann (direct) oder ob die Zahlung auf der Zahlungsseite von Payniko abgeschlossen werden muss (hosted). Durch die Prüfung entsteht keine Belastung.
Parameter
Feld
Typ
Erforderlich
Beschreibung
Beispiel
bin6
string · path
Erforderlich
Erste sechs Ziffern der Karte.
540061
amount
string · query
Erforderlich
Betrag mit zwei Nachkommastellen, zum Beispiel 100.00. Wird mit Ihren Kontolimits abgeglichen.
100.00
currency
string · query
Optional
USD, EUR, AED oder TRY. Standardmäßig die Hauptwährung des Systems. Verwenden Sie beim Erstellen der Zahlung dieselbe Währung.
USD
Andere Abfrageparameter werden abgelehnt. Die Abfrageparameter sind nicht Bestandteil der Signatur.
./payniko.sh GET '/api/payments/bin/540061?amount=100.00¤cy=USD'
Liefert { "data": ... } mit den folgenden Feldern:
Feld
Typ
Beschreibung
bin6
string
Die übermittelten sechs Ziffern.
bank
string | null
Kartenausgebende Bank, sofern bekannt.
network
string | null
Kartennetzwerk, sofern bekannt.
visamastercardamextroy
card_type
string | null
Kartentyp, sofern bekannt.
creditdebitprepaid
card_program
string | null
Kartenprogramm. Derzeit immer null.
country
string | null
Ausgabeland (ISO 3166-1 alpha-2), sofern bekannt.
domestic
boolean
true bei einer inländischen Karte; andernfalls oder wenn unbekannt false.
payment_mode
string | null
direct: Erfassen Sie die vollständigen Kartendaten. hosted: Leiten Sie den Zahler auf die Zahlungsseite weiter. Jeder andere Wert, einschließlich null: Erstellen Sie keine Zahlung.
directhosted
Umgang mit der Antwort
direct: Erfassen Sie die vollständigen Kartendaten und erstellen Sie die Zahlung mit card.
hosted: Erstellen Sie die Zahlung nur mit bin6. Der Zahler gibt die Kartendaten auf der Zahlungsseite von Payniko ein.
Jeder andere payment_mode oder ein Kartenfehler mit 409 (0055230, 0055231, 0055233): Erstellen Sie keine Zahlung und bitten Sie den Zahler mit einer neutralen Meldung um eine andere Karte, da es sich nicht um eine Ablehnung durch die Bank handelt. 4220055235 weist auf ein Problem mit dem Betrag hin. Bei 429 oder 5xx wiederholen Sie dieselbe Prüfung später.
Beispiel
import {api} from './payniko.mjs';
let mode = null;
try {
const lookup = await api('GET', '/api/payments/bin/411111?amount=100.00¤cy=USD');
mode = lookup.data.payment_mode;
} catch (error) {
// No answer, 429 or 5xx: temporary. Try the same lookup again later.
if (error.status === undefined || error.status === 429 || error.status >= 500) throw error;
// Other 4xx: error.problem.code says why, for example 0055233.
}
if (mode !== 'direct' && mode !== 'hosted') {
// Also null: do not create a payment. Ask for another card with a neutral message.
}
Liefert immer 200 mit der aktuellen Zahlung, auch wenn dieselbe Anfrage erneut gesendet wird. Eine Kartenablehnung ist kein HTTP-Fehler; die Antwort lautet 200 mit status: failed.
Anfrage-Body
Feld
Typ
Erforderlich
Beschreibung
Beispiel
merchant_order_id
string
Erforderlich
Ihre Bestellreferenz: 1–64 Zeichen aus A-Z a-z 0-9 . _ -. Eine Zahlung pro Referenz und Umgebung; bei der Eindeutigkeitsprüfung wird nicht zwischen Groß- und Kleinschreibung unterschieden.
ORDER-2026-002
amount
string
Erforderlich
Betrag mit zwei Nachkommastellen, zum Beispiel "100.00", innerhalb Ihrer Kontolimits.
"100.00"
currency
string
Optional
Standardmäßig die Hauptwährung des Systems. Verwenden Sie dieselbe Währung wie bei der BIN-Prüfung.
USDEURAEDTRY
USD
webhook_url
string · uri
Erforderlich
Öffentliche HTTPS-URL auf Port 443, die die Webhooks für diese Zahlung empfängt, höchstens 500 Zeichen. Lokale Adressen werden nicht akzeptiert, auch nicht in der Sandbox; der Hostname muss zum Zeitpunkt der Anfrage auf eine öffentliche IP-Adresse auflösen, andernfalls 4229000008.
https://merchant.example/webhooks/payments
success_url
string · uri
Erforderlich
Weiterleitungsziel für den Browser, wenn die Zahlung erfolgreich war. HTTPS, höchstens 2048 Zeichen, ohne Benutzerinformationen und Fragment. In der Sandbox werden zusätzlich http://localhost, 127.0.0.1 und [::1] akzeptiert.
https://merchant.example/payment/success
fail_url
string · uri
Erforderlich
Weiterleitungsziel für den Browser in allen anderen Fällen. Es gelten dieselben Regeln wie für success_url.
https://merchant.example/payment/failure
customer
object
Erforderlich
Kontaktdaten des Zahlenden; sie werden für das Zahlungsergebnis, den Beleg und die Prüfschritte der Bank verwendet. Nur email und phone werden akzeptiert; jedes andere Feld innerhalb von customer wird mit 4221000001 abgelehnt.
{ … }
customer.email
string
Erforderlich
Die echte E-Mail-Adresse des Zahlenden: RFC 5322, höchstens 254 Zeichen, Domain mit Punkt. Sie wird in Kleinbuchstaben gespeichert.
Die echte Telefonnummer des Zahlenden im internationalen Format (+ Ländervorwahl, E.164), zum Beispiel +37061234567. Regel für die Türkei: +90 gefolgt von der 10-stelligen nationalen Nummer (5xx mobil, 2xx, 3xx, 4xx Festnetz, 8xx Sonderrufnummern). Eine Nummer ohne Ländervorwahl wird als türkische Nummer (+90) behandelt.
+37061234567
card
object
Bedingt erforderlich
Die vom Zahler eingegebenen Kartendaten (siehe Tabelle unten). Senden Sie entweder card oder bin6, nicht beides.
{ … }
bin6
string
Bedingt erforderlich
Nur bei einer hosted-BIN: die ersten sechs Ziffern. Der Zahler gibt die Kartendaten auf der Zahlungsseite ein. Eine Anfrage für eine direct-BIN, die nur bin6 enthält, liefert 4090055230.
540061
card.number
string
Erforderlich
13–19 Ziffern ohne Leerzeichen, mit gültiger Prüfziffer (15 Ziffern bei American Express).
4355090000044242
card.holder_name
string
Erforderlich
Name auf der Karte, 2–120 Zeichen.
JONAS KAZLAUSKAS
card.expiry_month
string
Erforderlich
Zwei Ziffern, 01 bis 12.
12
card.expiry_year
string
Erforderlich
Zwei Ziffern, zum Beispiel 30. Die Karte darf nicht abgelaufen sein, und das Jahr darf höchstens 30 Jahre in der Zukunft liegen.
30
card.cvv
string
Erforderlich
Genau 3 Ziffern, bei American Express 4 Ziffern. Niemals speichern oder protokollieren.
123
Alle Kartenwerte sind Zeichenketten. Unbekannte Felder innerhalb von card werden abgelehnt.
Bei einer hosted-BIN fragt die Zahlungsseite den Zahlenden nicht erneut nach E-Mail-Adresse und Telefonnummer. Aus Datenschutzgründen wird customer weder im Zahlungsobjekt noch in Webhooks zurückgegeben.
Feld
Grund
Meldung in errors
customer.email
Ungültiges Format oder länger als 254 Zeichen.
customer.email must be a valid e-mail address (max 254 characters).
customer.email
Platzhalter-, Beispiel- oder Wegwerfadresse.
customer.email must be the payer's real address; placeholder, example or disposable addresses (example.com, test@…, mailinator…) are not accepted.
customer.phone
Keine gültige Nummer im internationalen Format.
customer.phone must be a valid phone number in international format (e.g. +37061234567).
customer.phone
Wiederholtes oder aufeinanderfolgendes Ziffernmuster.
customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted.
customer.phone
Türkische Nummer nicht in der Form +90 gefolgt von 10 Ziffern, die mit 2, 3, 4, 5 oder 8 beginnen.
customer.phone for Türkiye must be +90 followed by a 10-digit number starting with 2, 3, 4, 5 or 8 (e.g. +905321234567).
HTTP 422 · 1000001
{
"type": "https://payniko.com/problems/1000001",
"title": "Validation failed",
"status": 422,
"detail": "The submitted fields are not valid.",
"code": "1000001",
"correlation_id": "4c7a1e9d-2b3f-4d85-9a6e-0f1b2c3d4e5f",
"errors": {
"customer.email": [
"customer.email must be the payer's real address; placeholder, example or disposable addresses (example.com, test@…, mailinator…) are not accepted."
],
"customer.phone": [
"customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted."
]
}
}
Wird eine Anfrage wegen customer abgelehnt, zeigen Sie dem Zahlenden den Grund klar an (zum Beispiel: Geben Sie Ihre eigene E-Mail-Adresse oder Telefonnummer ein) und lassen Sie ihn das Feld korrigieren, bevor Sie die Zahlung erneut erstellen. Die Direct API gibt die Meldungen in errors übersetzt zurück; die Tabelle zeigt den englischen Text.
Payniko-Zahlungslink-Seiten wenden dieselben Regeln auf die dort vom Zahlenden eingegebene E-Mail-Adresse und Telefonnummer an und zeigen dem Zahlenden den Grund einer Ablehnung.
Leiten Sie den Browser auf next_action.url weiter (3-D Secure oder, bei einer hosted-BIN, die Zahlungsseite).
processing
Warten Sie auf den Webhook und erstellen Sie keine neue Zahlung.
succeeded
Führen Sie die Bestellung aus.
failed
Zeigen Sie failure.message an und bitten Sie den Zahler um eine andere Karte.
Der Aufruf kann einige Sekunden dauern, während Payniko mit der Bank kommuniziert. Verwenden Sie ein Timeout von mindestens 60 Sekunden und senden Sie die Anfrage nach einem Timeout mit demselben Idempotency-Key und Body erneut. Solange die erste Anfrage noch läuft (bis zu 3 Minuten), liefert die Wiederholung 4090055503 mit metadata.retry_class: retry_same_request: Warten Sie einige Sekunden und senden Sie dieselbe Anfrage erneut.
Bei einer hosted-BIN wird eine mitgesendete card ignoriert, und der Zahler setzt den Vorgang auf der Zahlungsseite fort.
Existiert für die merchant_order_id bereits eine Zahlung oder wird derselbe Idempotency-Key mit einem anderen Body gesendet, wird die Anfrage mit 4090055503 abgelehnt.
Ungültige Kartendaten werden mit 4090055234 abgelehnt. metadata.developer_detail gibt das betroffene Feld an: card_number_invalid, card_expiry_invalid oder card_cvv_invalid. Bitten Sie den Zahler, die Kartendaten erneut einzugeben.
Fehlende oder ungültige Kontaktdaten werden mit 4221000001 abgelehnt; errors nennt customer, customer.email oder customer.phone.
3. 3-D Secure und Browser
Lautet der Status requires_action, muss der Browser des Zahlers next_action.url öffnen; dort führt die Bank 3-D Secure durch, oder der Zahler setzt den Vorgang auf der Zahlungsseite fort.
Leiten Sie die gesamte Seite auf next_action.url weiter oder öffnen Sie die URL in einem neuen Tab; beide Methoden werden immer unterstützt.
Die Seite fragt die Kartendaten nicht erneut ab; sie erfasst nur die Browserdaten, die die Bank benötigt.
Verwenden Sie immer die neueste next_action.url vor deren next_action.expires_at (dem Ende des 3-D-Secure-Schritts). Öffnen Sie niemals eine frühere URL erneut.
Anschließend wird der Browser mit signierten Parametern zu success_url (nur bei succeeded) oder zu fail_url weitergeleitet (siehe Rücksprung-URL). Rufen Sie danach die Zahlung ab.
Einbettung in Frames
Die 3-D-Secure-Seite kann nicht in einem Frame angezeigt werden; leiten Sie die gesamte Seite weiter oder öffnen Sie einen neuen Tab. Nur die Zahlungsseite einer hosted-BIN darf in einen Frame eingebettet werden, ausschließlich mit Live-Schlüsseln und nur auf genau dem Origin der success_url der Zahlung. Die Zahlungsseite der Hosted API kann niemals in einen Frame eingebettet werden.
Die Zahlungsseite einer hosted-BIN bettet die eigene sichere Zahlungs- und 3-D-Secure-Oberfläche des Zahlungsdienstleisters ein und kann als iFrame in Ihrem eigenen Zahlungsformular angezeigt werden.
Wenn Sie die Zahlungsseite einer hosted-BIN in einen Frame einbetten, dienen die Nachrichten, die sie an Ihr Fenster sendet, nur als Signal, die Zahlung zu prüfen. Akzeptieren Sie diese Nachrichten nur vom Origin von next_action.url und von Ihrem Frame, ignorieren Sie ihren Inhalt und lassen Sie Ihren Server die Zahlung abrufen.
Browser
// Only if you show the payment page of a hosted BIN in a frame on the exact origin of success_url.
// nextActionUrl: next_action.url from your server.
const pageOrigin = new URL(nextActionUrl).origin;
window.addEventListener('message', (event) => {
if (event.origin !== pageOrigin || event.source !== frame.contentWindow) return;
// A wake-up call only: ignore event.data. Ask your server to read the payment.
fetch('/checkout/ORDER-2026-002/refresh', {method: 'POST', credentials: 'same-origin'});
});
Zahlungsobjekt
Aufrufe zum Erstellen und Abrufen liefern dieses Objekt in data, und jeder Zahlungs-Webhook enthält es. Seine Struktur ist bei der Hosted API und der Direct API identisch.
Feld
Typ
Beschreibung
payment_id
string · LUVEI-ABCD-123456
Payniko-ID der Zahlung . Speichern Sie sie zusammen mit Ihrer Bestellung.
merchant_order_id
string
Ihre Bestellreferenz, genau wie übermittelt.
flow
string
hosted für die Hosted API, direct für die Direct API.
hosteddirect
status
string
Aktueller Status der Zahlung; siehe Statustabelle unten.
Währung des Betrags; die Hauptwährung des Systems, wenn keine angegeben wurde.
environment
string
Umgebung des Schlüssels, mit dem die Zahlung erstellt wurde.
sandboxlive
next_action
object | null
Die nächste vom Zahler erforderliche Aktion. Nur gesetzt, solange status den Wert requires_action hat.
next_action.type
string
Immer redirect.
next_action.url
string · uri
Öffnen Sie diese URL im Browser des Zahlers genau so, wie sie zurückgegeben wird. Erstellen oder verändern Sie sie niemals selbst.
next_action.expires_at
string · date-time
Frist für die Verwendung der URL. Bei der Direct API ist dies das Ende des 3-D-Secure-Schritts und kann vor dem eigenen expires_at der Zahlung liegen; bei der Hosted API ist es die Frist der Zahlung. Nach Ablauf geht die Zahlung in ihren nächsten Status über, und next_action wird null.
Grund für das Fehlschlagen. Nur gesetzt, wenn status den Wert failed hat.
failure.code
string
Code des Fehlergrunds zur Verwendung in Ihrer Programmlogik; siehe Liste unten.
failure.message
string
Kurze Meldung, die sich zur Anzeige für den Zahler als reiner Text eignet.
created_at
string · date-time
Zeitpunkt, zu dem die Zahlung erstellt wurde.
expires_at
string · date-time
Frist für den Abschluss der Zahlung.
completed_at
string · date-time | null
Zeitpunkt, zu dem die Zahlung einen Endstatus erreicht hat.
next_action kann selbst bei requires_action kurzzeitig null sein; rufen Sie die Zahlung in diesem Fall erneut ab. Die vollständige Kartennummer, der CVV, das Ablaufdatum und der Name des Karteninhabers werden nie zurückgegeben. card.network und card.bank können null sein, wenn sie unbekannt sind.
Die beim Erstellen der Zahlung gesendeten customer-Daten sind nicht Teil dieses Objekts und erscheinen nicht in Webhooks.
Der Browser muss next_action.url für 3-D Secure oder für die Zahlungsseite einer hosted-BIN öffnen.
Leiten Sie den Zahler zu next_action.url weiter.
processing
Die Antwort der Bank steht noch aus. Dieser Status kann auch direkt in der Antwort auf die Erstellungsanfrage zurückgegeben werden.
Warten Sie auf den Webhook und erstellen Sie keine neue Zahlung. Antwortet die Bank nie, kann die Zahlung bis zum Abgleich in processing bleiben und endet dann als failed oder expired.
succeeded
Die Gelder wurden eingezogen.
Führen Sie die Bestellung aus.
failed
Die Zahlung wurde nicht abgeschlossen. failure enthält den Grund.
Zeigen Sie failure.message an. Für einen erneuten Versuch erstellen Sie eine neue Zahlung mit einer neuen merchant_order_id.
expired
Der Zahler hat die Zahlung nicht vor expires_at abgeschlossen.
Wenn der Zahler weiterhin zahlen möchte, erstellen Sie eine neue Zahlung.
cancelled
Die Zahlung wurde abgebrochen.
Führen Sie die Bestellung nicht aus.
Szenario
Status
Ereignis
Nach 3-D Secure genehmigt
requires_action → succeeded
payment.succeeded
Abgelehnt
failed
payment.failed
Antwort der Bank ausstehend
processing
Keines bis zum endgültigen Ergebnis
Keine Antwort der Bank; Zeitüberschreitung beim Versuch
processing → failed / expired
payment.failed
Der Zahler hat nicht begonnen, und die Frist ist abgelaufen (bricht der Zahler die Zahlung vorzeitig ab, kann sie mit failed enden)
expired
payment.failed
Behandeln Sie jeden hier nicht aufgeführten Status wie processing und rufen Sie die Zahlung erneut ab.
Fehlergründe
Eine Zahlung mit dem Status failed enthält failure.code und failure.message. Verwenden Sie den Code in Ihrer Programmlogik und zeigen Sie die Meldung dem Zahler als reinen Text an.
Code
Bedeutung
payment_declined
Die Bank hat die Zahlung abgelehnt.
do_not_honor
Die Bank hat die Zahlung ohne Angabe von Gründen abgelehnt.
insufficient_funds
Die Karte weist keine ausreichende Deckung auf.
card_limit_reached
Ein Kartenlimit wurde erreicht.
card_details_invalid
Die Kartendaten sind ungültig.
card_expiry_invalid
Das Ablaufdatum ist ungültig oder die Karte ist abgelaufen.
card_security_code_invalid
Der CVV ist ungültig.
card_not_active
Die Karte ist gesperrt oder nicht aktiv.
card_brand_not_supported
Dieses Kartennetzwerk wird nicht unterstützt.
three_ds_failed
Die 3-D-Secure-Authentifizierung ist fehlgeschlagen oder wurde nicht abgeschlossen.
payment_not_permitted
Die Karte ist für diese Zahlungsart nicht zugelassen.
amount_not_allowed
Der Betrag ist für diese Karte nicht zulässig.
currency_not_supported
Die Währung wird für diese Karte nicht unterstützt.
payment_details_rejected
Die Bank hat die Zahlungsdaten abgelehnt.
duplicate_payment
Die Bank hat die Zahlung als Duplikat eingestuft.
payment_timed_out
Die Zahlung wurde nicht rechtzeitig abgeschlossen.
bank_unavailable
Die Bank war nicht erreichbar.
bank_requested_retry
Die Bank hat um einen erneuten Versuch zu einem späteren Zeitpunkt gebeten.
service_unavailable
Die Zahlung konnte derzeit nicht verarbeitet werden.
Es können neue Codes hinzukommen; behandeln Sie jeden unbekannten Code wie payment_declined. failure.message kann den Originaltext der Bank enthalten; andernfalls ist die Meldung in Webhooks auf Englisch und richtet sich bei GET nach Accept-Language.
Rücksprung-URL
Wenn der Zahler zurückkehrt, wird der Browser nur dann zu success_url weitergeleitet, wenn die Zahlung succeeded ist, in allen anderen Fällen zu fail_url. An die URL werden signierte Parameter angehängt. Prüfen Sie diese, um die richtige Seite anzuzeigen, und führen Sie die Bestellung erst aus, wenn der Webhook oder ein GETsucceeded bestätigt.
Direct API: Ist das Ergebnis noch nicht endgültig, wenn der Zahlende von 3-D Secure zurückkehrt, wird der Browser nicht zu fail_url weitergeleitet; der Zahlende bleibt auf einer neutralen Payniko-Ergebnisseite. Verlassen Sie sich für das Ergebnis auf den Webhook oder ein GET.
Feld
Beschreibung
payment_id
Die Zahlungs-ID .
merchant_order_id
Ihre Bestellreferenz. Sie ist nicht signiert: Verwenden Sie sie nur, um die Bestellung zu finden, und gleichen Sie anhand von payment_id ab.
status
Zahlungsstatus zum Zeitpunkt der Weiterleitung, mit denselben Werten wie bei GET. Auf success_url immer succeeded. Auf fail_url in der Regel failed, expired oder cancelled; bei der Hosted API kann er requires_action oder processing sein, wenn der Zahlende die Seite verlassen hat, bevor die Zahlung einen Endstatus erreicht hat, zum Beispiel über "Zurück zum Shop".
failure_code
Nur vorhanden, wenn status den Wert failed hat; identisch mit failure.code.
ts
Unix-Zeit, zu der der Rücksprung-Link erstellt wurde. Der Zahler kann den Link später öffnen; ein älterer ts allein ist daher kein Hinweis auf eine Fälschung.
key_version
Gibt den Schlüssel an, mit dem der Link signiert wurde: 1 für den ersten in dieser Umgebung erstellten Schlüssel, 2 für den nächsten und so weiter. Für eine bestimmte Zahlung ändert sich der Wert nie.
sig
HMAC-SHA256 als Hexadezimalwert in Kleinbuchstaben, 64 Zeichen.
Zu signierende Zeichenkette
ts + "." + payment_id + "." + status + "." + failure_code // failure_code empty: keep the last dot
Wählen Sie den geheimen Schlüssel des durch key_version angegebenen Schlüssels. Dies ist der Schlüssel, mit dem die Zahlung erstellt wurde, auch wenn Schlüssel seitdem ersetzt wurden.
Bilden Sie die Zeichenkette aus den URL-decodierten Werten. Fehlt failure_code, bleibt der abschließende Punkt erhalten.
Berechnen Sie den HMAC-SHA256 und vergleichen Sie ihn in konstanter Zeit mit sig. Stellen Sie sicher, dass payment_id zu einer Ihrer Zahlungen gehört.
Rufen Sie die Zahlung mit GET /api/payments/{payment_id} ab (oder warten Sie auf den Webhook), bevor Sie die Bestellung ausführen.
Ihre eigenen Abfrageparameter bleiben erhalten. Enthält die URL bereits einen Parameter mit einem der oben genannten Namen, wird er ersetzt.
Konnte der Link aufgrund eines vorübergehenden Fehlers nicht signiert werden, enthält die URL keinen dieser Parameter. Behandeln Sie das Ergebnis als unbekannt und rufen Sie die Zahlung ab.
// Node.js 20+: check the signed parameters on success_url / fail_url.
import {createHmac, timingSafeEqual} from 'node:crypto';
// query: URLSearchParams of the return URL.
// secretFor(keyVersion): the API secret of that key, or undefined if you do not know it.
export function verifyReturn(query, secretFor) {
const sig = query.get('sig') ?? '';
const secret = secretFor(query.get('key_version'));
if (!secret || !/^[0-9a-f]{64}$/.test(sig)) return false;
const signed = [query.get('ts') ?? '', query.get('payment_id') ?? '',
query.get('status') ?? '', query.get('failure_code') ?? ''].join('.');
const expected = createHmac('sha256', secret).update(signed, 'utf8').digest();
return timingSafeEqual(expected, Buffer.from(sig, 'hex'));
}
// Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
<?php
// PHP 8.1+: check the signed parameters on success_url / fail_url.
// $secretFor(string $keyVersion): the API secret of that key, or null if you do not know it.
function payniko_verify_return(array $query, callable $secretFor): bool
{
$sig = (string) ($query['sig'] ?? '');
$secret = $secretFor((string) ($query['key_version'] ?? ''));
if (!$secret || !preg_match('/^[0-9a-f]{64}$/', $sig)) return false;
$signed = implode('.', [(string) ($query['ts'] ?? ''), (string) ($query['payment_id'] ?? ''),
(string) ($query['status'] ?? ''), (string) ($query['failure_code'] ?? '')]);
return hash_equals(hash_hmac('sha256', $signed, $secret), $sig);
}
// $ok = payniko_verify_return($_GET, fn (string $v) => $secretsByKeyVersion[$v] ?? null);
// Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
# Python 3.9+: check the signed parameters on success_url / fail_url.
import hashlib, hmac, re
def verify_return(query, secret_for) -> bool:
# query: dict-like (for example Flask request.args); secret_for(key_version) -> secret or None
sig = query.get("sig", "")
secret = secret_for(query.get("key_version"))
if not secret or not re.fullmatch(r"[0-9a-f]{64}", sig):
return False
signed = ".".join([query.get("ts", ""), query.get("payment_id", ""),
query.get("status", ""), query.get("failure_code", "")])
expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
# Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
Rücksprung-URLs · Beispiel
https://merchant.example/payment/success?payment_id=LUVEI-PAYN-654321&merchant_order_id=ORDER-2026-001&status=succeeded&ts=1793001234&key_version=1&sig=200e8a5d4a7a03ead362a2c076408f9d8764f8f636f58bd4c17d78a50d59eb8a
string to sign: 1793001234.LUVEI-PAYN-654321.succeeded.
https://merchant.example/payment/failure?payment_id=LUVEI-PAYN-654321&merchant_order_id=ORDER-2026-001&status=failed&failure_code=insufficient_funds&ts=1793001234&key_version=1&sig=1cd0a104ec4ec4f5b2933d48d14c0eb33d949dd7f27936a067ab8f68b413b70d
string to sign: 1793001234.LUVEI-PAYN-654321.failed.insufficient_funds
secret used in this example (fake): example_secret_not_real
GET/api/payments/{payment_id}
Zahlung abrufen
Liefert { "data": Payment } mit dem aktuellen Status. Das Abrufen verändert die Zahlung nie; Sie können diesen Endpoint daher so oft wie erforderlich aufrufen: nach dem Rücksprung, wenn ein Webhook unstimmig erscheint, oder nach einer Zeitüberschreitung.
Parameter
Feld
Typ
Erforderlich
Beschreibung
payment_id
string · path
Erforderlich
Die payment_id, die beim Erstellen der Zahlung zurückgegeben wurde. Jeder Schlüssel aus derselben Umgebung kann die Zahlung abrufen.
./payniko.sh GET '/api/payments/3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35'
Prüfen Sie jeden Webhook: Der HMAC-SHA256 von X-Timestamp + "." + raw body, berechnet mit dem geheimen Schlüssel des API-Schlüssels, mit dem die Zahlung erstellt wurde, muss X-Signature entsprechen, und X-Timestamp muss innerhalb von 300 Sekunden liegen. Speichern Sie jede event_id nur einmal und antworten Sie umgehend mit 2xx. Codebeispiele und das Wiederholungsverhalten finden Sie unter Webhooks.
Fehler
Code
HTTP
Bedeutung
Empfohlene Maßnahme
1000001
422
Die Anfrage ist ungültig: ein unbekanntes Feld, ein ungültiges Format, Abfrageparameter bei einem POST, ein nicht unterstützter Abfrageparameter oder ein Body, der kein JSON-Objekt ist. errors listet die betroffenen Felder auf.
Korrigieren Sie die aufgeführten Felder.
1000009
422
Idempotency-Key fehlt oder ist ungültig.
Senden Sie bei jedem POST einen Idempotenzschlüssel mit 8–128 Zeichen aus A-Z a-z 0-9 _ -.
0055502
401 / 403
Zugriff verweigert. 401: X-Api-Key oder X-Merchant-Code fehlt, ein Header ist doppelt vorhanden oder fehlerhaft, der Schlüssel ist unbekannt oder widerrufen, der Händlercode stimmt nicht überein, oder X-Environment wurde gesendet und passt nicht zum Schlüssel. 403: Das Konto oder der Zugang zur Direct API ist nicht aktiv.
Prüfen Sie die Header und den API-Schlüssel. Wiederholen Sie die Anfrage nicht automatisch.
0055503
409
Konflikt in der Direct API: Für diese merchant_order_id existiert bereits eine Zahlung, der Idempotency-Key wurde mit einem anderen Body oder für eine Hosted-Zahlung verwendet, oder die ursprüngliche Anfrage wird noch verarbeitet.
Rufen Sie die Zahlung per GET ab. Senden Sie nur eine identische Anfrage erneut.
0055210
422
Die BIN ist nicht sechsstellig.
Senden Sie die ersten sechs Ziffern der Kartennummer.
0055234
409
Die Kartendaten sind ungültig: Kartennummer, Ablaufdatum oder CVV. metadata.developer_detail benennt das Feld: card_number_invalid, card_expiry_invalid oder card_cvv_invalid.
Bitten Sie den Zahler, die Kartendaten erneut einzugeben.
0055233
409
Die BIN ist unbekannt.
Bitten Sie den Zahler um eine andere Karte. Es handelt sich nicht um eine Ablehnung durch die Bank.
0055230
409
Diese Karte kann für diese Zahlung nicht verwendet werden.
Bitten Sie den Zahler um eine andere Karte. Es handelt sich nicht um eine Ablehnung durch die Bank.
0055231
409
Diese Karte wird für diese Zahlung nicht akzeptiert.
Bitten Sie den Zahler um eine andere Karte.
0055221
409
Ein Limit für diese Karte wurde erreicht.
Verwenden Sie eine andere Karte oder wiederholen Sie den Vorgang zu einem späteren Zeitpunkt.
0055235
422
Der Betrag wird nicht akzeptiert (Direct API und BIN-Prüfung). amount_limit enthält min und max, sofern verfügbar.
Passen Sie den Betrag an.
0055001
409
Ihr Konto ist für diese Zahlung nicht bereit. Mit einem Sandbox-Schlüssel bedeutet dies auch, dass die Sandbox-Simulation vorübergehend nicht verfügbar ist (developer_detail: sandbox_unavailable), gleichbedeutend mit 9000013.
Wenden Sie sich an den Support.
1000002
404
Nicht gefunden: Der Pfad ist unbekannt, oder dieses API-Produkt ist auf der Plattform nicht aktiviert. Eine für Ihr Konto deaktivierte API liefert stattdessen 403 (9000040 bei der Hosted API, 0055502 bei der Direct API).
Prüfen Sie den Pfad; wenden Sie sich an den Support, wenn die API verfügbar sein sollte.
1000011
503
Der Dienst ist vorübergehend nicht verfügbar.
Wiederholen Sie dieselbe Anfrage. Rufen Sie bestehende Ressourcen ab, bevor Sie neue erstellen.
Fehler der Direct API enthalten zusätzlich metadata.retry_class (no_retry, retry_same_request, retry_after, read_order). Ein geschlossenes Browserfenster oder eine Zeitüberschreitung auf Ihrer Seite bedeutet nicht, dass die Zahlung fehlgeschlagen ist; rufen Sie die Zahlung ab. Die vollständige Liste finden Sie unter Fehlercodes.
Tests mit Sandbox-Karten
Mit einem Sandbox-Schlüssel verwenden BIN-Prüfung und Zahlung feste Testdaten: 435509 und 400000 sind direct-BINs, 540061 ist eine hosted-BIN. Verwenden Sie ein beliebiges Ablaufdatum in der Zukunft und einen beliebigen dreistelligen CVV. Einzelheiten finden Sie unter Sandbox und Testkarten.
Kartennummer
Ergebnis
4000000000000010
Ohne 3-D Secure genehmigt. Bei der Direct API liefert bereits die Antwort auf die Erstellungsanfrage succeeded.
4000000000000002 · 4355090000044242
3-D-Secure-Testschritt, anschließend succeeded.
4000000000000051 · 4355090000040000
Vor 3-D Secure abgelehnt (do_not_honor). Hosted API: Die Zahlung kann etwa 5 Minuten in requires_action bleiben und payment.updated senden, bevor sie als failed endet. Direct API: Die Antwort auf die Erstellung liefert bereits failed.
4000000000000093 · 4355090000005151
3-D-Secure-Testschritt, anschließend failed mit insufficient_funds.
4000000000000044
Keine Antwort der Bank und kein 3-D Secure; die Zahlung verbleibt im Status processing.
4355090000089999
Hosted API: processing ohne 3-D-Secure-Schritt. Direct API: Test-3-D-Secure-Schritt, danach processing.
Bei der Direct API liefert eine abgelehnte Testkarte direkt in der Antwort auf die Erstellung failed; ein zwischenzeitliches requires_action tritt nur auf der Zahlungsseite einer hosted-BIN auf. Stützen Sie Entscheidungen auf den Webhook oder ein GET.
Vollständiges Beispiel
Ein minimaler Node.js-Server mit BIN-Prüfung, Erstellung der Zahlung, Weiterleitung zu 3-D Secure, Rücksprung-URL und Webhook. Er verwendet die Hilfsfunktion für Anfragen und die Prüffunktionen aus Webhooks und Rücksprung-URL.
server.mjs
// server.mjs: Direct API from start to end (Node.js 20+, no dependencies).
import http from 'node:http';
import {randomUUID} from 'node:crypto';
import {api} from './payniko.mjs'; // request helper (Authentication)
import {verifyWebhook} from './verify-webhook.mjs'; // webhook check (Webhooks)
import {verifyReturn} from './verify-return.mjs'; // return URL check (Return URL)
const site = 'https://merchant.example';
const orders = new Map(); // use your database
const seenEvents = new Set(); // use your database
const readBody = async (req) => { const parts = []; for await (const p of req) parts.push(p); return Buffer.concat(parts); };
const reply = (res, data) => {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify(data.status === 'requires_action' && data.next_action
? {redirect: data.next_action.url} : {status: data.status, failure: data.failure}));
};
http.createServer(async (req, res) => {
const url = new URL(req.url, site);
// 1. BIN check, 2. create the payment. Your own HTTPS form posts the card here.
if (req.method === 'POST' && url.pathname === '/pay') {
const form = JSON.parse((await readBody(req)).toString('utf8'));
const bin6 = form.number.slice(0, 6);
let mode = null;
try {
mode = (await api('GET', `/api/payments/bin/${bin6}?amount=100.00¤cy=USD`)).data.payment_mode;
} catch (error) {
if (error.status === undefined || error.status >= 500 || error.status === 429) throw error;
}
if (mode !== 'direct' && mode !== 'hosted') { res.writeHead(422).end('Please use another card.'); return; }
const orderId = 'ORDER-' + Date.now();
const key = randomUUID();
orders.set(orderId, {key, status: 'new'});
const body = {
merchant_order_id: orderId, amount: '100.00', currency: 'USD',
customer: {email: form.email, phone: form.phone},
webhook_url: `${site}/webhooks/payniko`,
success_url: `${site}/payment/success`, fail_url: `${site}/payment/failure`,
};
if (mode === 'direct') {
body.card = {number: form.number, holder_name: form.holder_name,
expiry_month: form.expiry_month, expiry_year: form.expiry_year, cvv: form.cvv};
} else {
body.bin6 = bin6; // payer enters the card on the payment page
}
const {data} = await api('POST', '/api/payments/direct', body, key);
Object.assign(orders.get(orderId), {paymentId: data.payment_id, status: data.status});
reply(res, data); // 3. browser goes to next_action.url
return;
}
// Return URL: show a page, but trust only the webhook or a GET.
if (req.method === 'GET' && (url.pathname === '/payment/success' || url.pathname === '/payment/failure')) {
const signed = verifyReturn(url.searchParams, () => process.env.API_SECRET);
let status = 'unknown';
if (signed) status = (await api('GET', `/api/payments/${url.searchParams.get('payment_id')}`)).data.status;
res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
res.end(status === 'succeeded' ? 'Thank you, payment received.' : 'Payment not completed.');
return;
}
// Webhook: verify, store event_id once, answer 2xx fast.
if (req.method === 'POST' && url.pathname === '/webhooks/payniko') {
const raw = await readBody(req);
if (!verifyWebhook(raw, req.headers, process.env.API_SECRET)) { res.writeHead(401).end(); return; }
const event = JSON.parse(raw.toString('utf8'));
if (!seenEvents.has(event.event_id)) { // use a unique index in your database
seenEvents.add(event.event_id);
const order = orders.get(event.data.merchant_order_id);
if (order && event.event_type.startsWith('payment.')) order.status = event.data.status;
}
res.writeHead(204).end();
return;
}
res.writeHead(404).end();
}).listen(8080);