Zum Inhalt springen
Dokumentation

Erste Schritte

Payniko nimmt Kartenzahlungen über eine einzige signierte REST-API entgegen. Sie können den Zahler entweder auf die Zahlungsseite von Payniko weiterleiten (Hosted API) oder die Kartendaten in Ihrem eigenen Zahlungsformular erfassen (Direct API).

Basis-URL und Formate

Basis-URL
https://payniko.com/api. Der Pfad enthält keine Versionsangabe.
Format
JSON, UTF-8-codiert. Für Fehler wird application/problem+json verwendet.
Beträge
Zeichenketten mit zwei Nachkommastellen, zum Beispiel "100.00". Senden Sie Beträge niemals als JSON-Zahlen.
Währung
USD, EUR, AED oder TRY. Fehlt currency, gilt die Hauptwährung des Systems.
Zeitangaben
ISO 8601 in UTC mit Millisekunden, zum Beispiel 2026-11-01T12:00:00.000Z.
IDs
Zahlungs-, Erstattungs-, Auszahlungs- und Streitfall-IDs haben das Format LUVEI-ABCD-123456. Speichern Sie die vollständige ID.
Sprache
Meldungen werden in der über Accept-Language festgelegten Sprache zurückgegeben (en, tr, de, ar, es, el); Standard ist en. Codes und Feldnamen werden nie übersetzt.

API-Schlüssel erhalten

Neue Konten erhalten Sandbox- und Live-Schlüssel automatisch. Haben Sie bereits ein Konto, anmelden und öffnen Sie Zugangsschlüssel. Wenn Sie eine Kontodatei erhalten haben, melden Sie sich mit deren Benutzername und Passwort an; registrieren Sie sich nicht erneut.

1. Konto erstellen

  1. Konto erstellen. Geben Sie Ihre verwendete E-Mail-Adresse ein und klicken Sie auf Code senden. Tragen Sie den sechsstelligen E-Mail-Code auf der Registrierungsseite ein und klicken Sie auf Bestätigen. Prüfen Sie bei fehlender E-Mail Adresse und Spamordner; nach Ablauf des Countdowns können Sie einen neuen Code anfordern.

  2. Wählen Sie nach der E-Mail-Bestätigung die Landesvorwahl und geben Sie die Telefonnummer Ihres eigenen Telegram-Kontos ein. Klicken Sie daneben auf Bestätigen. Die Bestätigung erfolgt über Telegram; es wird keine SMS gesendet.

  3. Öffnen Sie den angezeigten Telegram-Link oder scannen Sie am Computer den QR-Code mit Ihrem Telefon. Drücken Sie im Bot Start und teilen Sie Ihre eigene Nummer über dessen Kontakt-Schaltfläche. Eine eingetippte Nummer oder ein fremder Kontakt genügt nicht. Kehren Sie zur Registrierung zurück und warten Sie auf die Bestätigung.

  4. Klicken Sie auf Weiter zu den Kontodaten. Erstellen Sie Benutzernamen und Passwort, wiederholen Sie das Passwort und schließen Sie die Bestätigungen ab. Klicken Sie auf Konto erstellen. Benutzername: 4–32 Zeichen, Kleinbuchstaben, Ziffern, Punkt, Unterstrich oder Bindestrich. Passwort: mindestens 12 Zeichen.

  5. Speichern Sie den anschließend angezeigten Wiederherstellungscode sicher. Er wird nur auf diesem Bildschirm angezeigt und ist nicht der API-Geheimschlüssel. Bestätigen Sie die Speicherung und klicken Sie auf Code gespeichert, weiter zum Panel.

2. Sandbox-Schlüssel und Händlercode abrufen

  1. Öffnen Sie Zugangsschlüssel und die Sandbox-Karte. Bei neuen Konten sind die Schlüssel bereits vorhanden. Fehlt bei einem älteren Konto ein aktiver Schlüssel, geben Sie optional eine Bezeichnung ein und klicken Sie auf Schlüssel erstellen. Eine IP-Registrierung ist nicht erforderlich.

  2. Übernehmen Sie die drei Werte aus der folgenden Tabelle. Mit dem Augensymbol zeigen Sie den geheimen Schlüssel an, mit dem Kopiersymbol kopieren Sie ihn. Der Kontoinhaber kann einen aktiven Geheimschlüssel später erneut anzeigen; dafür muss kein neuer Schlüssel erstellt werden.

  3. Übermitteln Sie die Werte sicher an Ihren Entwickler oder speichern Sie sie in der privaten Serverkonfiguration. API-Schlüssel und Geheimschlüssel müssen aus derselben Sandbox-Karte stammen. Der Geheimschlüssel gehört nicht in Browser-JavaScript, öffentliche Dateien oder URLs.

WertFundstelleVerwendung
HändlercodeSechsstelliger Code auf der KarteHeader X-Merchant-Code; für dasselbe Konto in Sandbox und Produktivbetrieb identisch.
API-SchlüsselAPI-Schlüssel der Sandbox-KarteHeader X-Api-Key; bestimmt die Umgebung.
Geheimer SchlüsselAugen-/Kopiersymbol derselben KarteSigniert Anfragen und prüft Webhook-Signaturen; wird nicht als Anfragenfeld gesendet.

Eine Testzahlung erstellen

Testen Sie die Integration ohne echte Geldbewegung in der Sandbox. Ein erfolgreicher Test ist keine Voraussetzung für Live-Schlüssel; er wird zur Prüfung von Signaturen, Weiterleitungen und Webhooks empfohlen.

  1. Verwenden Sie ein Serverbeispiel aus Anfrage-Hilfsfunktionen mit Ihrem Sandbox-API-Schlüssel, Sandbox-Geheimschlüssel und Händlercode. Es gibt keine gesonderte Sandbox-API-Domain: Die Basis-URL bleibt gleich; der Schlüssel bestimmt die Umgebung.

  2. Prüfen Sie unter Zahlungserlebnis die Sandbox-Betragsgrenzen und Währungen. Wählen Sie einen zulässigen Betrag, der nicht auf .77 endet; dieses Ende löst das Testszenario mit ausstehendem Ergebnis aus.

  3. Füllen Sie das Beispiel POST /api/payments/hosted aus: eindeutige merchant_order_id, Betrag, Währung, customer.email / customer.phone und drei Benachrichtigungs-/Rückleitungsadressen. Verwenden Sie erreichbare Kontaktdaten und die Telefonnummer mit Landesvorwahl. webhook_url muss auch in Sandbox öffentlich per HTTPS erreichbar sein; localhost und private Netze sind unzulässig. success_url / fail_url sind die Browser-Rückleitungsadressen.

  4. Senden Sie die signierte Anfrage POST /api/payments/hosted vom Server. Für einen neuen Test brauchen Sie eine neue Auftragsreferenz und einen neuen Idempotency-Key; bei identischer Wiederholung nach einem Verbindungsfehler behalten Sie denselben Wert. HTTP 201 bedeutet nur „Zahlung erstellt“, nicht „erfolgreich“.

  5. Öffnen Sie die vollständige next_action.url aus der Antwort im Browser. Verwenden Sie auf der Sandbox-Zahlungsseite Testkarte 4000000000000002, ein zukünftiges Ablaufdatum und eine dreistellige CVV, etwa 12/30 und 123. Schließen Sie den erfolgreichen/bestätigten Test-3D-Secure-Ablauf ab. Keine echte Karte verwenden. Weitere Szenarien: Testkarten.

  6. Fragen Sie nach der Rückleitung mit GET /api/payments/{payment_id} ab oder verarbeiten Sie einen signaturgeprüften Webhook. Der API-Status muss succeeded sein. requires_action / processing sind Zwischenstände; failed zählt nicht. Eine angezeigte Erfolgsseite allein bestätigt keine Zahlung.

  7. Prüfen Sie das Ergebnis unter Anfrageprotokoll. Ein erfolgreicher Eintrag schließt dieses Testszenario ab. Live verwendet ein eigenes Schlüsselpaar.

Produktivschlüssel erhalten

Sandbox und Live verwenden getrennte Schlüsselpaare. Weder eine IP-Registrierung noch eine erfolgreiche Sandbox-Zahlung ist für Live-Schlüssel erforderlich. Die Umgebungswahl im Panel ändert nicht Ihre Serverkonfiguration.

  1. Öffnen Sie Produktivzugriff und die Karte Produktiv. Neue Konten haben bereits Live-Schlüssel; fehlt bei einem älteren Konto ein Schlüssel, klicken Sie auf Schlüssel erstellen.

  2. Übernehmen Sie API-Schlüssel und Geheimnis aus derselben Live-Karte. Der Händlercode bleibt gleich. Speichern Sie das Geheimnis nur in der privaten Serverkonfiguration.

  3. Verwenden Sie dieses Produktiv-Schlüsselpaar in Ihrer Serverkonfiguration. Die API-URL bleibt gleich; Sandbox-Schlüssel bleiben für Tests gültig. Das Anzeigen einer Karte im Panel ändert Ihre Serverkonfiguration nicht.

  4. Prüfen Sie unter Zahlungserlebnis die produktiven Grenzen und Währungen. Ihre Schlüssel sind bereit. Produktivschlüssel starten den echten Zahlungsablauf; Testkarten weiterhin nur mit Sandbox-Schlüsseln verwenden.

Wenn ein Schritt nicht funktioniert

SituationNächster Schritt
E-Mail oder Telegram nicht bestätigtE-Mail-Code auf der Registrierungsseite eingeben. Im dort verlinkten Telegram-Bot die eingegebene eigene Nummer per Kontaktschaltfläche teilen. Abgelaufenen Link auf der Registrierungsseite erneuern.
Schlüssel kann nicht erstellt werdenPrüfen Sie, ob das Konto aktiv und API-Zahlungen freigeschaltet sind. Eine IP-Registrierung oder Sandbox-Zahlung ist nicht erforderlich.
Live-Schlüssel fehltÖffnen Sie die Live-Karte. Neue Konten haben bereits Schlüssel; erstellen Sie bei älteren Konten ohne aktiven Schlüssel einen neuen. Bei weiteren Problemen Support kontaktieren.
Geheimschlüssel nicht gespeichertAugen- oder Kopiersymbol der aktiven Schlüsselkarte verwenden. Wiederherstellungscode und API-Geheimschlüssel sind verschieden.
API-Zahlungen nicht sichtbarSupport kontaktieren, um den API-Zugriff prüfen zu lassen. Zahlungslinks und API-Zahlungen können getrennt freigeschaltet sein.

Endpoint-Übersicht

EndpointZweck
POST /api/payments/hostedHosted-API-Zahlung erstellen · Hosted API
GET /api/payments/{payment_id}Zahlung abrufen · Hosted API · Direct API
GET /api/payments/bin/{bin6}BIN prüfen (erste sechs Ziffern der Karte) · Direct API
POST /api/payments/directDirect-API-Zahlung erstellen · Direct API
GET /api/account/balanceKontoguthaben · Guthaben
GET /api/account/available-balanceAuszahlbares Guthaben · Guthaben
POST|GET /api/account/refundsRückerstattungen und Stornierungen · Rückerstattungen
POST|GET /api/account/withdrawalsUSDT-Auszahlungen · Auszahlungen
GET /api/account/disputesAnfechtungen und Antworten auf Anfechtungen · Anfechtungen
GET|POST /api/webhooks/deliveriesVerlauf der Webhook-Zustellungen und erneutes Senden · Zustellungen
POST /api/sandbox/disputesTestanfechtung erstellen (nur mit Sandbox-Schlüssel) · Sandbox

Alle Endpoints verwenden dieselbe Authentifizierung, und jeder POST erfordert zusätzlich einen Idempotency-Key.

Authentifizierung

Jede Anfrage enthält Ihren API-Schlüssel und eine HMAC-SHA256-Signatur, die mit Ihrem geheimen Schlüssel berechnet wird.

API-Schlüssel und geheimer Schlüssel

WertFormatBeschreibung
API-Schlüsselapi_ + 32 hexWird in X-Api-Key gesendet. Der Schlüssel bestimmt zugleich die Umgebung.
Geheimer Schlüssel64 hexSigniert Anfragen und prüft Webhook-Signaturen; wird nicht mitgesendet. Der Kontoinhaber kann den aktiven Geheimschlüssel im Panel über das Augen-/Kopiersymbol erneut anzeigen oder kopieren.
Händlercode[0-9]{6}Ihre Kontonummer, gesendet in X-Merchant-Code.

Jeder API-Schlüssel gehört zu genau einer Umgebung, Sandbox oder Live, und der Schlüssel bestimmt, in welcher Umgebung eine Anfrage ausgeführt wird: Verwenden Sie den Sandbox-Schlüssel für Tests und den Live-Schlüssel für echte Zahlungen. Der Header X-Environment ist optional; fehlt er, wird die Anfrage in der Umgebung des Schlüssels ausgeführt. Wird er gesendet, muss sein Wert sandbox oder live lauten (ohne Berücksichtigung der Groß- und Kleinschreibung) und mit der Umgebung des Schlüssels übereinstimmen; andernfalls wird die Anfrage mit HTTP 401 und dem Code 0055502 abgelehnt und nicht verarbeitet. Für die Authentifizierung erforderlich sind die Header X-Api-Key, X-Merchant-Code, X-Timestamp und X-Signature, bei jeder schreibenden Anfrage zusätzlich Idempotency-Key. Die Umgebung ist nicht Bestandteil der zu signierenden Zeichenkette. Jeder Antwort-Body, der environment enthält (Zahlungen, Guthaben, Rückerstattungen und weitere Ressourcen), gibt die verwendete Umgebung an.

Schlüsselrotation

  • Erstellen Sie einen neuen Schlüssel, wenn der geheime Schlüssel offengelegt worden sein könnte oder eine Person mit Zugriff darauf ausscheidet. Der bisherige Schlüssel verliert sofort seine Gültigkeit.
  • Webhooks und Rücksprung-Links einer Zahlung werden mit dem geheimen Schlüssel des API-Schlüssels signiert, mit dem die Zahlung erstellt wurde; dies gilt auch für Webhooks zu Rückerstattungen und Anfechtungen, die lange nach der Zahlung gesendet werden. Bewahren Sie den bisherigen geheimen Schlüssel auf, solange solche Ereignisse noch eintreffen können.

Anfrage-Header

HeaderErforderlichWert
X-Api-KeyErforderlichIhr API-Schlüssel.
X-Merchant-CodeErforderlichIhr sechsstelliger Händlercode.
X-TimestampErforderlichAktuelle Unix-Zeit in Sekunden (10 Ziffern).
X-SignatureErforderlichDie Signatur der Anfrage: 64 Hexadezimalzeichen in Kleinbuchstaben.
Idempotency-KeyBedingt erforderlichBei jedem POST erforderlich. Siehe Idempotency-Key.
Content-TypeBedingt erforderlichapplication/json bei Anfragen mit Body.
Accept-LanguageOptionalSprache der Fehlermeldungen: en (Standard), tr, de, ar, es oder el.

Senden Sie jeden X--Header genau einmal. Header-Werte dürfen keine Kommas, Zeilenumbrüche oder Leerzeichen enthalten.

Anfragesignatur

Die zu signierende Zeichenkette besteht aus vier Werten, die durch Zeilenvorschübe (LF) verbunden sind: dem Zeitstempel, der HTTP-Methode, dem Pfad und dem SHA-256-Hashwert des Bodys.

Zu signierende Zeichenkette

canonical   = X-Timestamp + "\n" + METHOD + "\n" + path + "\n" + hex(sha256(raw body))
X-Signature = hex(HMAC-SHA256(key = API secret, message = canonical))   // 64 lowercase hex characters
  1. Codieren Sie den Body einmalig als JSON und bilden Sie den SHA-256-Hashwert über genau diese Bytes (hexadezimal in Kleinbuchstaben). Bei einer Anfrage ohne Body bilden Sie den Hashwert der leeren Zeichenkette (e3b0c442…b855).

  2. Verwenden Sie den Pfad genau so, wie er in der URL steht, beginnend mit /api/ und ohne Abfrageparameter. Bei GET /api/payments/bin/540061?amount=100.00 wird beispielsweise /api/payments/bin/540061 signiert.

  3. Berechnen Sie den HMAC-SHA256 und verwenden Sie dabei den geheimen Schlüssel als HMAC-Schlüssel. Verwenden Sie die Zeichenkette des geheimen Schlüssels unverändert; decodieren Sie sie nicht.

  4. Senden Sie das Ergebnis in X-Signature und als Body der Anfrage genau die Bytes, über die der Hashwert gebildet wurde.

Zeitstempeltoleranz

X-Timestamp darf höchstens 300 Sekunden von der Serverzeit von Payniko abweichen, in beide Richtungen. Synchronisieren Sie Ihre Serveruhr per NTP. Bei einer größeren Abweichung wird die Anfrage mit 401 0055504 abgelehnt, und detail enthält die Serverzeit von Payniko.

Signaturbeispiel

Verwenden Sie diese Beispielwerte, um Ihre Implementierung zu testen. Mit diesem geheimen Schlüssel, diesem Zeitstempel und diesem Body muss Ihr Code genau diese Signatur erzeugen.

Geheimer Schlüssel (Beispiel)

e3c4a5b6d7e8f90112233445566778899aabbccddeeff0011223344556677889

Body (exakte Bytes)

{"merchant_order_id":"ORDER-2026-001","amount":"100.00","currency":"USD","webhook_url":"https://merchant.example/webhooks/payments","success_url":"https://merchant.example/payment/success","fail_url":"https://merchant.example/payment/failure","customer":{"email":"[email protected]","phone":"+37061234567"}}

sha256(body)

dca2bae6e50c13c26b22b57e3c31161ba3174147060fcbf22b2ba99cc13560ae

Zu signierende Zeichenkette

1793534400
POST
/api/payments/hosted
dca2bae6e50c13c26b22b57e3c31161ba3174147060fcbf22b2ba99cc13560ae

X-Signature

5522c27ff0b49ffa4c44712ed5db350c1c5a2cbfc0c568edbce39cff59ca620c

Anfrage-Header

POST /api/payments/hosted
X-Api-Key: api_0123456789abcdef0123456789abcdef
X-Merchant-Code: 123456
X-Timestamp: 1793534400
X-Signature: 5522c27ff0b49ffa4c44712ed5db350c1c5a2cbfc0c568edbce39cff59ca620c
Idempotency-Key: 6f1c2a9e-1d2b-4c1a-9f0e-3b7d5a2c8e41
Content-Type: application/json
Accept: application/json

Hilfsfunktionen für Anfragen

Fügen Sie eine dieser Hilfsfunktionen auf Ihrem Server hinzu. Sie liest API_KEY, API_SECRET und MERCHANT_CODE aus Umgebungsvariablen, signiert die Anfrage und sendet sie. Die übrigen Codebeispiele verwenden diese Hilfsfunktion.

#!/usr/bin/env bash
# payniko.sh - signed request helper (bash, curl, openssl).
# Usage: ./payniko.sh METHOD PATH [JSON_BODY] [IDEMPOTENCY_KEY]
# Reads API_KEY, API_SECRET and MERCHANT_CODE. The key decides sandbox or live.
# Optional: SANDBOX_SCENARIO adds the X-Sandbox-Scenario header (sandbox keys only).
set -euo pipefail
: "${API_KEY:?set API_KEY}" "${API_SECRET:?set API_SECRET}" "${MERCHANT_CODE:?set MERCHANT_CODE}"
BASE_URL="https://payniko.com"
method="$(printf '%s' "${1:?METHOD}" | tr '[:lower:]' '[:upper:]')"
target="${2:?PATH}"            # may include a query string; only the path is signed
body="${3-}"                   # sent byte-for-byte as hashed
idempotency_key="${4-}"
path="${target%%\?*}"
timestamp="$(date +%s)"
body_hash="$(printf '%s' "$body" | openssl dgst -sha256 | sed 's/^.* //')"
signature="$(printf '%s\n%s\n%s\n%s' "$timestamp" "$method" "$path" "$body_hash" \
  | openssl dgst -sha256 -hmac "$API_SECRET" | sed 's/^.* //')"
args=(-sS --fail-with-body -X "$method" "$BASE_URL$target"
  -H "X-Api-Key: $API_KEY" -H "X-Merchant-Code: $MERCHANT_CODE" -H "X-Timestamp: $timestamp"
  -H "X-Signature: $signature" -H "Accept: application/json" -H "Accept-Language: en")
if [ -n "$body" ]; then args+=(-H "Content-Type: application/json" --data-binary "$body"); fi
if [ -n "$idempotency_key" ]; then args+=(-H "Idempotency-Key: $idempotency_key"); fi
if [ -n "${SANDBOX_SCENARIO-}" ]; then args+=(-H "X-Sandbox-Scenario: $SANDBOX_SCENARIO"); fi
curl "${args[@]}"
echo
// payniko.mjs - signed request helper (Node.js 20+, no dependencies).
import {createHash, createHmac, randomUUID} from 'node:crypto';

const base = 'https://payniko.com';
const key = process.env.API_KEY ?? '';
const secret = process.env.API_SECRET ?? '';
const merchant = process.env.MERCHANT_CODE ?? '';
if (!/^api_[0-9a-f]{32}$/.test(key) || !/^[0-9a-f]{64}$/.test(secret) || !/^[0-9]{6}$/.test(merchant)) {
  throw new Error('Set API_KEY, API_SECRET and MERCHANT_CODE.');
}

export async function api(method, path, body, idempotencyKey, extraHeaders = {}) {
  const url = new URL(path, base);
  if (url.origin !== base || !url.pathname.startsWith('/api/')) throw new Error('Invalid API URL');
  method = method.toUpperCase();
  const raw = body === undefined ? '' : JSON.stringify(body);   // hash and send the same bytes
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const canonical = [timestamp, method, url.pathname,
    createHash('sha256').update(raw, 'utf8').digest('hex')].join('\n');
  const signature = createHmac('sha256', secret).update(canonical, 'utf8').digest('hex');
  const response = await fetch(url, {
    method,
    redirect: 'error',
    headers: {
      ...extraHeaders,
      'X-Api-Key': key, 'X-Merchant-Code': merchant,
      'X-Timestamp': timestamp, 'X-Signature': signature,
      'Accept': 'application/json', 'Accept-Language': 'en',
      ...(raw ? {'Content-Type': 'application/json'} : {}),
      ...(idempotencyKey ? {'Idempotency-Key': idempotencyKey} : {}),
    },
    ...(raw ? {body: raw} : {}),
  });
  const text = await response.text();
  const data = text ? JSON.parse(text) : null;
  if (!response.ok) {
    throw Object.assign(new Error(`${response.status} ${data?.code ?? ''} ${data?.detail ?? ''}`.trim()),
      {status: response.status, problem: data, correlationId: response.headers.get('x-correlation-id')});
  }
  return data;
}
export {randomUUID};
<?php
// payniko.php - signed request helper (PHP 8.1+ with the curl extension).
function payniko_api(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null, array $extraHeaders = []): ?array
{
    $base = 'https://payniko.com';
    $key = getenv('API_KEY') ?: '';
    $secret = getenv('API_SECRET') ?: '';
    $merchant = getenv('MERCHANT_CODE') ?: '';
    if (!preg_match('/^api_[0-9a-f]{32}$/', $key) || !preg_match('/^[0-9a-f]{64}$/', $secret)
        || !preg_match('/^[0-9]{6}$/', $merchant)) {
        throw new RuntimeException('Set API_KEY, API_SECRET and MERCHANT_CODE.');
    }
    $method = strtoupper($method);
    // Hash and send the same bytes.
    $raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
    $timestamp = (string) time();
    $canonical = implode("\n", [$timestamp, $method, explode('?', $path, 2)[0], hash('sha256', $raw)]);
    $headers = array_merge($extraHeaders, [
        'X-Api-Key: ' . $key,
        'X-Merchant-Code: ' . $merchant,
        'X-Timestamp: ' . $timestamp,
        'X-Signature: ' . hash_hmac('sha256', $canonical, $secret),
        'Accept: application/json',
        'Accept-Language: en',
    ]);
    if ($raw !== '') $headers[] = 'Content-Type: application/json';
    if ($idempotencyKey !== null) $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    $ch = curl_init($base . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_TIMEOUT => 60,
    ]);
    if ($raw !== '') curl_setopt($ch, CURLOPT_POSTFIELDS, $raw);
    $response = curl_exec($ch);
    if ($response === false) throw new RuntimeException('Network error: ' . curl_error($ch));
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $data = $response === '' ? null : json_decode($response, true);
    if ($status >= 400) {
        throw new RuntimeException($status . ' ' . ($data['code'] ?? '') . ' ' . ($data['detail'] ?? ''), $status);
    }
    return $data;
}
# payniko.py - signed request helper (Python 3.9+, standard library only).
import hashlib, hmac, json, os, re, time, urllib.error, urllib.request

BASE_URL = "https://payniko.com"
KEY = os.environ.get("API_KEY", "")
SECRET = os.environ.get("API_SECRET", "")
MERCHANT = os.environ.get("MERCHANT_CODE", "")
if not (re.fullmatch(r"api_[0-9a-f]{32}", KEY) and re.fullmatch(r"[0-9a-f]{64}", SECRET)
        and re.fullmatch(r"[0-9]{6}", MERCHANT)):
    raise SystemExit("Set API_KEY, API_SECRET and MERCHANT_CODE.")


def api(method, path, body=None, idempotency_key=None, extra_headers=None):
    method = method.upper()
    # Hash and send the same bytes.
    raw = b"" if body is None else json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    timestamp = str(int(time.time()))
    canonical = "\n".join([timestamp, method, path.split("?", 1)[0], hashlib.sha256(raw).hexdigest()])
    signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    headers = dict(extra_headers or {})
    headers.update({
        "X-Api-Key": KEY, "X-Merchant-Code": MERCHANT,
        "X-Timestamp": timestamp, "X-Signature": signature,
        "Accept": "application/json", "Accept-Language": "en",
    })
    if raw:
        headers["Content-Type"] = "application/json"
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    request = urllib.request.Request(BASE_URL + path, data=raw or None, method=method, headers=headers)
    try:
        with urllib.request.urlopen(request, timeout=60) as response:
            text = response.read()
            return json.loads(text) if text else None
    except urllib.error.HTTPError as error:
        problem = json.loads(error.read() or b"{}")
        raise RuntimeError(f"{error.code} {problem.get('code')} {problem.get('detail')}") from None

Einrichtung prüfen

Das Abrufen des Guthabens ist ein sicherer erster Aufruf, da dabei nichts erstellt wird.

./payniko.sh GET '/api/account/balance'
import {api} from './payniko.mjs';

const result = await api('GET', '/api/account/balance');
console.log(result);
<?php
require __DIR__ . '/payniko.php';

$result = payniko_api('GET', '/api/account/balance');
print_r($result);
from payniko import api

result = api("GET", "/api/account/balance")
print(result)

Wiederholt ungültige Signaturen sperren den Schlüssel vorübergehend: 20 Fehlversuche von derselben Adresse innerhalb von 15 Minuten sperren ihn für 15 Minuten, 300 Fehlversuche innerhalb einer Stunde für eine Stunde (429 1000005 mit Retry-After). Korrigieren Sie die Signaturlogik, bevor Sie es erneut versuchen.

Idempotency-Key

Jeder POST erfordert einen Idempotency-Key. Eine Anfrage, die mit demselben Idempotenzschlüssel erneut gesendet wird, wird nur einmal ausgeführt und liefert das ursprüngliche Ergebnis; dadurch sind Wiederholungen nach einer Zeitüberschreitung sicher.

  • Erzeugen Sie für jeden neuen Vorgang einen neuen zufälligen Idempotenzschlüssel, zum Beispiel eine UUID, und speichern Sie ihn zusammen mit Ihrer Bestellung, bevor Sie die Anfrage senden.
  • Senden Sie nach einer Zeitüberschreitung oder einem Netzwerkfehler denselben Body mit demselben Idempotenzschlüssel erneut.
  • Verwenden Sie einen Idempotenzschlüssel niemals mit einem anderen Body; eine solche Anfrage wird mit 409 abgelehnt (9000010, bei der Direct API 0055503).
  • Wird die ursprüngliche Anfrage noch verarbeitet, lautet die Antwort 409: 9000039 bei der Hosted API (mit integration.retryable: true) oder 0055503 mit metadata.retry_class: retry_same_request bei der Direct API. Senden Sie dieselbe Anfrage nach einer kurzen Wartezeit erneut.
  • Format: 8–128 Zeichen aus A-Z a-z 0-9 _ -. Ein fehlender oder ungültiger Idempotenzschlüssel führt zu 422 1000009.
  • Eine signierte Anfrage (X-Timestamp und X-Signature) wird 10 Minuten lang an den ersten Idempotency-Key gebunden, mit dem sie gesendet wurde; dieselbe Signatur mit einem anderen Schlüssel wird mit 409 9000010 abgelehnt. Erzeugen Sie für jeden neuen Vorgang einen neuen Zeitstempel und eine neue Signatur. Ist diese Prüfung vorübergehend nicht verfügbar, lautet die Antwort 503 1000011.

Anforderungen an Anfragen

  • Senden Sie nur dokumentierte Felder. Unbekannte Felder werden mit 422 1000001 abgelehnt.
  • POST-Anfragen dürfen keine Abfrageparameter enthalten. GET-Anfragen dürfen keinen Body enthalten und akzeptieren nur die dokumentierten Abfrageparameter.
  • Der Body muss ein einzelnes JSON-Objekt sein, das einschließlich des Objekts der obersten Ebene höchstens 7 Ebenen tief verschachtelt ist.
  • Ratenbegrenzungen gelten pro IP-Adresse (600 Anfragen pro Minute vor der Authentifizierung) und pro Schlüssel (300 Anfragen pro Minute). Warten Sie bei 429 (1000005) das Retry-After-Intervall ab und versuchen Sie es dann mit Backoff erneut. Jede Antwort enthält X-RateLimit-Limit und X-RateLimit-Remaining.
HeaderBedeutung
X-Correlation-IdEindeutige ID der Anfrage. Protokollieren Sie sie und geben Sie sie in Supportanfragen an.
Retry-AfterWartezeit in Sekunden. Bei 429 immer vorhanden; bei 503 vorhanden, sofern bekannt.
X-RateLimit-LimitAnfragelimit des aktuellen Fensters. Es gibt keinen Header X-RateLimit-Reset.
X-RateLimit-RemainingVerbleibende Anfragen im aktuellen Zeitfenster.
Content-LanguageSprache der Meldungen in der Antwort.

Authentifizierungsfehler

CodeHTTPBedeutungEmpfohlene Maßnahme
0055502401 / 403Zugriff 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.
0055504401X-Timestamp oder X-Signature fehlt oder ist fehlerhaft, oder Ihre Serveruhr weicht um mehr als 300 Sekunden von der Serverzeit von Payniko ab.Korrigieren Sie Ihre Serveruhr (NTP) und signieren Sie die Anfrage mit einem aktuellen Zeitstempel neu.
0055505401Die Signatur stimmt nicht mit der Anfrage überein.Bilden Sie den Hashwert über genau die Bytes des Bodys, die Sie senden, signieren Sie den Pfad ohne Abfrageparameter und verwenden Sie den korrekten geheimen Schlüssel.
9000002401Die Anfrage war zum Zeitpunkt der Verarbeitung bereits abgelaufen.Signieren Sie die Anfrage mit einem aktuellen Zeitstempel neu und senden Sie sie erneut.
1000009422Idempotency-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 _ -.
9000010409Dieser Idempotency-Key wurde bereits für eine andere Anfrage verwendet, oder dieselbe signierte Anfrage wurde mit einem anderen Idempotenzschlüssel erneut gesendet.Wiederholen Sie die Anfrage mit demselben Idempotenzschlüssel und demselben Body; verwenden Sie einen neuen Idempotenzschlüssel nur für einen neuen Vorgang.
1000005429Zu viele Anfragen oder eine vorübergehende Sperre nach wiederholten Signaturfehlern.Warten Sie das in Retry-After angegebene Intervall ab und wiederholen Sie die Anfrage dann mit zunehmenden Wartezeiten.

Hosted API

Ihr Server erstellt die Zahlung und leitet den Zahler auf die Zahlungsseite von Payniko weiter. Payniko erfasst die Kartendaten und führt 3-D Secure durch, sodass Kartendaten Ihre Systeme nie erreichen. Die Hosted API ist die einfachste Integrationsvariante.

Integrationsschritte

  1. Zahlung erstellen

    Ihr Server ruft POST /api/payments/hosted auf. Speichern Sie die zurückgegebene payment_id zusammen mit Ihrer Bestellung.

  2. Zahler weiterleiten

    Leiten Sie den Browser auf next_action.url weiter oder öffnen Sie die URL in einem neuen Tab; laden Sie sie niemals in einem iFrame.

  3. Zahlung durch den Zahler

    Der Zahler gibt die Kartendaten ein. Verlangt die Bank 3-D Secure, zeigt die Seite die 3-D-Secure-Abfrage an. Eine vorläufige Ablehnung in diesem Schritt ist nicht endgültig; die Zahlung erreicht einen Endstatus, ohne dass Sie tätig werden müssen.

  4. Rückkehr des Zahlers

    Die Seite zeigt das Ergebnis mit einem kurzen Countdown an und leitet den Browser anschließend mit signierten Parametern zu success_url (nur wenn die Zahlung succeeded ist) oder zu fail_url weiter. Siehe Rücksprung-URL.

  5. Ergebnis bestätigen

    Warten Sie auf den Webhook oder rufen Sie GET /api/payments/{payment_id} auf. Führen Sie die Bestellung erst aus, wenn der Status succeeded lautet.

Zahlungsablauf

Your server                          Payniko                        Payer's browser
 1  POST /api/payments/hosted  ─────►  201  status: requires_action
                               ◄─────  next_action.url
 2  redirect (303) ───────────────────────────────────────────────►  payment page
 3                                     card + 3-D Secure  ◄────────  payer pays
 4                                     ───────────────────────────►  success_url / fail_url
                                                                     (signed parameters)
 5  webhook  ◄───────────────────────  payment.succeeded / payment.failed
    GET /api/payments/{payment_id} ──►  current status
POST/api/payments/hostedIdempotency-Key erforderlich

Zahlung erstellen

Liefert 201 mit der neuen Zahlung. Wird dieselbe Anfrage mit demselben Idempotency-Key erneut gesendet, lautet die Antwort 200 mit derselben Zahlung.

Anfrage-Body

FeldTypErforderlichBeschreibungBeispiel
merchant_order_idstringErforderlichIhre Bestellreferenz: 1–64 Zeichen aus A-Z a-z 0-9 . _ -. Jede Referenz kann pro Umgebung für genau eine Zahlung verwendet werden, auch wenn diese Zahlung fehlgeschlagen oder abgelaufen ist. Bei der Eindeutigkeitsprüfung wird nicht zwischen Groß- und Kleinschreibung unterschieden: ORDER-1 und order-1 gelten als dieselbe Referenz.ORDER-2026-001
amountstringErforderlichPositiver Betrag mit zwei Nachkommastellen und ohne führende Nullen, zum Beispiel "100.00". Muss innerhalb Ihrer Kontolimits liegen."100.00"
currencystringOptionalFehlt die Angabe, gilt die Hauptwährung des Systems.
USDEURAEDTRY
USD
webhook_urlstring · uriErforderlichEndpunkt, der die Webhooks für diese Zahlung empfängt. Öffentliches HTTPS auf Port 443, höchstens 500 Zeichen, ohne Benutzerinformationen oder Fragment. Private und 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 422 9000008.https://merchant.example/webhooks/payments
success_urlstring · uriErforderlichWeiterleitungsziel für den Browser nach einer erfolgreichen Zahlung. 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_urlstring · uriErforderlichWeiterleitungsziel für den Browser, wenn die Zahlung fehlschlägt, abläuft oder abgebrochen wird. Es gelten dieselben Regeln wie für success_url.https://merchant.example/payment/failure
customerobjectErforderlichKontaktdaten 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 422 1000001 abgelehnt.{ … }
customer.emailstringErforderlichDie echte E-Mail-Adresse des Zahlenden: RFC 5322, höchstens 254 Zeichen, Domain mit Punkt. Sie wird in Kleinbuchstaben gespeichert.[email protected]
customer.phonestringErforderlichDie 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

Diese drei URLs gelten nur für diese Zahlung und werden nicht aus Ihren Panel-Einstellungen übernommen.

Die Zahlungsseite fragt den Zahlenden nicht erneut nach E-Mail-Adresse und Telefonnummer. Aus Datenschutzgründen wird customer weder im Zahlungsobjekt noch in Webhooks zurückgegeben.

FeldGrundMeldung in errors
customer.emailUngültiges Format oder länger als 254 Zeichen.customer.email must be a valid e-mail address (max 254 characters).
customer.emailPlatzhalter-, 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.phoneKeine gültige Nummer im internationalen Format.customer.phone must be a valid phone number in international format (e.g. +37061234567).
customer.phoneWiederholtes oder aufeinanderfolgendes Ziffernmuster.customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted.
customer.phoneTü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 Meldungen in errors sind auf Englisch und für Ihre Protokolle und den Support gedacht.

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.

Anfrage-Body · Beispiel

{
  "merchant_order_id": "ORDER-2026-001",
  "amount": "100.00",
  "currency": "USD",
  "webhook_url": "https://merchant.example/webhooks/payments",
  "success_url": "https://merchant.example/payment/success",
  "fail_url": "https://merchant.example/payment/failure",
  "customer": {
    "email": "[email protected]",
    "phone": "+37061234567"
  }
}
KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
./payniko.sh POST '/api/payments/hosted' '{
  "merchant_order_id": "ORDER-2026-001",
  "amount": "100.00",
  "currency": "USD",
  "webhook_url": "https://merchant.example/webhooks/payments",
  "success_url": "https://merchant.example/payment/success",
  "fail_url": "https://merchant.example/payment/failure",
  "customer": {
    "email": "[email protected]",
    "phone": "+37061234567"
  }
}' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const payment = await api('POST', '/api/payments/hosted', {
  "merchant_order_id": "ORDER-2026-001",
  "amount": "100.00",
  "currency": "USD",
  "webhook_url": "https://merchant.example/webhooks/payments",
  "success_url": "https://merchant.example/payment/success",
  "fail_url": "https://merchant.example/payment/failure",
  "customer": {
    "email": "[email protected]",
    "phone": "+37061234567"
  }
}, key);
console.log(payment);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$payment = payniko_api('POST', '/api/payments/hosted', [
    "merchant_order_id" => 'ORDER-2026-001',
    "amount" => '100.00',
    "currency" => 'USD',
    "webhook_url" => 'https://merchant.example/webhooks/payments',
    "success_url" => 'https://merchant.example/payment/success',
    "fail_url" => 'https://merchant.example/payment/failure',
    "customer" => [
        "email" => '[email protected]',
        "phone" => '+37061234567',
    ],
], $key);
print_r($payment);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
payment = api("POST", "/api/payments/hosted", {
    "merchant_order_id": "ORDER-2026-001",
    "amount": "100.00",
    "currency": "USD",
    "webhook_url": "https://merchant.example/webhooks/payments",
    "success_url": "https://merchant.example/payment/success",
    "fail_url": "https://merchant.example/payment/failure",
    "customer": {
        "email": "[email protected]",
        "phone": "+37061234567"
    }
}, key)
print(payment)

Antwort · 201

Liefert { "data": Payment } mit status: requires_action und next_action.url.

Antwort · Beispiel

{
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "requires_action",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": {
      "type": "redirect",
      "url": "https://payniko.com/l/example-checkout",
      "expires_at": "2026-11-02T12:00:00.000Z"
    },
    "card": null,
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": null
  }
}

Weitere Antworten

  • 200: Derselbe Idempotenzschlüssel und derselbe Body wurden erneut gesendet. Die ursprüngliche Zahlung wird zurückgegeben, und es wird keine neue Zahlung erstellt.
  • 409 9000010: Dieser Idempotenzschlüssel wurde bereits mit einem anderen Body verwendet.
  • 409 9000039: Die erste Anfrage mit diesem Schlüssel wird noch verarbeitet. Senden Sie dieselbe Anfrage nach einer kurzen Wartezeit erneut.
  • 409 9000007: Für diese merchant_order_id existiert bereits eine Zahlung.
  • 422 9000005: Der Betrag liegt außerhalb Ihrer Kontolimits; limits enthält den zulässigen Bereich.
  • 422 1000001 mit customer, customer.email oder customer.phone in errors: Die Kontaktdaten fehlen oder sind ungültig.
  • 422 1000001, 1000020, 1000021, 9000008, 9000038: Ein Feld ist ungültig; Einzelheiten enthalten errors und integration.violations.

HTTP 422 · 9000005

{
  "type": "https://payniko.com/problems/9000005",
  "title": "Invalid amount",
  "status": 422,
  "detail": "The amount is outside the account limits.",
  "code": "9000005",
  "correlation_id": "6d1f0c2e-8a4b-4c7d-9e21-3f5a7b9c1d20",
  "limits": {
    "currency_code": "USD",
    "min_amount": "25.00",
    "max_amount": null
  }
}

Limits werden in der Hauptwährung des Systems (limits.currency_code) geprüft, nachdem Ihr Betrag umgerechnet wurde. max_amount: null bedeutet, dass keine Obergrenze besteht. Die Limitwerte in diesem Beispiel dienen nur der Veranschaulichung.

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.

FeldTypBeschreibung
payment_idstring · LUVEI-ABCD-123456Payniko-ID der Zahlung . Speichern Sie sie zusammen mit Ihrer Bestellung.
merchant_order_idstringIhre Bestellreferenz, genau wie übermittelt.
flowstringhosted für die Hosted API, direct für die Direct API.
hosteddirect
statusstringAktueller Status der Zahlung; siehe Statustabelle unten.
requires_actionprocessingsucceededfailedexpiredcancelled
amountstringBetrag mit zwei Nachkommastellen.
currencystringWährung des Betrags; die Hauptwährung des Systems, wenn keine angegeben wurde.
environmentstringUmgebung des Schlüssels, mit dem die Zahlung erstellt wurde.
sandboxlive
next_actionobject | nullDie nächste vom Zahler erforderliche Aktion. Nur gesetzt, solange status den Wert requires_action hat.
next_action.typestringImmer redirect.
next_action.urlstring · 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_atstring · date-timeFrist 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.
cardobject | nullMaskierte Kartendaten, sobald bekannt.
card.bin6string | nullErste sechs Ziffern.
card.last_fourstring | nullLetzte vier Ziffern.
card.networkstring | nullKartennetzwerk.
visamastercardamextroyunionpaydiscoverjcbdinersmaestro
card.bankstring | nullKartenausgebende Bank, sofern bekannt.
failureobject | nullGrund für das Fehlschlagen. Nur gesetzt, wenn status den Wert failed hat.
failure.codestringCode des Fehlergrunds zur Verwendung in Ihrer Programmlogik; siehe Liste unten.
failure.messagestringKurze Meldung, die sich zur Anzeige für den Zahler als reiner Text eignet.
created_atstring · date-timeZeitpunkt, zu dem die Zahlung erstellt wurde.
expires_atstring · date-timeFrist für den Abschluss der Zahlung.
completed_atstring · date-time | nullZeitpunkt, 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.

Zahlung · Beispiel

{
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "requires_action",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": {
      "type": "redirect",
      "url": "https://payniko.com/l/example-checkout",
      "expires_at": "2026-11-02T12:00:00.000Z"
    },
    "card": null,
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": null
  }
}

Status von Hosted-Zahlungen

StatusBedeutungEmpfohlene Maßnahme
requires_actionDie Zahlung wurde erstellt, und der Zahler hat sie noch nicht abgeschlossen.Leiten Sie den Zahler zu next_action.url weiter (bis next_action.expires_at).
processingDer Zahler hat bezahlt, die Antwort der Bank steht noch aus.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.
succeededDie Gelder wurden eingezogen.Führen Sie die Bestellung aus.
failedDie 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.
expiredDer Zahler hat die Zahlung nicht innerhalb von 24 Stunden abgeschlossen (expires_at). Bricht der Zahler die Zahlung vorzeitig ab, kann sie stattdessen mit failed enden.Wenn der Zahler weiterhin zahlen möchte, erstellen Sie eine neue Zahlung.
cancelledDie Zahlung wurde abgebrochen.Führen Sie die Bestellung nicht aus.
  • Eine vorläufige Ablehnung auf der Zahlungsseite ist nicht endgültig: Die Zahlung kann im Status requires_action verbleiben, und Sie erhalten möglicherweise payment.updated. Letztlich erhalten Sie genau ein endgültiges Ergebnis.
  • succeeded, failed, expired und cancelled sind Endstatus. Eine Zahlung im Status processing kann auch nach expires_at noch erfolgreich abgeschlossen werden.
  • 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.

CodeBedeutung
payment_declinedDie Bank hat die Zahlung abgelehnt.
do_not_honorDie Bank hat die Zahlung ohne Angabe von Gründen abgelehnt.
insufficient_fundsDie Karte weist keine ausreichende Deckung auf.
card_limit_reachedEin Kartenlimit wurde erreicht.
card_details_invalidDie Kartendaten sind ungültig.
card_expiry_invalidDas Ablaufdatum ist ungültig oder die Karte ist abgelaufen.
card_security_code_invalidDer CVV ist ungültig.
card_not_activeDie Karte ist gesperrt oder nicht aktiv.
card_brand_not_supportedDieses Kartennetzwerk wird nicht unterstützt.
three_ds_failedDie 3-D-Secure-Authentifizierung ist fehlgeschlagen oder wurde nicht abgeschlossen.
payment_not_permittedDie Karte ist für diese Zahlungsart nicht zugelassen.
amount_not_allowedDer Betrag ist für diese Karte nicht zulässig.
currency_not_supportedDie Währung wird für diese Karte nicht unterstützt.
payment_details_rejectedDie Bank hat die Zahlungsdaten abgelehnt.
duplicate_paymentDie Bank hat die Zahlung als Duplikat eingestuft.
payment_timed_outDie Zahlung wurde nicht rechtzeitig abgeschlossen.
bank_unavailableDie Bank war nicht erreichbar.
bank_requested_retryDie Bank hat um einen erneuten Versuch zu einem späteren Zeitpunkt gebeten.
service_unavailableDie 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.

Anforderungen an die Weiterleitung

  • Leiten Sie die gesamte Seite auf next_action.url weiter (303 oder window.location) oder öffnen Sie die URL in einem neuen Tab. Betten Sie sie nicht in einen iFrame ein; die Seite blockiert die Einbettung in Frames.
  • Verwenden Sie die URL vor next_action.expires_at. Ist sie abgelaufen, rufen Sie die Zahlung ab.
  • Die Sprache der Seite richtet sich nach den Browsereinstellungen des Zahlers; ein Parameter ist nicht erforderlich.

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 GET succeeded bestätigt.

FeldBeschreibung
payment_idDie Zahlungs-ID .
merchant_order_idIhre Bestellreferenz. Sie ist nicht signiert: Verwenden Sie sie nur, um die Bestellung zu finden, und gleichen Sie anhand von payment_id ab.
statusZahlungsstatus 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_codeNur vorhanden, wenn status den Wert failed hat; identisch mit failure.code.
tsUnix-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_versionGibt 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.
sigHMAC-SHA256 als Hexadezimalwert in Kleinbuchstaben, 64 Zeichen.

Zu signierende Zeichenkette

ts + "." + payment_id + "." + status + "." + failure_code   // failure_code empty: keep the last dot
  1. 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.

  2. Bilden Sie die Zeichenkette aus den URL-decodierten Werten. Fehlt failure_code, bleibt der abschließende Punkt erhalten.

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

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

FeldTypErforderlichBeschreibung
payment_idstring · pathErforderlichDie 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/LUVEI-PAYN-123456'
import {api} from './payniko.mjs';

const payment = await api('GET', '/api/payments/LUVEI-PAYN-123456');
console.log(payment);
<?php
require __DIR__ . '/payniko.php';

$payment = payniko_api('GET', '/api/payments/LUVEI-PAYN-123456');
print_r($payment);
from payniko import api

payment = api("GET", "/api/payments/LUVEI-PAYN-123456")
print(payment)

Mögliche Fehler

Zahlungs-Webhooks

Die folgenden Ereignisse werden an die webhook_url der Zahlung gesendet. Jedes Zahlungsereignis wird pro Zahlung höchstens einmal gesendet.

EreignisBedeutung
payment.succeededDer Status der Zahlung ist succeeded. Führen Sie die Bestellung aus.
payment.failedDie Zahlung wurde ohne Erfolg beendet: failed, expired oder cancelled. Siehe data.status.
payment.updatedEine Aktion des Zahlers ist noch erforderlich (requires_action), zum Beispiel nach einer vorläufigen Ablehnung. Dies ist kein endgültiges Ergebnis.
refund.* · payment.voided · dispute.*Nachfolgende Ereignisse zu dieser Zahlung (Rückerstattungen, Stornierungen, Anfechtungen). Siehe Webhooks.

payment.succeeded

{
  "event_id": "e0743f4c-a297-4bf0-8085-ceb7d9d5e181",
  "event_type": "payment.succeeded",
  "environment": "sandbox",
  "occurred_at": "2026-11-01T12:02:00.000Z",
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "succeeded",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": null,
    "card": {
      "bin6": "400000",
      "last_four": "0002",
      "network": "visa",
      "bank": null
    },
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": "2026-11-01T12:02:00.000Z"
  }
}

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

CodeHTTPBedeutungEmpfohlene Maßnahme
1000001422Die 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.
1000009422Idempotency-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 _ -.
9000010409Dieser Idempotency-Key wurde bereits für eine andere Anfrage verwendet, oder dieselbe signierte Anfrage wurde mit einem anderen Idempotenzschlüssel erneut gesendet.Wiederholen Sie die Anfrage mit demselben Idempotenzschlüssel und demselben Body; verwenden Sie einen neuen Idempotenzschlüssel nur für einen neuen Vorgang.
9000039409Die erste Anfrage mit diesem Idempotency-Key wird noch verarbeitet (Hosted API).Senden Sie dieselbe Anfrage nach einer kurzen Wartezeit erneut.
9000007409Für diese merchant_order_id existiert bereits eine Zahlung (Hosted API).Rufen Sie die bestehende Zahlung ab oder verwenden Sie eine neue Bestellreferenz.
9000005422Der Betrag liegt außerhalb Ihrer Kontolimits (Hosted API). limits enthält den zulässigen Bereich.Passen Sie den Betrag an.
1000020422Das Betragsformat ist ungültig.Senden Sie eine positive Zeichenkette mit zwei Nachkommastellen und ohne führende Nullen, zum Beispiel "100.00".
1000021422Die Währung wird nicht unterstützt.Verwenden Sie USD, EUR, AED oder TRY, oder lassen Sie currency weg.
9000008422webhook_url ist ungültig.Verwenden Sie eine öffentliche HTTPS-URL auf Port 443 mit einer Länge von höchstens 500 Zeichen.
9000038422success_url oder fail_url ist ungültig.Verwenden Sie eine HTTPS-URL mit einer Länge von höchstens 2048 Zeichen. In der Sandbox wird zusätzlich http://localhost akzeptiert.
9000040403Die Hosted API ist für Ihr Konto nicht freigeschaltet.Wenden Sie sich an den Support.
9000013409Der Sandbox-Vorgang ist nicht verfügbar, zum Beispiel wenn ein ausschließlich für die Sandbox bestimmter Endpoint mit einem Live-Schlüssel aufgerufen wird.Verwenden Sie einen Sandbox-Schlüssel oder wiederholen Sie die Anfrage zu einem späteren Zeitpunkt.
9000004403Ihr Konto ist für diesen Vorgang eingeschränkt.Wenden Sie sich an den Support.
3000001503Der Wechselkurs ist vorübergehend nicht verfügbar.Wiederholen Sie die Anfrage zu einem späteren Zeitpunkt.
1000011503Der Dienst ist vorübergehend nicht verfügbar.Wiederholen Sie dieselbe Anfrage. Rufen Sie bestehende Ressourcen ab, bevor Sie neue erstellen.
1000013503Diese Funktion ist noch nicht verfügbar.Wiederholen Sie die Anfrage nach retry_after Sekunden.
1000014503Es finden geplante Wartungsarbeiten statt. until und note enthalten Einzelheiten.Wiederholen Sie die Anfrage nach Ablauf des in Retry-After angegebenen Intervalls.

Eine Kartenablehnung ist kein HTTP-Fehler; die Zahlung endet mit failed und failure.code. Die vollständige Liste finden Sie unter Fehlercodes.

Tests mit Sandbox-Karten

Erstellen Sie die Zahlung mit Ihrem Sandbox-Schlüssel, öffnen Sie next_action.url und zahlen Sie mit einer der unten aufgeführten Karten, mit einem beliebigen Ablaufdatum in der Zukunft und einem beliebigen dreistelligen CVV. Einzelheiten finden Sie unter Sandbox und Testkarten.

KartennummerErgebnis
4000000000000010Ohne 3-D Secure genehmigt. Bei der Direct API liefert bereits die Antwort auf die Erstellungsanfrage succeeded.
4000000000000002 · 43550900000442423-D-Secure-Testschritt, anschließend succeeded.
4000000000000051 · 4355090000040000Vor 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 · 43550900000051513-D-Secure-Testschritt, anschließend failed mit insufficient_funds.
4000000000000044Keine Antwort der Bank und kein 3-D Secure; die Zahlung verbleibt im Status processing.
4355090000089999Hosted API: processing ohne 3-D-Secure-Schritt. Direct API: Test-3-D-Secure-Schritt, danach processing.

Vollständiges Beispiel

Ein minimaler Node.js-Server, der eine Zahlung erstellt, den Zahler weiterleitet und den Rücksprung sowie den Webhook verarbeitet. Er verwendet die Hilfsfunktion für Anfragen und die Prüffunktionen aus Webhooks und Rücksprung-URL.

server.mjs

// server.mjs: Hosted 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); };

http.createServer(async (req, res) => {
  const url = new URL(req.url, site);
  // 1. Create the payment, 2. redirect the payer.
  if (req.method === 'POST' && url.pathname === '/checkout') {
    const form = JSON.parse((await readBody(req)).toString('utf8')); // your checkout form: the payer's e-mail and phone
    const orderId = 'ORDER-' + Date.now();
    const key = randomUUID();
    orders.set(orderId, {key, status: 'new'});      // save the key before you call the API
    const {data} = await api('POST', '/api/payments/hosted', {
      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`,
    }, key);
    Object.assign(orders.get(orderId), {paymentId: data.payment_id, status: data.status});
    // next_action can be null for a moment: read the payment again before you redirect.
    res.writeHead(303, {Location: data.next_action ? data.next_action.url : `/orders/${orderId}`}).end();
    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);

Webhooks

Payniko sendet einen signierten POST an Ihre webhook_url, wenn sich der Status einer Zahlung, Rückerstattung, Reklamation oder Auszahlung ändert; der erste Versuch erfolgt in der Regel innerhalb einer Minute nach dem Ereignis. Prüfen Sie die Signatur, speichern Sie das Ereignis und antworten Sie mit 2xx, bevor Sie es verarbeiten.

Ereignisse

EreignisBedeutung
payment.succeededDer Status der Zahlung ist succeeded. Führen Sie die Bestellung aus.
payment.failedDie Zahlung wurde ohne Erfolg beendet: failed, expired oder cancelled. Siehe data.status.
payment.updatedEine Aktion des Zahlers ist noch erforderlich (requires_action), zum Beispiel nach einer vorläufigen Ablehnung. Dies ist kein endgültiges Ergebnis.
refund.completedEine Rückerstattung wurde abgeschlossen.
payment.voidedEine Stornierung wurde abgeschlossen.
refund.rejectedEine Rückerstattung oder Stornierung wurde abgelehnt.
refund.failedEine Rückerstattung oder Stornierung ist fehlgeschlagen.
refund.cancelledEine Rückerstattung oder Stornierung wurde abgebrochen.
dispute.openedEine Anfechtung wurde eröffnet.
dispute.in_defenseDie Anfechtung wird geprüft.
dispute.wonDie Anfechtung wurde zu Ihren Gunsten entschieden.
dispute.lostDie Anfechtung wurde zu Ihren Ungunsten entschieden.
withdrawal.sentEine Auszahlung wurde gesendet.
withdrawal.rejectedEine Auszahlung wurde abgelehnt.

Ereignisse zu Zahlungen, Rückerstattungen und Anfechtungen werden an die webhook_url der Zahlung gesendet, Auszahlungsereignisse an die webhook_url der Auszahlung. Jedes Zahlungsereignis wird pro Zahlung höchstens einmal gesendet, jedes Rückerstattungs-, Anfechtungs- oder Auszahlungsereignis höchstens einmal pro Rückerstattung, Anfechtung oder Auszahlung und Status.

Nutzdaten

FeldTypBeschreibung
event_idstring · uuidEindeutige ID des Ereignisses; identisch mit dem Header X-Event-Id.
event_typestringName des Ereignisses gemäß der Tabelle oben.
environmentstringsandbox oder live.
occurred_atstring · date-timeZeitpunkt, zu dem das Ereignis eingetreten ist.
subjectobjectNur bei Rückerstattungs-, Anfechtungs- und Auszahlungsereignissen vorhanden: type und id der Rückerstattung, Anfechtung oder Auszahlung.
dataobjectBei Zahlungsereignissen das Zahlungsobjekt (siehe Hosted API); bei anderen Ereignissen die unten aufgeführten Felder.

payment.succeeded

{
  "event_id": "e0743f4c-a297-4bf0-8085-ceb7d9d5e181",
  "event_type": "payment.succeeded",
  "environment": "sandbox",
  "occurred_at": "2026-11-01T12:02:00.000Z",
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "succeeded",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": null,
    "card": {
      "bin6": "400000",
      "last_four": "0002",
      "network": "visa",
      "bank": null
    },
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": "2026-11-01T12:02:00.000Z"
  }
}

Ereignisse zu Rückerstattungen, Anfechtungen und Auszahlungen

BezugsobjektFeld
refundid, status, operation_type, amount, currency, payment_id oder payment_code, merchant_order_id
disputeid, status, amount, currency, payment_id oder payment_code, merchant_order_id, reason, evidence_due_at (sofern gesetzt)
withdrawalid, status, amount, currency, usdt_amount, asset, network, tx_hash
  • Ereignisse zu Rückerstattungen und Anfechtungen enthalten genau eine Zahlungsreferenz: payment_id oder, bei manchen Ereignissen, payment_code. refund.completed enthält immer payment_id. Enthält ein Ereignis payment_code, ordnen Sie die Bestellung anhand von merchant_order_id zu.
  • Bevor Sie auf ein Ereignis reagieren, rufen Sie die Rückerstattung, Anfechtung oder Auszahlung per GET ab, um ihren aktuellen Status zu bestätigen.

refund.completed

{
  "event_id": "c4b2e7d1-8a3f-4e65-b0d9-5f1a2c3e4d70",
  "event_type": "refund.completed",
  "environment": "sandbox",
  "occurred_at": "2026-11-02T09:20:00.000Z",
  "subject": {
    "type": "refund",
    "id": "LUVEI-RFND-100001"
  },
  "data": {
    "id": "LUVEI-RFND-100001",
    "status": "completed",
    "operation_type": "refund",
    "amount": "100.00",
    "currency": "USD",
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001"
  }
}

withdrawal.sent

{
  "event_id": "b3e8f0a2-5c71-4d96-8e14-7a2c9d0f6b35",
  "event_type": "withdrawal.sent",
  "environment": "sandbox",
  "occurred_at": "2026-11-02T10:00:00.000Z",
  "subject": {
    "type": "withdrawal",
    "id": "LUVEI-WTHD-100002"
  },
  "data": {
    "id": "LUVEI-WTHD-100002",
    "status": "sent",
    "amount": "50.00",
    "currency": "USD",
    "usdt_amount": "49.950000",
    "asset": "USDT",
    "network": "TRC20",
    "tx_hash": "9f2b6c1d4e8a7f30b5c2d9e1a6f4b7c08d3e5a2f1b9c6d4e7a0f3b8c5d2e1f6a"
  }
}

Signatur prüfen

HeaderWert
X-Event-IdIdentisch mit event_id.
X-Environmentsandbox oder live. Verwenden Sie den geheimen Schlüssel dieser Umgebung.
X-TimestampUnix-Zeit dieses Zustellversuchs.
X-SignatureHMAC-SHA256 von X-Timestamp + "." + raw body als Hexadezimalwert in Kleinbuchstaben.
  1. Lesen Sie die unveränderten Bytes des Bodys ein, bevor Sie das JSON parsen.

  2. Prüfen Sie, dass X-Timestamp 10 Ziffern hat und höchstens 300 Sekunden von Ihrer Serverzeit abweicht.

  3. Berechnen Sie den HMAC-SHA256 von X-Timestamp + "." + raw body mit Ihrem geheimen Schlüssel und vergleichen Sie das Ergebnis in konstanter Zeit mit X-Signature.

  4. Schlägt die Prüfung fehl, lehnen Sie die Anfrage mit 401 ab.

// Node.js 20+: verify a Payniko notification before parsing it.
import {createHmac, timingSafeEqual} from 'node:crypto';

export function verifyWebhook(rawBody, headers, apiSecret, now = Math.floor(Date.now() / 1000)) {
  const timestamp = headers['x-timestamp'];
  const signature = headers['x-signature'];
  if (typeof timestamp !== 'string' || !/^[0-9]{10}$/.test(timestamp) ||
      typeof signature !== 'string' || !/^[0-9a-f]{64}$/.test(signature) ||
      Math.abs(now - Number(timestamp)) > 300) return false;
  const expected = createHmac('sha256', apiSecret).update(timestamp + '.').update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}

// Example with node:http - read the raw bytes first, never re-serialise the JSON.
// import http from 'node:http';
// http.createServer((req, res) => {
//   const chunks = [];
//   req.on('data', (c) => chunks.push(c)).on('end', () => {
//     const raw = Buffer.concat(chunks);
//     const secret = req.headers['x-environment'] === 'live' ? process.env.LIVE_API_SECRET : process.env.SANDBOX_API_SECRET;
//     if (!verifyWebhook(raw, req.headers, secret)) { res.writeHead(401).end(); return; }
//     const event = JSON.parse(raw.toString('utf8'));
//     // Save event.event_id (unique) and the event, then answer quickly.
//     res.writeHead(204).end();
//   });
// }).listen(8080);
<?php
// PHP 8.1+: verify a Payniko notification before parsing it.
function payniko_verify_webhook(string $rawBody, string $timestamp, string $signature, string $apiSecret, ?int $now = null): bool
{
    $now ??= time();
    if (!preg_match('/^[0-9]{10}$/', $timestamp) || !preg_match('/^[0-9a-f]{64}$/', $signature)) return false;
    if (abs($now - (int) $timestamp) > 300) return false;
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $apiSecret);
    return hash_equals($expected, $signature);
}

$raw = file_get_contents('php://input');            // raw bytes, before json_decode
$environment = $_SERVER['HTTP_X_ENVIRONMENT'] ?? '';
$secret = $environment === 'live' ? getenv('LIVE_API_SECRET') : getenv('SANDBOX_API_SECRET');
if (!payniko_verify_webhook($raw, $_SERVER['HTTP_X_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_SIGNATURE'] ?? '', (string) $secret)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true, 32, JSON_THROW_ON_ERROR);
// Save $event['event_id'] (unique) and the event, then answer quickly.
http_response_code(204);
# Python 3.9+: verify a Payniko notification before parsing it.
import hashlib, hmac, re, time


def verify_webhook(raw_body: bytes, headers, api_secret: str, now=None) -> bool:
    timestamp = headers.get("X-Timestamp", "")
    signature = headers.get("X-Signature", "")
    if not re.fullmatch(r"[0-9]{10}", timestamp) or not re.fullmatch(r"[0-9a-f]{64}", signature):
        return False
    if abs(int(now or time.time()) - int(timestamp)) > 300:
        return False
    expected = hmac.new(api_secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example:
# @app.post("/webhooks/payments")
# def payniko_webhook():
#     raw = request.get_data()                    # raw bytes, before JSON parsing
#     env = request.headers.get("X-Environment")
#     secret = os.environ["LIVE_API_SECRET" if env == "live" else "SANDBOX_API_SECRET"]
#     if not verify_webhook(raw, request.headers, secret):
#         return "", 401
#     event = json.loads(raw)
#     # Save event["event_id"] (unique) and the event, then answer quickly.
#     return "", 204

Jedes Ereignis nur einmal verarbeiten

  1. Speichern Sie event_id in einer Tabelle mit eindeutigem Index. Ist der Wert bereits vorhanden, antworten Sie mit 2xx und beenden die Verarbeitung.

  2. Antworten Sie innerhalb weniger Sekunden mit 2xx und erledigen Sie zeitaufwendige Aufgaben (E-Mails, Bestandsaktualisierungen) anschließend über eine Warteschlange.

  3. Ereignisse können verspätet, mehrfach oder in anderer Reihenfolge eintreffen. Eine Zahlung, die durch Zeitüberschreitung endet, kann eine Minute oder später gemeldet werden. Treffen Sie Entscheidungen anhand des Status in data oder rufen Sie das Objekt per GET erneut ab.

  4. Führen Sie eine Bestellung niemals nur deshalb aus, weil der Browser zu success_url zurückgekehrt ist.

Wiederholungen

  • Jede 2xx-Antwort innerhalb von etwa 10 Sekunden gilt als erfolgreiche Zustellung. Weiterleitungen werden nicht verfolgt und gelten als Fehlschlag.
  • Fehlgeschlagene Zustellungen werden nach etwa 1 Minute, 5 Minuten, 15 Minuten, 1 Stunde und 6 Stunden wiederholt. Body und X-Event-Id bleiben unverändert; X-Timestamp und X-Signature werden für jeden Versuch neu erzeugt.
  • Nach dem letzten Versuch wird die Zustellung als failed markiert. Bestimmte Fehler (ein Ziel mit privater IP-Adresse, ein fehlender Signaturschlüssel) werden nicht wiederholt; die Zustellung wird dann sofort als failed markiert. Fehlgeschlagene Zustellungen können über die Zustellungs-API erneut gesendet werden.
  • Ihre webhook_url muss bei jedem Versuch zu einer öffentlichen IP-Adresse aufgelöst werden.

Webhook-Zustellungen

Jeder gesendete Webhook wird als Zustellung gespeichert. Mit dieser API ermitteln Sie fehlgeschlagene Zustellungen und senden sie erneut.

GET/api/webhooks/deliveries

Zustellungen auflisten und abrufen

./payniko.sh GET '/api/webhooks/deliveries?status=failed&page=1&per_page=20'
import {api} from './payniko.mjs';

const deliveries = await api('GET', '/api/webhooks/deliveries?status=failed&page=1&per_page=20');
console.log(deliveries);
<?php
require __DIR__ . '/payniko.php';

$deliveries = payniko_api('GET', '/api/webhooks/deliveries?status=failed&page=1&per_page=20');
print_r($deliveries);
from payniko import api

deliveries = api("GET", "/api/webhooks/deliveries?status=failed&page=1&per_page=20")
print(deliveries)

Filtern Sie nach status (pending, delivered, failed) und paginieren Sie mit page und per_page. Die Liste enthält die Zustellungen der Umgebung Ihres Schlüssels. GET /api/webhooks/deliveries/{id} liefert zusätzlich url und payload.

FeldTypBeschreibung
idstring · uuidZustellungs-ID; identisch mit der event_id des Webhooks.
eventstringName des Ereignisses, zum Beispiel payment.succeeded.
statusstringStatus der Zustellung.
pendingdeliveredfailed
payment_idstring · LUVEI-ABCD-123456 | nullZugehörige Zahlung, sofern vorhanden.
subject_typestring | nullrefund, dispute oder withdrawal; null bei Zahlungsereignissen.
subject_idstring | nullID der Rückerstattung, Anfechtung oder Auszahlung.
attempt_countintegerAnzahl der bisherigen Versuche.
last_status_codeinteger | nullHTTP-Status, den Ihr Endpoint beim letzten Versuch zurückgegeben hat.
last_error_codestring | nullGrund für das Fehlschlagen des letzten Versuchs; siehe Tabelle unten.
last_error_messagestring | nullLesbare Beschreibung von last_error_code.
last_errorstring | nullFehlertext in freier Form, möglicherweise auf Türkisch. Verwenden Sie stattdessen last_error_code.
next_attempt_atstring · date-time | nullZeitpunkt des nächsten automatischen Versuchs.
delivered_atstring · date-time | nullZeitpunkt, zu dem Ihr Endpoint die Zustellung angenommen hat.
created_atstring · date-timeZeitpunkt, zu dem der Webhook erstellt wurde.
urlstringNur in der Detailantwort: die Ziel-URL.
payloadobject | nullNur in der Detailantwort: der gespeicherte Webhook-Body, neu codiert (Zahlungsreferenzen werden als payment_id dargestellt). Er entspricht nicht den signierten Bytes; prüfen Sie Signaturen nicht anhand dieses Werts.

Antwort · Beispiel

{
  "id": "0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70",
  "event": "payment.succeeded",
  "status": "failed",
  "payment_id": "LUVEI-PAYN-123456",
  "subject_type": null,
  "subject_id": null,
  "attempt_count": 6,
  "last_status_code": 500,
  "last_error": "Bildirim adresi başarısız yanıt verdi.",
  "last_error_code": "webhook_response_unsuccessful",
  "last_error_message": "Webhook delivery failed.",
  "next_attempt_at": null,
  "delivered_at": null,
  "created_at": "2026-11-01T12:02:00.000Z"
}
CodeBedeutung
webhook_response_unsuccessfulIhr Endpoint hat mit einem Status außerhalb von 2xx (siehe last_status_code) oder mit einer Weiterleitung geantwortet.
webhook_unreachableIhr Endpoint war nicht erreichbar (DNS, Verbindung oder Zeitüberschreitung).
invalid_webhook_targetDie gespeicherte URL ist keine gültige Webhook-URL.
webhook_target_private_networkDie URL wurde zu einer privaten oder lokalen IP-Adresse aufgelöst.
webhook_environment_invalidDie Umgebung des Webhooks konnte nicht ermittelt werden.
webhook_signing_unavailableDer Webhook konnte nicht signiert werden.
webhook_egress_proxy_unreachableEin vorübergehendes Zustellproblem auf Seiten von Payniko.
webhook_delivery_failedEin sonstiger Zustellfehler.
POST/api/webhooks/deliveries/{id}/retryIdempotency-Key erforderlich

Fehlgeschlagene Zustellung erneut senden

KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
./payniko.sh POST '/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry' '' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const delivery = await api('POST', '/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry', undefined, key);
console.log(delivery);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$delivery = payniko_api('POST', '/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry', null, $key);
print_r($delivery);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
delivery = api("POST", "/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry", None, key)
print(delivery)

Antwort · 200

Liefert die Zustellung mit status: pending.

  • Nur Zustellungen mit dem Status failed können erneut gesendet werden; für jede andere Zustellung lautet die Antwort 409 9000025.
  • Derselbe Webhook wird mit derselben id, derselben event_id und demselben Body erneut in die Warteschlange gestellt, und eine neue Versuchsreihe beginnt.
  • Senden Sie einen leeren Body oder {}.

Mögliche Fehler

Sandbox und Testkarten

Testen Sie Ihre Integration mit einem Sandbox-Schlüssel, bevor Sie in den Live-Betrieb wechseln. Die Sandbox bietet dieselbe API und dieselben Webhooks wie die Live-Umgebung, jedoch ohne echte Gelder und ohne echte Bankverarbeitung.

Funktionsweise der Sandbox

  • Es gibt keinen Umgebungsschalter: Mit einem Sandbox-Schlüssel signierte Anfragen werden in der Sandbox ausgeführt, mit einem Live-Schlüssel signierte Anfragen in der Live-Umgebung.
  • Die unten aufgeführten Karten funktionieren sowohl auf der Zahlungsseite als auch in Ihrem eigenen Zahlungsformular; echte Karten werden abgelehnt. Ihr eigenes Formular akzeptiert außerdem jede andere gültige Kartennummer mit der BIN 435509 oder 400000; auf einer Testseite wählen Sie dann Erfolg, Fehlschlag oder keine Antwort.
  • Die 3-D-Secure-Seiten sind Testseiten, und Kartennummern müssen eine gültige Prüfziffer haben.
  • Webhooks werden als echte HTTPS-Anfragen an Ihre webhook_url zugestellt und mit Ihrem geheimen Sandbox-Schlüssel signiert.

Testkarten

Die Karten verhalten sich bei der Hosted API und der Direct API gleich, sofern die Tabelle nichts anderes angibt. Verwenden Sie ein beliebiges zukünftiges Ablaufdatum und eine beliebige 3-stellige CVV.

KartennummerErgebnis
4000000000000010Ohne 3-D Secure genehmigt. Bei der Direct API liefert bereits die Antwort auf die Erstellungsanfrage succeeded.
4000000000000002 · 43550900000442423-D-Secure-Testschritt, anschließend succeeded.
4000000000000051 · 4355090000040000Vor 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 · 43550900000051513-D-Secure-Testschritt, anschließend failed mit insufficient_funds.
4000000000000044Keine Antwort der Bank und kein 3-D Secure; die Zahlung verbleibt im Status processing.
4355090000089999Hosted API: processing ohne 3-D-Secure-Schritt. Direct API: Test-3-D-Secure-Schritt, danach processing.
  • Mit jeder Testkarte verbleibt die Zahlung bei einem Betrag, der auf .77 endet (zum Beispiel 100.77), im Status processing (manuelle Prüfung).
  • Eine Testzahlung, die nie eine Antwort erhält, wird schließlich mit failed und service_unavailable geschlossen.
  • Eine Zahlung mit einer abgelehnten Testkarte (zum Beispiel 4000000000000051) kann einige Zeit im Status requires_action verbleiben, bevor sie einen Endstatus erreicht. Treffen Sie Entscheidungen anhand des Webhooks oder eines GET, nicht anhand einer vorläufigen Ablehnung.

Test-BINs

In der Sandbox liefert die BIN-Prüfung folgende Ergebnisse:

BINErgebnis
435509 · 400000200 mit payment_mode: direct.
540061200 mit payment_mode: hosted; der Zahler setzt den Vorgang auf der Sandbox-Zahlungsseite fort.
979210409 mit Code 0055230.
Alle anderen sechsstelligen Werte409 mit Code 0055233.

Rückerstattungen und Auszahlungen

In der Sandbox werden Rückerstattungen und Auszahlungen sofort gegen ein Testguthaben abgeschlossen. Wählen Sie das Ergebnis mit dem Header X-Sandbox-Scenario, der nicht Teil der Signatur ist. Verwenden Sie für jedes Szenario einen neuen Idempotency-Key: Der Header ist Teil des Idempotenz-Fingerabdrucks, und derselbe Schlüssel mit einem anderen Szenario wird mit 409 9000010 abgelehnt. Ein unbekannter Wert oder der Header mit einem Live-Schlüssel liefert 422 1000001; der Nachweis-Endpunkt akzeptiert den Header nicht. Das angewendete Szenario wird im Antwort-Header X-Sandbox-Scenario zurückgegeben.

Bevor das Szenario angewendet wird, setzt die Sandbox dieselben Regeln wie Live durch: Mindest- und Tageslimits für Auszahlungen, Wechselkurse (503 3000001, wenn nicht verfügbar) und Adressprüfung. Jede TRC20-Adresse mit gültiger Prüfsumme wird in der Sandbox akzeptiert, auch die TRON-Nulladresse, und es findet keine Überweisung statt; 1000030 (eine Adresse, die keine Auszahlungen empfangen kann) lässt sich in der Sandbox nicht reproduzieren.

EndpointWert
POST /api/account/refundscompleted (Standard) oder rejected
POST /api/account/withdrawalssent (Standard) oder rejected
KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
SANDBOX_SCENARIO=rejected ./payniko.sh POST '/api/account/refunds' '{
  "payment_id": "LUVEI-PAYN-123456",
  "operation_type": "refund",
  "reason": "Test refund"
}' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const refund = await api('POST', '/api/account/refunds', {
  "payment_id": "LUVEI-PAYN-123456",
  "operation_type": "refund",
  "reason": "Test refund"
}, key, {'X-Sandbox-Scenario': 'rejected'});
console.log(refund);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$refund = payniko_api('POST', '/api/account/refunds', [
    "payment_id" => 'LUVEI-PAYN-123456',
    "operation_type" => 'refund',
    "reason" => 'Test refund',
], $key, ['X-Sandbox-Scenario: rejected']);
print_r($refund);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
refund = api("POST", "/api/account/refunds", {
    "payment_id": "LUVEI-PAYN-123456",
    "operation_type": "refund",
    "reason": "Test refund"
}, key, {"X-Sandbox-Scenario": "rejected"})
print(refund)

Ein tx_hash aus der Sandbox ist simuliert und verweist auf keine echte Blockchain-Transaktion.

POST/api/sandbox/disputesIdempotency-Key erforderlichNur mit Sandbox-Schlüssel

Testanfechtung erstellen

Eröffnet eine Anfechtung zu einer erfolgreichen Sandbox-Zahlung, damit Sie Ihre Anfechtungsbearbeitung und Ihre Webhooks testen können. Nur mit einem Sandbox-Schlüssel verfügbar; ein Live-Schlüssel erhält 409 9000013.

FeldTypErforderlichBeschreibung
payment_idstring · LUVEI-ABCD-123456ErforderlichEine Ihrer Sandbox-Zahlungen mit dem Status succeeded.
reasonstringErforderlichGrund der Anfechtung.
fraudunauthorizedproduct_not_deliveredduplicateamount_mismatchother
scenariostringOptionalAnfangsstatus. Standard: open.
openwonlost
KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
./payniko.sh POST '/api/sandbox/disputes' '{
  "payment_id": "LUVEI-PAYN-123456",
  "reason": "product_not_delivered",
  "scenario": "open"
}' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const dispute = await api('POST', '/api/sandbox/disputes', {
  "payment_id": "LUVEI-PAYN-123456",
  "reason": "product_not_delivered",
  "scenario": "open"
}, key);
console.log(dispute);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$dispute = payniko_api('POST', '/api/sandbox/disputes', [
    "payment_id" => 'LUVEI-PAYN-123456',
    "reason" => 'product_not_delivered',
    "scenario" => 'open',
], $key);
print_r($dispute);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
dispute = api("POST", "/api/sandbox/disputes", {
    "payment_id": "LUVEI-PAYN-123456",
    "reason": "product_not_delivered",
    "scenario": "open"
}, key)
print(dispute)

Antwort · 201

Liefert das Anfechtungsobjekt. Eine Testanfechtung im Status open nimmt 7 Tage lang Antworten an; won und lost sind sofort endgültig. Der Status under_review kommt in der Sandbox nicht vor, und Testanfechtungen wirken sich nicht auf Ihr Guthaben aus.

Anfrageprotokoll

Die Seite Anfrageprotokoll listet Ihre letzten API-Anfragen mit Status, Fehlercode und correlation_id auf. Anfragen, die abgelehnt werden, bevor der Schlüssel identifiziert ist (ungültiger Schlüssel, ungültige Signatur oder ungültiger Händlercode), werden nicht aufgeführt. Sandbox-Zahlungen finden Sie unter Sandbox-API-Aufträge.

Fehlercodes

Fehler werden als application/problem+json zurückgegeben. Stützen Sie Ihre Programmlogik auf den HTTP-Status und den 7-stelligen code; title und detail sind für menschliche Leser bestimmt.

Format der Fehlerantwort

FeldBeschreibung
typeURL des Fehlertyps: https://payniko.com/problems/{code}.
titleKurze Zusammenfassung des Fehlers.
statusHTTP-Statuscode.
detailErläuterung des Fehlers in dieser Anfrage.
codeStabiler 7-stelliger Code; verwenden Sie ihn in Ihrer Programmlogik.
correlation_idIdentisch mit dem Header X-Correlation-Id. Geben Sie den Wert in Supportanfragen an.
errorsMeldungen auf Feldebene bei Validierungsfehlern: { "field": ["message"] }.
integrationIntegrationshinweise: stage, reason, violations (location, path, expected, hint), docs_url, retryable und in einigen Fällen retry_after.
metadataKann bei Fehlern der Direct API sowie bei Authentifizierungsfehlern und Fehlern aufgrund der Ratenbegrenzung vorhanden sein, mit retry_class und einem informativen developer_detail. Behandeln Sie das Feld als optional.
limitsZulässiger Betragsbereich bei 9000005. Bei 0055235 heißt das entsprechende Feld amount_limit.
retry_afterWartezeit in Sekunden; bei einigen 503-Fehlern vorhanden.

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": "9b8c7d6e-5f40-4a3b-8c2d-1e0f9a8b7c6d",
  "errors": {
    "merchant_order_id": [
      "This field is required."
    ]
  },
  "integration": {
    "stage": "validation",
    "reason": "validation_failed",
    "violations": [
      {
        "location": "body",
        "path": "merchant_order_id",
        "expected": {
          "type": "string",
          "min_length": 1,
          "max_length": 64,
          "unique_within": "account_and_environment"
        },
        "hint": "Check this field against the documented format."
      }
    ],
    "docs_url": "/en/developers#errors",
    "retryable": false
  }
}

Hinweise zu Wiederholungen

retry_classBedeutung
no_retryKorrigieren Sie die Anfrage, bevor Sie sie erneut senden.
retry_same_requestSenden Sie die identische Anfrage nach kurzer Wartezeit erneut (derselbe Idempotenzschlüssel, derselbe Body).
retry_afterWarten Sie das in Retry-After angegebene Intervall ab und senden Sie die Anfrage dann erneut.
read_orderRufen Sie die Zahlung per GET ab, bevor Sie über das weitere Vorgehen entscheiden.
  • retry_class ist nur in Antworten enthalten, die metadata tragen (Direct API, Authentifizierungs- und Ratenbegrenzungsfehler). Andere Fehler tragen stattdessen integration.retryable.
  • Erstellen Sie nach einer Zeitüberschreitung oder einem 5xx niemals eine neue Zahlung; rufen Sie die Zahlung ab oder senden Sie die Anfrage mit demselben Idempotenzschlüssel erneut.
  • Eine Kartenablehnung ist kein HTTP-Fehler: Die Zahlung wird mit 200, status: failed und failure zurückgegeben.
  • developer_detail kann sich ändern; stützen Sie Ihre Integrationslogik nicht darauf.

Alle Fehlercodes

Authentifizierung und Zugriff

CodeHTTPBedeutungEmpfohlene Maßnahme
0055502401 / 403Zugriff 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.
0055504401X-Timestamp oder X-Signature fehlt oder ist fehlerhaft, oder Ihre Serveruhr weicht um mehr als 300 Sekunden von der Serverzeit von Payniko ab.Korrigieren Sie Ihre Serveruhr (NTP) und signieren Sie die Anfrage mit einem aktuellen Zeitstempel neu.
0055505401Die Signatur stimmt nicht mit der Anfrage überein.Bilden Sie den Hashwert über genau die Bytes des Bodys, die Sie senden, signieren Sie den Pfad ohne Abfrageparameter und verwenden Sie den korrekten geheimen Schlüssel.
9000002401Die Anfrage war zum Zeitpunkt der Verarbeitung bereits abgelaufen.Signieren Sie die Anfrage mit einem aktuellen Zeitstempel neu und senden Sie sie erneut.
9000040403Die Hosted API ist für Ihr Konto nicht freigeschaltet.Wenden Sie sich an den Support.
1000004403Neue Zahlungen sind vorübergehend ausgesetzt.Wiederholen Sie die Anfrage zu einem späteren Zeitpunkt.
9000004403Ihr Konto ist für diesen Vorgang eingeschränkt.Wenden Sie sich an den Support.

Idempotenz und Konflikte

CodeHTTPBedeutungEmpfohlene Maßnahme
1000009422Idempotency-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 _ -.
9000010409Dieser Idempotency-Key wurde bereits für eine andere Anfrage verwendet, oder dieselbe signierte Anfrage wurde mit einem anderen Idempotenzschlüssel erneut gesendet.Wiederholen Sie die Anfrage mit demselben Idempotenzschlüssel und demselben Body; verwenden Sie einen neuen Idempotenzschlüssel nur für einen neuen Vorgang.
9000039409Die erste Anfrage mit diesem Idempotency-Key wird noch verarbeitet (Hosted API).Senden Sie dieselbe Anfrage nach einer kurzen Wartezeit erneut.
9000007409Für diese merchant_order_id existiert bereits eine Zahlung (Hosted API).Rufen Sie die bestehende Zahlung ab oder verwenden Sie eine neue Bestellreferenz.
0055503409Konflikt 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.

Validierung

CodeHTTPBedeutungEmpfohlene Maßnahme
1000001422Die 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.
1000020422Das Betragsformat ist ungültig.Senden Sie eine positive Zeichenkette mit zwei Nachkommastellen und ohne führende Nullen, zum Beispiel "100.00".
1000021422Die Währung wird nicht unterstützt.Verwenden Sie USD, EUR, AED oder TRY, oder lassen Sie currency weg.
1000023422Der Betrag ist zu hoch.Senden Sie einen niedrigeren Betrag.
9000008422webhook_url ist ungültig.Verwenden Sie eine öffentliche HTTPS-URL auf Port 443 mit einer Länge von höchstens 500 Zeichen.
9000038422success_url oder fail_url ist ungültig.Verwenden Sie eine HTTPS-URL mit einer Länge von höchstens 2048 Zeichen. In der Sandbox wird zusätzlich http://localhost akzeptiert.

Betrag, Karte und BIN

CodeHTTPBedeutungEmpfohlene Maßnahme
9000005422Der Betrag liegt außerhalb Ihrer Kontolimits (Hosted API). limits enthält den zulässigen Bereich.Passen Sie den Betrag an.
0055235422Der 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.
0055210422Die BIN ist nicht sechsstellig.Senden Sie die ersten sechs Ziffern der Kartennummer.
0055233409Die BIN ist unbekannt.Bitten Sie den Zahler um eine andere Karte. Es handelt sich nicht um eine Ablehnung durch die Bank.
0055230409Diese 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.
0055231409Diese Karte wird für diese Zahlung nicht akzeptiert.Bitten Sie den Zahler um eine andere Karte.
0055221409Ein Limit für diese Karte wurde erreicht.Verwenden Sie eine andere Karte oder wiederholen Sie den Vorgang zu einem späteren Zeitpunkt.
0055234409Die 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.
0055001409Ihr 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.
0055002403Kartenzahlungen sind für Ihr Konto nicht freigeschaltet.Wenden Sie sich an den Support.
9000013409Der Sandbox-Vorgang ist nicht verfügbar, zum Beispiel wenn ein ausschließlich für die Sandbox bestimmter Endpoint mit einem Live-Schlüssel aufgerufen wird.Verwenden Sie einen Sandbox-Schlüssel oder wiederholen Sie die Anfrage zu einem späteren Zeitpunkt.

Nicht gefunden

CodeHTTPBedeutungEmpfohlene Maßnahme
9000009404Die Zahlung existiert in diesem Konto und dieser Umgebung nicht, oder sie kann für diesen Vorgang nicht verwendet werden.Prüfen Sie die payment_id und verwenden Sie einen Schlüssel aus derselben Umgebung.
1000002404Nicht 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.
1000007405Die HTTP-Methode ist nicht zulässig. Allow listet die gültigen Methoden auf.Verwenden Sie die dokumentierte Methode.

Limits und Verfügbarkeit

CodeHTTPBedeutungEmpfohlene Maßnahme
1000005429Zu viele Anfragen oder eine vorübergehende Sperre nach wiederholten Signaturfehlern.Warten Sie das in Retry-After angegebene Intervall ab und wiederholen Sie die Anfrage dann mit zunehmenden Wartezeiten.
1000011503Der Dienst ist vorübergehend nicht verfügbar.Wiederholen Sie dieselbe Anfrage. Rufen Sie bestehende Ressourcen ab, bevor Sie neue erstellen.
1000013503Diese Funktion ist noch nicht verfügbar.Wiederholen Sie die Anfrage nach retry_after Sekunden.
1000014503Es finden geplante Wartungsarbeiten statt. until und note enthalten Einzelheiten.Wiederholen Sie die Anfrage nach Ablauf des in Retry-After angegebenen Intervalls.
3000001503Der Wechselkurs ist vorübergehend nicht verfügbar.Wiederholen Sie die Anfrage zu einem späteren Zeitpunkt.
1000006500Es ist ein unerwarteter Serverfehler aufgetreten.Wiederholen Sie dieselbe Anfrage oder rufen Sie die Ressource ab.

Kontovorgänge

CodeHTTPBedeutungEmpfohlene Maßnahme
9000014409Für diese Zahlung besteht eine offene oder abgeschlossene Rückerstattung oder Stornierung oder eine Anfechtung, die nicht gewonnen wurde.Rufen Sie die Rückerstattung oder die Anfechtung ab.
9000015404Die Anfechtung existiert nicht.Prüfen Sie die Anfechtungs-ID.
9000016409Die Anfechtung nimmt keine Antwort mehr an (sie ist geschlossen oder evidence_due_at ist überschritten).Rufen Sie die Anfechtung ab.
9000017422Die TRC20-Adresse ist ungültig.Prüfen Sie die Adresse gemeinsam mit ihrem Inhaber.
1000030422An diese Adresse sind keine Auszahlungen möglich.Verwenden Sie eine andere Adresse.
9000018422address_acknowledged ist nicht true.Bestätigen Sie die Adresse und senden Sie anschließend true.
9000019422Der Auszahlungsbetrag ist ungültig oder liegt unter dem Mindestbetrag.Passen Sie den Betrag an.
9000020409Das verfügbare Guthaben reicht nicht aus.Prüfen Sie das verfügbare Guthaben.
9000021409Das tägliche Auszahlungslimit wurde erreicht.Wiederholen Sie die Anfrage am nächsten Tag oder fordern Sie einen niedrigeren Betrag an.
9000022409Derzeit wird eine andere Auszahlung bearbeitet.Warten Sie und senden Sie anschließend dieselbe Anfrage erneut.
9000023404Die Auszahlung existiert nicht.Prüfen Sie die Auszahlungs-ID.
9000024404Die Webhook-Zustellung existiert nicht.Prüfen Sie die Zustellungs-ID.
9000025409Nur fehlgeschlagene Zustellungen können erneut gesendet werden.Prüfen Sie den Status der Zustellung.

Nur Sandbox

CodeHTTPBedeutungEmpfohlene Maßnahme
9000026503Die Sandbox ist für diesen Vorgang noch nicht verfügbar.Wiederholen Sie die Anfrage nach Ablauf des in Retry-After angegebenen Intervalls.
9000027409Das Testguthaben wird gerade von einem anderen Vorgang verwendet.Senden Sie dieselbe Anfrage nach kurzer Wartezeit erneut.
9000030409Das Testguthaben konnte nicht gutgeschrieben werden.Wiederholen Sie die Anfrage zu einem späteren Zeitpunkt.
9000033409Das Testguthaben reicht für diese Rückerstattung nicht aus. Eine Sandbox-Auszahlung mit unzureichendem Guthaben liefert stattdessen 9000020.Erstellen Sie zunächst weitere Testzahlungen.

Sprache der Meldungen

title und detail richten sich nach Accept-Language (en, tr, de, ar, es, el; de-AT wird als de behandelt). Bei jedem anderen Wert wird Englisch verwendet. Content-Language gibt die verwendete Sprache an.

Checkliste für den Live-Betrieb

Arbeiten Sie diese Checkliste ab, bevor Sie echte Zahlungen annehmen.

  • In der Sandbox wurde eine Zahlung mit succeeded und eine weitere mit failed abgeschlossen, und Ihre Bestellstatus wurden entsprechend aktualisiert.

  • Ihr Webhook-Endpoint prüft die Signatur anhand des unveränderten Bodys, lehnt veraltete Zeitstempel ab, speichert jede event_id nur einmal und antwortet umgehend mit 2xx.

  • Sie führen Bestellungen erst aus, wenn ein geprüfter Webhook oder ein GET succeeded anzeigt, niemals aufgrund des Rücksprungs im Browser.

  • Jeder POST speichert seinen Idempotency-Key vor dem Senden und verwendet ihn nach Netzwerkfehlern erneut.

  • Ihr Code behandelt requires_action, processing, 429 und 5xx, ohne doppelte Zahlungen zu erstellen.

  • Live-Schlüssel und geheimer Schlüssel sind ausschließlich in Ihrem sicheren Schlüsselspeicher hinterlegt. Der Austausch des Sandbox-Schlüssels gegen den Live-Schlüssel ist die einzige Codeänderung.

  • webhook_url, success_url und fail_url verweisen auf die HTTPS-Adressen Ihrer Produktionsumgebung.

  • Sie protokollieren X-Correlation-Id für jede Anfrage.

Prüfen Sie die Integration vor echten Zahlungen anhand dieser Liste, auch wenn die Schlüssel bereit sind. Ein Test ist keine Voraussetzung für die Schlüsselerstellung.